From 7fb26639f5d7bcc5369651675d4c401e6406273d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 20 Feb 2024 11:48:55 +0600 Subject: [PATCH] GPU interop features now don't require to skip the first frame and available earlier in general (#14651) --- .../Avalonia.Android/AndroidPlatform.cs | 1 + .../Platform/IPlatformRenderInterface.cs | 5 ++ .../Rendering/Composition/Compositor.cs | 64 +++++++++++++------ .../Server/ServerCompositor.UserApis.cs | 50 +++++++++++++++ .../Composition/Server/ServerCompositor.cs | 6 ++ .../PlatformRenderInterfaceContextManager.cs | 11 +++- src/Avalonia.Native/AvaloniaNativePlatform.cs | 1 + src/Avalonia.X11/X11Platform.cs | 1 + .../HeadlessPlatformRenderInterface.cs | 1 + src/Skia/Avalonia.Skia/SkiaBackendContext.cs | 18 ++++++ .../Avalonia.Direct2D1/Direct2D1Platform.cs | 1 + src/Windows/Avalonia.Win32/Win32Platform.cs | 1 + src/iOS/Avalonia.iOS/Platform.cs | 1 + 13 files changed, 139 insertions(+), 22 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index b991d8067f..f3dcb3560c 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -90,6 +90,7 @@ namespace Avalonia.Android } Compositor = new Compositor(graphics); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Compositor); } private static IPlatformGraphics InitializeGraphics(AndroidPlatformOptions opts) diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 57fedb3d69..c783a5ea65 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -217,5 +217,10 @@ namespace Avalonia.Platform /// Indicates that the context is no longer usable. This method should be thread-safe /// bool IsLost { get; } + + /// + /// Exposes features that should be available for consumption while context isn't active (e. g. from the UI thread) + /// + IReadOnlyDictionary PublicFeatures { get; } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 24817d7865..b8b0d73679 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Animation; using Avalonia.Animation.Easings; +using Avalonia.Controls; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.Platform; @@ -256,33 +257,56 @@ namespace Avalonia.Rendering.Composition return tcs.Task; } + internal ValueTask> GetRenderInterfacePublicFeatures() + { + if (Server.AT_TryGetCachedRenderInterfaceFeatures() is { } rv) + return new(rv); + if (!Loop.RunsInBackground) + return new(Server.RT_GetRenderInterfaceFeatures()); + return new(InvokeServerJobAsync(Server.RT_GetRenderInterfaceFeatures)); + } + /// /// Attempts to query for a feature from the platform render interface /// - public ValueTask TryGetRenderInterfaceFeature(Type featureType) => - new(InvokeServerJobAsync(() => - { - using (Server.RenderInterface.EnsureCurrent()) - { - return Server.RenderInterface.Value.TryGetFeature(featureType); - } - })); + public async ValueTask TryGetRenderInterfaceFeature(Type featureType) + { + (await GetRenderInterfacePublicFeatures().ConfigureAwait(false)).TryGetValue(featureType, out var rv); + return rv; + } + + /// + /// Attempts to query for GPU interop feature from the platform render interface + /// + /// + public async ValueTask TryGetCompositionGpuInterop() + { + var externalObjects = + (IExternalObjectsRenderInterfaceContextFeature?)await TryGetRenderInterfaceFeature( + typeof(IExternalObjectsRenderInterfaceContextFeature)).ConfigureAwait(false); - public ValueTask TryGetCompositionGpuInterop() => - new(InvokeServerJobAsync(() => - { - using (Server.RenderInterface.EnsureCurrent()) - { - var feature = Server.RenderInterface.Value - .TryGetFeature(); - if (feature == null) - return null; - return new CompositionInterop(this, feature); - } - })); + if (externalObjects == null) + return null; + return new CompositionInterop(this, externalObjects); + } internal bool UnitTestIsRegisteredForSerialization(ICompositorSerializable serializable) => _objectSerializationHashSet.Contains(serializable); + + /// + /// Attempts to get the Compositor instance that will be used by default for new s + /// created by the current platform backend. + /// + /// This won't work for every single platform backend and backend settings, e. g. with web we'll need to have + /// separate Compositor instances per output HTML canvas since they don't share OpenGL state. + /// Another case where default compositor won't be available is our planned multithreaded rendering mode + /// where each window would get its own Compositor instance + /// + /// This method is still useful for obtaining GPU device LUID to speed up initialization, but you should + /// always check if default Compositor matches one used by our control once it gets attached to a TopLevel + /// + /// + public static Compositor? TryGetDefaultCompositor() => AvaloniaLocator.Current.GetService(); } internal interface ICompositorScheduler diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs new file mode 100644 index 0000000000..bbdea64004 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositor +{ + private IReadOnlyDictionary? _renderInterfaceFeatureCache; + private readonly object _renderInterfaceFeaturesUserApiLock = new(); + + void RT_OnContextCreated(IPlatformRenderInterfaceContext context) + { + lock (_renderInterfaceFeaturesUserApiLock) + { + _renderInterfaceFeatureCache = null; + _renderInterfaceFeatureCache = context.PublicFeatures.ToDictionary(x => x.Key, x => x.Value); + } + } + + bool RT_OnContextLostExceptionFilterObserver(Exception e) + { + if (e is PlatformGraphicsContextLostException) + { + lock (_renderInterfaceFeaturesUserApiLock) + _renderInterfaceFeatureCache = null; + } + return false; + } + + void RT_OnContextDisposed() + { + lock (_renderInterfaceFeaturesUserApiLock) + _renderInterfaceFeatureCache = null; + } + + public IReadOnlyDictionary? AT_TryGetCachedRenderInterfaceFeatures() + { + lock (_renderInterfaceFeaturesUserApiLock) + return _renderInterfaceFeatureCache; + } + + public IReadOnlyDictionary RT_GetRenderInterfaceFeatures() + { + lock (_renderInterfaceFeaturesUserApiLock) + return _renderInterfaceFeatureCache ??= RenderInterface.Value.PublicFeatures; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index f1bc865475..c1f286e827 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -44,6 +44,8 @@ namespace Avalonia.Rendering.Composition.Server { _renderLoop = renderLoop; RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); + RenderInterface.ContextDisposed += RT_OnContextDisposed; + RenderInterface.ContextCreated += RT_OnContextCreated; BatchObjectPool = batchObjectPool; BatchMemoryPool = batchMemoryPool; _renderLoop.Add(this); @@ -187,6 +189,10 @@ namespace Avalonia.Rendering.Composition.Server _safeThread = Thread.CurrentThread; RenderCore(); } + catch (Exception e) when (RT_OnContextLostExceptionFilterObserver(e) && false) + // Will never get here, only using exception filter side effect + { + } finally { NotifyBatchesRendered(); diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index 82dcd8f184..d6576511b9 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -11,6 +11,8 @@ internal class PlatformRenderInterfaceContextManager private readonly IPlatformGraphics? _graphics; private IPlatformRenderInterfaceContext? _backend; private OwnedDisposable? _gpuContext; + public event Action? ContextDisposed; + public event Action? ContextCreated; public PlatformRenderInterfaceContextManager(IPlatformGraphics? graphics) { @@ -23,8 +25,12 @@ internal class PlatformRenderInterfaceContextManager { _backend?.Dispose(); _backend = null; - _gpuContext?.Dispose(); - _gpuContext = null; + if (_gpuContext != null) + { + _gpuContext?.Dispose(); + _gpuContext = null; + ContextDisposed?.Invoke(); + } if (_graphics != null) { @@ -36,6 +42,7 @@ internal class PlatformRenderInterfaceContextManager _backend = AvaloniaLocator.Current.GetRequiredService() .CreateBackendContext(_gpuContext?.Value); + ContextCreated?.Invoke(_backend); } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 3f9f5ad0b3..85331bea6b 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -161,6 +161,7 @@ namespace Avalonia.Native Compositor = new Compositor(_platformGraphics, true); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Compositor); AppDomain.CurrentDomain.ProcessExit += OnProcessExit; } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index b7444c69c6..75e0776b5e 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -98,6 +98,7 @@ namespace Avalonia.X11 } Compositor = new Compositor(graphics); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Compositor); } public IntPtr DeferredDisplay { get; set; } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 1d4bae7e6a..da46b45998 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -60,6 +60,7 @@ namespace Avalonia.Headless public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; + public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); public object? TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index 51e182f7e3..555c564f4a 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.OpenGL; using Avalonia.Platform; namespace Avalonia.Skia; @@ -14,6 +15,22 @@ internal class SkiaContext : IPlatformRenderInterfaceContext public SkiaContext(ISkiaGpu? gpu) { _gpu = gpu; + + var features = new Dictionary(); + + if (gpu != null) + { + void TryFeature() where T : class + { + if (gpu!.TryGetFeature() is { } feature) + features!.Add(typeof(T), feature); + } + // TODO12: extend ISkiaGpu with PublicFeatures instead + TryFeature(); + TryFeature(); + } + + PublicFeatures = features; } public void Dispose() @@ -44,6 +61,7 @@ internal class SkiaContext : IPlatformRenderInterfaceContext } public bool IsLost => _gpu?.IsLost ?? false; + public IReadOnlyDictionary PublicFeatures { get; } public object? TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 7750a8ed4e..1c2682607d 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -181,6 +181,7 @@ namespace Avalonia.Direct2D1 public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); public bool IsLost => false; + public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 3580426dfd..56da4ccb88 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -127,6 +127,7 @@ namespace Avalonia.Win32 AvaloniaLocator.CurrentMutable.Bind().ToSingleton(); s_compositor = new Compositor( platformGraphics); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_compositor); } public event EventHandler? ShutdownRequested; diff --git a/src/iOS/Avalonia.iOS/Platform.cs b/src/iOS/Avalonia.iOS/Platform.cs index 1192bb962c..07c212a01a 100644 --- a/src/iOS/Avalonia.iOS/Platform.cs +++ b/src/iOS/Avalonia.iOS/Platform.cs @@ -78,6 +78,7 @@ namespace Avalonia.iOS .Bind().ToConstant(keyboard); Compositor = new Compositor(AvaloniaLocator.Current.GetService()); + AvaloniaLocator.CurrentMutable.Bind().ToConstant(Compositor); } private static IPlatformGraphics InitializeGraphics(iOSPlatformOptions opts)