diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs index 3022f92ec5..c1c0a74542 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs @@ -26,7 +26,12 @@ internal unsafe class SkiaMetalApi // Make sure that skia is loaded GC.KeepAlive(new SKPaint()); - var dll = NativeLibraryEx.Load("libSkiaSharp", typeof(SKPaint).Assembly); + // https://github.com/mono/SkiaSharp/blob/25e70a390e2128e5a54d28795365bf9fdaa7161c/binding/SkiaSharp/SkiaApi.cs#L9-L13 + // Note, IsIOS also returns true on MacCatalyst. + var libSkiaSharpPath = OperatingSystemEx.IsIOS() || OperatingSystemEx.IsTvOS() ? + "@rpath/libSkiaSharp.framework/libSkiaSharp" : + "libSkiaSharp"; + var dll = NativeLibraryEx.Load(libSkiaSharpPath, typeof(SKPaint).Assembly); IntPtr address; @@ -75,7 +80,7 @@ internal unsafe class SkiaMetalApi var context = _gr_direct_context_make_metal_with_options(device, queue, pOptions); Marshal.FreeHGlobal(pOptions); if (context == IntPtr.Zero) - throw new ArgumentException(); + throw new InvalidOperationException("Unable to create GRContext from Metal device."); return (GRContext)_contextCtor.Invoke(new object[] { context, true }); } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index a51d3f2b28..b9b8b78c1e 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -19,7 +19,6 @@ using Avalonia.Rendering.Composition; using CoreAnimation; using Foundation; using ObjCRuntime; -using OpenGLES; using UIKit; using IInsetsManager = Avalonia.Controls.Platform.IInsetsManager; @@ -36,6 +35,7 @@ namespace Avalonia.iOS private TextInputMethodClient? _client; private IAvaloniaViewController? _controller; private IInputRoot? _inputRoot; + private MetalRenderTarget? _currentRenderTarget; public AvaloniaView() { @@ -47,23 +47,33 @@ namespace Avalonia.iOS _topLevel.StartRendering(); - InitEagl(); + InitLayerSurface(); MultipleTouchEnabled = true; } [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] [SupportedOSPlatform("ios")] [UnsupportedOSPlatform("maccatalyst")] - private void InitEagl() + private void InitLayerSurface() { - var l = (CAEAGLLayer)Layer; + var l = Layer; l.ContentsScale = UIScreen.MainScreen.Scale; l.Opaque = true; - l.DrawableProperties = new NSDictionary( - EAGLDrawableProperty.RetainedBacking, false, - EAGLDrawableProperty.ColorFormat, EAGLColorFormat.RGBA8 - ); - _topLevelImpl.Surfaces = new[] { new EaglLayerSurface(l) }; +#if !MACCATALYST + if (l is CAEAGLLayer eaglLayer) + { + eaglLayer.DrawableProperties = new NSDictionary( + OpenGLES.EAGLDrawableProperty.RetainedBacking, false, + OpenGLES.EAGLDrawableProperty.ColorFormat, OpenGLES.EAGLColorFormat.RGBA8 + ); + _topLevelImpl.Surfaces = new[] { new EaglLayerSurface(eaglLayer) }; + } + else +#endif + if (l is CAMetalLayer metalLayer) + { + _topLevelImpl.Surfaces = new[] { new MetalPlatformSurface(metalLayer, this) }; + } } /// @@ -239,7 +249,16 @@ namespace Avalonia.iOS [Export("layerClass")] public static Class LayerClass() { - return new Class(typeof(CAEAGLLayer)); +#if !MACCATALYST + if (Platform.Graphics is EaglPlatformGraphics) + { + return new Class(typeof(CAEAGLLayer)); + } + else +#endif + { + return new Class(typeof(CAMetalLayer)); + } } public override void TouchesBegan(NSSet touches, UIEvent? evt) => _touches.Handle(touches, evt); @@ -253,6 +272,12 @@ namespace Avalonia.iOS public override void LayoutSubviews() { _topLevelImpl.Resized?.Invoke(_topLevelImpl.ClientSize, WindowResizeReason.Layout); + if (_currentRenderTarget is not null) + { + _currentRenderTarget.PendingSize = new PixelSize((int)Bounds.Width, (int)Bounds.Height); + _currentRenderTarget.PendingScaling = Window.ContentScaleFactor; + } + base.LayoutSubviews(); } @@ -261,5 +286,10 @@ namespace Avalonia.iOS get => (Control?)_topLevel.Content; set => _topLevel.Content = value; } + + internal void SetRenderTarget(MetalRenderTarget target) + { + _currentRenderTarget = target; + } } } diff --git a/src/iOS/Avalonia.iOS/EaglDisplay.cs b/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs similarity index 89% rename from src/iOS/Avalonia.iOS/EaglDisplay.cs rename to src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs index f003c4c6d7..cbd7089f4c 100644 --- a/src/iOS/Avalonia.iOS/EaglDisplay.cs +++ b/src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs @@ -1,6 +1,8 @@ +#if !MACCATALYST using System; using System.Collections.Generic; using System.Runtime.Versioning; +using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Reactive; @@ -19,7 +21,7 @@ namespace Avalonia.iOS public GlContext Context { get; } public static GlVersion GlVersion { get; } = new(GlProfileType.OpenGLES, 3, 0); - public EaglPlatformGraphics() + private EaglPlatformGraphics() { const string path = "/System/Library/Frameworks/OpenGLES.framework/OpenGLES"; @@ -29,6 +31,19 @@ namespace Avalonia.iOS var iface = new GlInterface(GlVersion, proc => ObjCRuntime.Dlfcn.dlsym(libGl, proc)); Context = new(iface, null); } + + public static EaglPlatformGraphics TryCreate() + { + try + { + return new EaglPlatformGraphics(); + } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(null, "Unable to initialize EAGL-based rendering: {0}", e); + return null; + } + } } [ObsoletedOSPlatform("ios12.0", "Use 'Metal' instead.")] @@ -122,3 +137,4 @@ namespace Avalonia.iOS public object TryGetFeature(Type featureType) => null; } } +#endif diff --git a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs b/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs similarity index 95% rename from src/iOS/Avalonia.iOS/EaglLayerSurface.cs rename to src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs index 6323bb3acb..e62395c985 100644 --- a/src/iOS/Avalonia.iOS/EaglLayerSurface.cs +++ b/src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs @@ -1,10 +1,13 @@ - +#if !MACCATALYST using System; using System.Runtime.Versioning; using System.Threading; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using CoreAnimation; +using Foundation; +using OpenGLES; +using UIKit; namespace Avalonia.iOS { @@ -84,7 +87,7 @@ namespace Avalonia.iOS public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) { CheckThread(); - var ctx = Platform.GlFeature.Context; + var ctx = ((EaglPlatformGraphics)Platform.Graphics).Context; if (ctx != context) throw new InvalidOperationException("Platform surface is only usable with tha main context"); using (ctx.MakeCurrent()) @@ -97,3 +100,4 @@ namespace Avalonia.iOS } } } +#endif diff --git a/src/iOS/Avalonia.iOS/LayerFbo.cs b/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs similarity index 99% rename from src/iOS/Avalonia.iOS/LayerFbo.cs rename to src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs index d971858b6d..97a9692d1c 100644 --- a/src/iOS/Avalonia.iOS/LayerFbo.cs +++ b/src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs @@ -1,3 +1,4 @@ +#if !MACCATALYST using System; using System.Runtime.Versioning; using Avalonia.OpenGL; @@ -148,3 +149,4 @@ namespace Avalonia.iOS public double Scaling => _oldLayerScale; } } +#endif diff --git a/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs b/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs new file mode 100644 index 0000000000..796cc2a613 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalDevice.cs @@ -0,0 +1,32 @@ +using System; +using Avalonia.Metal; +using Avalonia.Utilities; +using Metal; + +namespace Avalonia.iOS; + +internal class MetalDevice : IMetalDevice +{ + private readonly DisposableLock _syncRoot = new(); + + public MetalDevice(IMTLDevice device) + { + Device = device; + Queue = device.CreateCommandQueue(); + } + + public IMTLDevice Device { get; } + public IMTLCommandQueue Queue { get; } + IntPtr IMetalDevice.Device => Device.Handle; + IntPtr IMetalDevice.CommandQueue => Queue.Handle; + + public bool IsLost => false; + + public IDisposable EnsureCurrent() => _syncRoot.Lock(); + public object TryGetFeature(Type featureType) => null; + + public void Dispose() + { + Queue.Dispose(); + } +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs b/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs new file mode 100644 index 0000000000..1587377fbd --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs @@ -0,0 +1,34 @@ +using System; +using Avalonia.Metal; +using CoreAnimation; + +namespace Avalonia.iOS; + +internal class MetalDrawingSession : IMetalPlatformSurfaceRenderingSession +{ + private readonly MetalDevice _device; + private readonly ICAMetalDrawable _drawable; + + public MetalDrawingSession(MetalDevice device, ICAMetalDrawable drawable, PixelSize size, double scaling) + { + _device = device; + _drawable = drawable; + Size = size; + Scaling = scaling; + Texture = _drawable.Texture.Handle; + } + + public void Dispose() + { + var buffer = _device.Queue.CommandBuffer(); + buffer.PresentDrawable(_drawable); + buffer.Commit(); + } + + public IntPtr Texture { get; } + public PixelSize Size { get; } + + public double Scaling { get; } + + public bool IsYFlipped => false; +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs new file mode 100644 index 0000000000..5947a13b0a --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs @@ -0,0 +1,43 @@ +using System; +using Avalonia.Platform; +using Metal; +using SkiaSharp; + +namespace Avalonia.iOS; +#nullable enable + +internal class MetalPlatformGraphics : IPlatformGraphics +{ + private MetalPlatformGraphics() + { + + } + + public bool UsesSharedContext => false; + public IPlatformGraphicsContext CreateContext() => new MetalDevice(MTLDevice.SystemDefault); + + public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); + + public static MetalPlatformGraphics? TryCreate() + { + var device = MTLDevice.SystemDefault; + if (device is null) + { + // Can be null on unsupported OS versions. + return null; + } + +#if !TVOS + using var queue = device.CreateCommandQueue(); + using var context = GRContext.CreateMetal(new GRMtlBackendContext { Device = device, Queue = queue }); + if (context is null) + { + // Can be null on macCatalyst because of older Skia bug. + // Fixed in SkiaSharp 3.0 + return null; + } +#endif + + return new MetalPlatformGraphics(); + } +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs b/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs new file mode 100644 index 0000000000..453c9992c9 --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs @@ -0,0 +1,25 @@ +using Avalonia.Metal; +using CoreAnimation; + +namespace Avalonia.iOS; + +internal class MetalPlatformSurface : IMetalPlatformSurface +{ + private readonly CAMetalLayer _layer; + private readonly AvaloniaView _avaloniaView; + + public MetalPlatformSurface(CAMetalLayer layer, AvaloniaView avaloniaView) + { + _layer = layer; + _avaloniaView = avaloniaView; + } + public IMetalPlatformSurfaceRenderTarget CreateMetalRenderTarget(IMetalDevice device) + { + var dev = (MetalDevice)device; + _layer.Device = dev.Device; + + var target = new MetalRenderTarget(_layer, dev); + _avaloniaView.SetRenderTarget(target); + return target; + } +} diff --git a/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs b/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs new file mode 100644 index 0000000000..f8a28154df --- /dev/null +++ b/src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs @@ -0,0 +1,41 @@ +using Avalonia.Metal; +using Avalonia.Platform; +using CoreAnimation; +using CoreGraphics; +using Foundation; + +namespace Avalonia.iOS; + +internal class MetalRenderTarget : IMetalPlatformSurfaceRenderTarget +{ + private readonly CAMetalLayer _layer; + private readonly MetalDevice _device; + private double _scaling = 1; + private PixelSize _size = new(1, 1); + + public MetalRenderTarget(CAMetalLayer layer, MetalDevice device) + { + _layer = layer; + _device = device; + } + + public double PendingScaling { get; set; } = 1; + public PixelSize PendingSize { get; set; } = new(1, 1); + public void Dispose() + { + } + + public IMetalPlatformSurfaceRenderingSession BeginRendering() + { + // Flush all existing rendering + var buffer = _device.Queue.CommandBuffer(); + buffer.Commit(); + buffer.WaitUntilCompleted(); + _size = PendingSize; + _scaling= PendingScaling; + _layer.DrawableSize = new CGSize(_size.Width, _size.Height); + + var drawable = _layer.NextDrawable() ?? throw new PlatformGraphicsContextLostException(); + return new MetalDrawingSession(_device, drawable, _size, _scaling); + } +} diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index bb61861596..e586052087 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -1,9 +1,8 @@ using System; - -using Avalonia.Controls; +using System.Collections.Generic; +using System.Linq; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; @@ -11,6 +10,26 @@ using Avalonia.Threading; namespace Avalonia { + public enum iOSRenderingMode + { + OpenGl = 1, + Metal + } + + public class iOSPlatformOptions + { + /// + /// Gets or sets Avalonia rendering modes with fallbacks. + /// The first element in the array has the highest priority. + /// The default value is: , . + /// + /// Thrown if no values were matched. + public IReadOnlyList RenderingMode { get; set; } = new[] + { + iOSRenderingMode.OpenGl, iOSRenderingMode.Metal + }; + } + public static class IOSApplicationExtensions { public static AppBuilder UseiOS(this AppBuilder builder) @@ -27,18 +46,21 @@ namespace Avalonia.iOS { static class Platform { - public static EaglPlatformGraphics GlFeature; + public static iOSPlatformOptions Options; + public static IPlatformGraphics Graphics; public static DisplayLinkTimer Timer; internal static Compositor Compositor { get; private set; } public static void Register() { - GlFeature ??= new EaglPlatformGraphics(); + Options = AvaloniaLocator.Current.GetService() ?? new iOSPlatformOptions(); + + Graphics = InitializeGraphics(Options); Timer ??= new DisplayLinkTimer(); var keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable - .Bind().ToConstant((IPlatformGraphics) GlFeature) + .Bind().ToConstant(Graphics) .Bind().ToConstant(new CursorFactoryStub()) .Bind().ToConstant(new WindowingPlatformStub()) .Bind().ToSingleton() @@ -48,7 +70,34 @@ namespace Avalonia.iOS .Bind().ToConstant(DispatcherImpl.Instance) .Bind().ToConstant(keyboard); - Compositor = new Compositor(AvaloniaLocator.Current.GetService()); + Compositor = new Compositor(AvaloniaLocator.Current.GetService()); + } + + private static IPlatformGraphics InitializeGraphics(iOSPlatformOptions opts) + { + if (opts.RenderingMode is null || !opts.RenderingMode.Any()) + { + throw new InvalidOperationException($"{nameof(iOSPlatformOptions)}.{nameof(iOSPlatformOptions.RenderingMode)} must not be empty or null"); + } + + foreach (var renderingMode in opts.RenderingMode) + { +#if !MACCATALYST + if (renderingMode == iOSRenderingMode.OpenGl + && EaglPlatformGraphics.TryCreate() is { } eaglGraphics) + { + return eaglGraphics; + } +#endif + + if (renderingMode == iOSRenderingMode.Metal + && MetalPlatformGraphics.TryCreate() is { } metalGraphics) + { + return metalGraphics; + } + } + + throw new InvalidOperationException($"{nameof(iOSPlatformOptions)}.{nameof(iOSPlatformOptions.RenderingMode)} has a value of \"{string.Join(", ", opts.RenderingMode)}\", but no options were applied."); } } }