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.");
}
}
}