Browse Source

Add initial metal support for iOS

pull/14196/head
Max Katz 2 years ago
parent
commit
ff978daba7
  1. 9
      src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalApi.cs
  2. 50
      src/iOS/Avalonia.iOS/AvaloniaView.cs
  3. 18
      src/iOS/Avalonia.iOS/Eagl/EaglDisplay.cs
  4. 8
      src/iOS/Avalonia.iOS/Eagl/EaglLayerSurface.cs
  5. 2
      src/iOS/Avalonia.iOS/Eagl/LayerFbo.cs
  6. 32
      src/iOS/Avalonia.iOS/Metal/MetalDevice.cs
  7. 34
      src/iOS/Avalonia.iOS/Metal/MetalDrawingSession.cs
  8. 43
      src/iOS/Avalonia.iOS/Metal/MetalPlatformGraphics.cs
  9. 25
      src/iOS/Avalonia.iOS/Metal/MetalPlatformSurface.cs
  10. 41
      src/iOS/Avalonia.iOS/Metal/MetalRenderTarget.cs
  11. 63
      src/iOS/Avalonia.iOS/Platform.cs

9
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 });
}

50
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) };
}
}
/// <inheritdoc />
@ -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;
}
}
}

18
src/iOS/Avalonia.iOS/EaglDisplay.cs → 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

8
src/iOS/Avalonia.iOS/EaglLayerSurface.cs → 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

2
src/iOS/Avalonia.iOS/LayerFbo.cs → 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

32
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();
}
}

34
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;
}

43
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();
}
}

25
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;
}
}

41
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);
}
}

63
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
{
/// <summary>
/// Gets or sets Avalonia rendering modes with fallbacks.
/// The first element in the array has the highest priority.
/// The default value is: <see cref="iOSRenderingMode.OpenGl"/>, <see cref="iOSRenderingMode.Metal"/>.
/// </summary>
/// <exception cref="System.InvalidOperationException">Thrown if no values were matched.</exception>
public IReadOnlyList<iOSRenderingMode> 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<iOSPlatformOptions>() ?? new iOSPlatformOptions();
Graphics = InitializeGraphics(Options);
Timer ??= new DisplayLinkTimer();
var keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IPlatformGraphics>().ToConstant((IPlatformGraphics) GlFeature)
.Bind<IPlatformGraphics>().ToConstant(Graphics)
.Bind<ICursorFactory>().ToConstant(new CursorFactoryStub())
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformStub())
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
@ -48,7 +70,34 @@ namespace Avalonia.iOS
.Bind<IDispatcherImpl>().ToConstant(DispatcherImpl.Instance)
.Bind<IKeyboardDevice>().ToConstant(keyboard);
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IPlatformGraphics>());
}
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.");
}
}
}

Loading…
Cancel
Save