diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index c77d65ddf1..ff49dedb15 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -327,7 +327,7 @@ namespace ControlCatalog.Pages gl.ClearColor(0, 0, 0, 0); gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); gl.Enable(GL_DEPTH_TEST); - gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); + gl.Viewport(0, 0, FramebufferPixelSize.Width, FramebufferPixelSize.Height); var GL = gl; GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); diff --git a/src/Avalonia.Base/Rendering/AsyncSwapchainBase.cs b/src/Avalonia.Base/Rendering/AsyncSwapchainBase.cs new file mode 100644 index 0000000000..2369419f60 --- /dev/null +++ b/src/Avalonia.Base/Rendering/AsyncSwapchainBase.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Logging; +using Avalonia.Reactive; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Rendering; + +/// +/// A helper class for composition-backed swapchains, should not be a public API +/// +abstract class AsyncSwapchainBase : IAsyncDisposable where TImage : class +{ + private readonly int _queueLen; + private readonly string _logArea; + protected ICompositionGpuInterop Interop { get; } + protected CompositionDrawingSurface Target { get; } + private readonly Queue _imageQueue = new(); + private readonly Queue _pendingClearOperations = new(); + private PixelSize _size; + private bool _disposed; + + struct QueueElement + { + public Task Present; + public TImage Image; + } + + public AsyncSwapchainBase(ICompositionGpuInterop interop, CompositionDrawingSurface target, + PixelSize size, int queueLen, string logArea) + { + if (queueLen < 2) + throw new ArgumentOutOfRangeException(); + _queueLen = queueLen; + _logArea = logArea; + Interop = interop; + Target = target; + Resize(size); + } + + static bool IsBroken(QueueElement image) => image.Present.IsFaulted; + static bool IsReady(QueueElement image) => image.Present.Status == TaskStatus.RanToCompletion; + + TImage? CleanupAndFindNextImage() + { + while (_imageQueue.Count > 0 && IsBroken(_imageQueue.Peek())) + DisposeQueueItem(_imageQueue.Dequeue()); + + if (_imageQueue.Count < _queueLen) + return null; + + if (IsReady(_imageQueue.Peek())) + return _imageQueue.Dequeue().Image; + + return null; + } + + protected abstract TImage CreateImage(PixelSize size); + protected abstract ValueTask DisposeImage(TImage image); + protected abstract Task PresentImage(TImage image); + protected abstract void BeginDraw(TImage image); + + private (IDisposable session, TImage image, Task presented)? BeginDraw(bool forceCreateImage) + { + if (_disposed) + throw new ObjectDisposedException(null); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var img = CleanupAndFindNextImage(); + if (img == null) + { + if (_imageQueue.Count < _queueLen || forceCreateImage) + img = CreateImage(_size); + else + { + return null; + } + } + + BeginDraw(img); + return (Disposable.Create(() => + { + var presentTask = PresentImage(img); + // Synchronize the user-visible task + presentTask.ContinueWith(_ => + { + if (presentTask.Status == TaskStatus.Canceled) + tcs.SetCanceled(); + else if (presentTask.Status == TaskStatus.Faulted) + tcs.SetException(presentTask.Exception!); + else + tcs.SetResult(0); + }); + _imageQueue.Enqueue(new() + { + Present = presentTask, + Image = img + }); + }), img, tcs.Task); + } + + protected (IDisposable session, TImage image, Task presented) BeginDraw() => BeginDraw(true)!.Value; + + protected (IDisposable session, TImage image, Task presented)? TryBeginDraw() => BeginDraw(false); + + protected async ValueTask<(IDisposable session, TImage image, Task presented)> BeginDrawAsync() + { + while (true) + { + var session = BeginDraw(false); + if (session != null) + return session.Value; + try + { + await _imageQueue.Peek().Present!; + } + catch + { + // Ignore result, we just need to wait for it + } + } + } + + public void Resize(PixelSize size) + { + if (size.Width < 1 || size.Height < 1) + throw new ArgumentOutOfRangeException(); + if (size == _size) + return; + DisposeQueueItems(); + _size = size; + } + + + async ValueTask DisposeQueueItemCore(QueueElement img) + { + if (img.Present != null) + try + { + await img.Present; + } + catch + { + // Ignore + } + + try + { + await DisposeImage(img.Image); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, _logArea) + ?.Log(this, "Unable to dispose for swapchain image: {Exception}", e); + } + } + + async ValueTask DisposeQueueItemsCore(List images) + { + foreach (var img in images) + await DisposeQueueItemCore(img); + } + + void DisposeQueueItem(QueueElement image) + { + _pendingClearOperations.Enqueue(DisposeQueueItemCore(image)); + DrainPendingClearOperations(); + } + + void DrainPendingClearOperations() + { + while (_pendingClearOperations.Count > 0 && _pendingClearOperations.Peek().IsCompleted) + _pendingClearOperations.Dequeue().GetAwaiter().GetResult(); + } + + void DisposeQueueItems() + { + if (_imageQueue.Count == 0) + return; + + var images = _imageQueue.ToList(); + _imageQueue.Clear(); + _pendingClearOperations.Enqueue(DisposeQueueItemsCore(images)); + DrainPendingClearOperations(); + } + + public virtual async ValueTask DisposeAsync() + { + _disposed = true; + DisposeQueueItems(); + while (_pendingClearOperations.Count > 0) + await _pendingClearOperations.Dequeue(); + } +} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/Composition/CompositionGlContext.cs b/src/Avalonia.OpenGL/Composition/CompositionGlContext.cs new file mode 100644 index 0000000000..c0454471e5 --- /dev/null +++ b/src/Avalonia.OpenGL/Composition/CompositionGlContext.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.OpenGL.Controls; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL.Composition; + +abstract class CompositionGlContextBase : ICompositionGlContext +{ + protected readonly ICompositionGpuInterop Interop; + public Compositor Compositor { get; } + public IGlContext Context { get; } + private List _swapchains = new(); + + public abstract ICompositionGlSwapchain CreateSwapchain(CompositionDrawingSurface surface, PixelSize size, + Action? onDispose = null); + + internal CompositionGlContextBase( + Compositor compositor, + IGlContext context, + ICompositionGpuInterop interop) + { + Compositor = compositor; + Interop = interop; + Context = context; + } + + public ValueTask DisposeAsync() + { + if (Compositor.Dispatcher.CheckAccess()) + return DisposeAsyncCore(); + return new ValueTask(Compositor.Dispatcher.InvokeAsync(() => DisposeAsyncCore().AsTask())); + } + + private async ValueTask DisposeAsyncCore() + { + while (_swapchains.Count > 0) + { + var chain = _swapchains[_swapchains.Count - 1]; + // The swapchain will remove itself + await chain.DisposeAsync(); + } + Context.Dispose(); + } + + internal void RemoveSwapchain(CompositionGlSwapchain chain) + { + Compositor.Dispatcher.VerifyAccess(); + _swapchains.Remove(chain); + } + + internal void AddSwapchain(CompositionGlSwapchain chain) + { + Compositor.Dispatcher.VerifyAccess(); + _swapchains.Add(chain); + } +} + + +class CompositionGlContextViaContextSharing : CompositionGlContextBase +{ + private readonly IOpenGlTextureSharingRenderInterfaceContextFeature _sharing; + + public CompositionGlContextViaContextSharing( + Compositor compositor, + IGlContext context, + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature sharing) : base(compositor, context, interop) + { + _sharing = sharing; + } + + public override ICompositionGlSwapchain CreateSwapchain(CompositionDrawingSurface surface, PixelSize size, + Action? onDispose) + { + Compositor.Dispatcher.VerifyAccess(); + return new CompositionGlSwapchain(this, surface, Interop, + (imageSize, surface) => new CompositionOpenGlSwapChainImage(Context, _sharing, imageSize, Interop, surface), + size, 2, onDispose); + } +} + +class CompositionGlContextViaExternalObjects : CompositionGlContextBase +{ + private readonly IGlContextExternalObjectsFeature _externalObjectsFeature; + + // ReSharper disable once NotAccessedField.Local + // TODO: Implement + private readonly string _externalImageType; + // ReSharper disable once NotAccessedField.Local + // TODO: Implement + private readonly string? _externalSemaphoreType; + + private readonly CompositionGpuImportedImageSynchronizationCapabilities _syncMode; + + + public CompositionGlContextViaExternalObjects(Compositor compositor, IGlContext context, + ICompositionGpuInterop interop, IGlContextExternalObjectsFeature externalObjectsFeature, + string externalImageType, CompositionGpuImportedImageSynchronizationCapabilities syncMode, + string? externalSemaphoreType) : base(compositor, context, interop) + { + _externalObjectsFeature = externalObjectsFeature; + _externalImageType = externalImageType; + _syncMode = syncMode; + _externalSemaphoreType = externalSemaphoreType; + if (_syncMode != CompositionGpuImportedImageSynchronizationCapabilities.KeyedMutex) + throw new NotSupportedException("Only IDXGIKeyedMutex sync is supported for non-shared contexts"); + } + + public override ICompositionGlSwapchain CreateSwapchain(CompositionDrawingSurface surface, PixelSize size, Action? onDispose) + { + Compositor.Dispatcher.VerifyAccess(); + if (_syncMode == CompositionGpuImportedImageSynchronizationCapabilities.KeyedMutex) + + return new CompositionGlSwapchain(this, surface, Interop, + (imageSize, surface) => + new DxgiMutexOpenGlSwapChainImage(Interop, surface, _externalObjectsFeature, imageSize), + size, 2, onDispose); + + throw new System.NotSupportedException(); + } +} diff --git a/src/Avalonia.OpenGL/Composition/CompositionGlContextExtensions.cs b/src/Avalonia.OpenGL/Composition/CompositionGlContextExtensions.cs new file mode 100644 index 0000000000..aae2b704ee --- /dev/null +++ b/src/Avalonia.OpenGL/Composition/CompositionGlContextExtensions.cs @@ -0,0 +1,45 @@ +using System; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL.Composition; + +public static class CompositionGlContextExtensions +{ + public static ICompositionGlSwapchain CreateSwapchain(this ICompositionGlContext context, Visual visual, PixelSize size) + { + if (visual.CompositionVisual == null) + throw new InvalidOperationException("Visual isn't attached to composition tree"); + if (visual.CompositionVisual.Compositor != context.Compositor) + throw new InvalidOperationException("Visual is attached to a different compositor"); + + var surface = context.Compositor.CreateDrawingSurface(); + var surfaceVisual = context.Compositor.CreateSurfaceVisual(); + surfaceVisual.Surface = surface; + + void Resize() => surfaceVisual!.Size = new Vector(visual.Bounds.Width, visual.Bounds.Height); + ElementComposition.SetElementChildVisual(visual, surfaceVisual); + + void OnVisualOnPropertyChanged(object? s, AvaloniaPropertyChangedEventArgs e) => Resize(); + visual.PropertyChanged += OnVisualOnPropertyChanged; + + void Dispose() + { + visual.PropertyChanged -= OnVisualOnPropertyChanged; + ElementComposition.SetElementChildVisual(visual, null); + } + + Resize(); + bool success = false; + try + { + var res = context.CreateSwapchain(surface, size, Dispose); + success = true; + return res; + } + finally + { + if(!success) + Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/Composition/CompositionGlSwapchain.cs b/src/Avalonia.OpenGL/Composition/CompositionGlSwapchain.cs new file mode 100644 index 0000000000..be5f8068dc --- /dev/null +++ b/src/Avalonia.OpenGL/Composition/CompositionGlSwapchain.cs @@ -0,0 +1,139 @@ +using System; +using System.Threading.Tasks; +using Avalonia.OpenGL.Controls; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Threading; + +namespace Avalonia.OpenGL.Composition; + +class CompositionGlSwapchain : AsyncSwapchainBase, ICompositionGlSwapchain +{ + protected readonly IGlContext Context; + private readonly Func _imageFactory; + private readonly Action? _onDispose; + private readonly CompositionDrawingSurface _surface; + private readonly Dispatcher _dispatcher; + private readonly CompositionGlContextBase _parent; + + public CompositionGlSwapchain( + CompositionGlContextBase parentContext, + CompositionDrawingSurface surface, + ICompositionGpuInterop interop, + Func imageFactory, + PixelSize size, int queueLength, Action? onDispose) : base(interop, surface, size, queueLength, "OpenGL") + { + _parent = parentContext; + Context = parentContext.Context; + _imageFactory = imageFactory; + _onDispose = onDispose; + _surface = surface; + _dispatcher = _surface.Compositor.Dispatcher; + _parent.AddSwapchain(this); + } + + + public CompositionSurface Surface => _surface; + + class LockedTexture : ICompositionGlSwapchainLockedTexture + { + private IDisposable? _disposable; + + public Task Presented { get; } + public int TextureId { get; private set; } + + public LockedTexture((IDisposable disposable, IGlSwapchainImage texture, Task presented) res) + { + TextureId = res.texture.TextureId; + _disposable = res.disposable; + Presented = res.presented; + } + + public void Dispose() + { + _disposable?.Dispose(); + _disposable = null; + TextureId = 0; + } + + } + + ICompositionGlSwapchainLockedTexture? TryGetNextTextureCore() + { + var res = TryBeginDraw(); + if (res != null) + return new LockedTexture(res.Value); + return null; + } + + public ICompositionGlSwapchainLockedTexture? TryGetNextTexture() + { + _dispatcher.VerifyAccess(); + using (Context.EnsureCurrent()) + return TryGetNextTextureCore(); + } + + public async ValueTask GetNextTextureAsync() + { + _dispatcher.VerifyAccess(); + if (Context is IGlContextWithIsCurrentCheck currentCheck && currentCheck.IsCurrent) + throw new InvalidOperationException( + "You should not be calling _any_ asynchronous methods inside of MakeCurrent/EnsureCurrent blocks. Awaiting such method will result in a broken opengl state"); + + var res = await BeginDrawAsync(); + return new LockedTexture(res); + } + + public ICompositionGlSwapchainLockedTexture GetNextTextureIgnoringQueueLimits() + { + _dispatcher.VerifyAccess(); + using (Context.EnsureCurrent()) + { + return new LockedTexture(base.BeginDraw()); + } + } + + + protected override IGlSwapchainImage CreateImage(PixelSize size) + { + using (Context.EnsureCurrent()) + return _imageFactory(size, _surface); + } + + protected override async ValueTask DisposeImage(IGlSwapchainImage image) + { + await image.DisposeImportedAsync(); + using (Context.EnsureCurrent()) + image.DisposeTexture(); + } + + protected override Task PresentImage(IGlSwapchainImage image) + { + _dispatcher.VerifyAccess(); + using (Context.EnsureCurrent()) + { + Context.GlInterface.Flush(); + return image.Present(); + } + } + + protected override void BeginDraw(IGlSwapchainImage image) + { + using (Context.EnsureCurrent()) + image.BeginDraw(); + } + + public override ValueTask DisposeAsync() + { + if (!_dispatcher.CheckAccess()) + return new ValueTask(_dispatcher.InvokeAsync(() => DisposeAsyncCore().AsTask())); + return DisposeAsyncCore(); + } + + private async ValueTask DisposeAsyncCore() + { + _onDispose?.Invoke(); + await base.DisposeAsync(); + _parent.RemoveSwapchain(this); + } +} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs b/src/Avalonia.OpenGL/Composition/CompositionGlSwapchainImages.cs similarity index 61% rename from src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs rename to src/Avalonia.OpenGL/Composition/CompositionGlSwapchainImages.cs index 42b1af7b25..16f4cf9ac6 100644 --- a/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs +++ b/src/Avalonia.OpenGL/Composition/CompositionGlSwapchainImages.cs @@ -1,67 +1,25 @@ -using System; using System.Threading.Tasks; using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.Rendering.Composition; -namespace Avalonia.OpenGL.Controls; +namespace Avalonia.OpenGL.Composition; -internal class CompositionOpenGlSwapchain : SwapchainBase -{ - private readonly IGlContext _context; - private readonly IGlContextExternalObjectsFeature? _externalObjectsFeature; - private readonly IOpenGlTextureSharingRenderInterfaceContextFeature? _sharingFeature; - - public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, - IOpenGlTextureSharingRenderInterfaceContextFeature sharingFeature - ) : base(interop, target) - { - _context = context; - _sharingFeature = sharingFeature; - } - - public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, - IGlContextExternalObjectsFeature? externalObjectsFeature) : base(interop, target) - { - _context = context; - _externalObjectsFeature = externalObjectsFeature; - } - - - - protected override IGlSwapchainImage CreateImage(PixelSize size) - { - if (_sharingFeature != null) - return new CompositionOpenGlSwapChainImage(_context, _sharingFeature, size, Interop, Target); - return new DxgiMutexOpenGlSwapChainImage(Interop, Target, _externalObjectsFeature!, size); - } - - public IDisposable BeginDraw(PixelSize size, out IGlTexture texture) - { - var rv = BeginDrawCore(size, out var tex); - texture = tex; - return rv; - } -} - -internal interface IGlTexture +interface IGlSwapchainImage { int TextureId { get; } int InternalFormat { get; } PixelSize Size { get; } + ValueTask DisposeImportedAsync(); + void DisposeTexture(); + void BeginDraw(); + Task Present(); } - -interface IGlSwapchainImage : ISwapchainImage, IGlTexture -{ - -} internal class DxgiMutexOpenGlSwapChainImage : IGlSwapchainImage { private readonly ICompositionGpuInterop _interop; private readonly CompositionDrawingSurface _surface; private readonly IGlExportableExternalImageTexture _texture; - private Task? _lastPresent; private ICompositionImportedGpuImage? _imported; public DxgiMutexOpenGlSwapChainImage(ICompositionGpuInterop interop, CompositionDrawingSurface surface, @@ -72,7 +30,8 @@ internal class DxgiMutexOpenGlSwapChainImage : IGlSwapchainImage _texture = externalObjects.CreateImage(KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle, size, PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm); } - public async ValueTask DisposeAsync() + + public async ValueTask DisposeImportedAsync() { // The texture is already sent to the compositor, so we need to wait for its attempts to use the texture // before destroying it @@ -88,20 +47,20 @@ internal class DxgiMutexOpenGlSwapChainImage : IGlSwapchainImage // Ignore } } - _texture.Dispose(); } + public void DisposeTexture() => _texture.Dispose(); + public int TextureId => _texture.TextureId; public int InternalFormat => _texture.InternalFormat; public PixelSize Size => new(_texture.Properties.Width, _texture.Properties.Height); - public Task? LastPresent => _lastPresent; public void BeginDraw() => _texture.AcquireKeyedMutex(0); - public void Present() + public Task Present() { _texture.ReleaseKeyedMutex(1); _imported ??= _interop.ImportImage(_texture.GetHandle(), _texture.Properties); - _lastPresent = _surface.UpdateWithKeyedMutexAsync(_imported, 1, 0); + return _surface.UpdateWithKeyedMutexAsync(_imported, 1, 0); } } @@ -123,9 +82,8 @@ internal class CompositionOpenGlSwapChainImage : IGlSwapchainImage _target = target; _texture = sharingFeature.CreateSharedTextureForComposition(context, size); } - - public async ValueTask DisposeAsync() + public async ValueTask DisposeImportedAsync() { // The texture is already sent to the compositor, so we need to wait for its attempts to use the texture // before destroying it @@ -141,22 +99,21 @@ internal class CompositionOpenGlSwapChainImage : IGlSwapchainImage // Ignore } } - - _texture.Dispose(); } + public void DisposeTexture() => _texture.Dispose(); + public int TextureId => _texture.TextureId; public int InternalFormat => _texture.InternalFormat; public PixelSize Size => _texture.Size; - public Task? LastPresent { get; private set; } public void BeginDraw() { // No-op for texture sharing } - public void Present() + public Task Present() { _imported ??= _interop.ImportImage(_texture); - LastPresent = _target.UpdateAsync(_imported); + return _target.UpdateAsync(_imported); } } diff --git a/src/Avalonia.OpenGL/Composition/ICompositionGlContext.cs b/src/Avalonia.OpenGL/Composition/ICompositionGlContext.cs new file mode 100644 index 0000000000..e84ac04c87 --- /dev/null +++ b/src/Avalonia.OpenGL/Composition/ICompositionGlContext.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Metadata; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL.Composition; + +[NotClientImplementable, Unstable] +public interface ICompositionGlContext : IAsyncDisposable +{ + /// + /// The associated compositor + /// + Compositor Compositor { get; } + /// + /// The OpenGL context + /// + IGlContext Context { get; } + /// + /// Creates a swapchain that can draw into provided CompositionDrawingSurface instance + /// + /// The surface to draw into + /// The pixel size for the textures generated by the swapchain + /// The callback to be called when the swapchain is about to be disposed + ICompositionGlSwapchain CreateSwapchain(CompositionDrawingSurface surface, PixelSize size, Action? onDispose = null); +} + +[NotClientImplementable, Unstable] +public interface ICompositionGlSwapchain : IAsyncDisposable +{ + /// + /// Attempts to get the next texture in the swapchain. If all textures in the swapchain are currently queued for + /// presentation, returns null + /// + ICompositionGlSwapchainLockedTexture? TryGetNextTexture(); + /// + /// Gets the the next texture in the swapchain or extends the swapchain. + /// Note that calling this method without waiting for previous textures to be presented can introduce + /// high GPU resource usage + /// + ICompositionGlSwapchainLockedTexture GetNextTextureIgnoringQueueLimits(); + /// + /// Asynchronously gets the next texture from the swapchain once one becomes available + /// You should not be calling this method while your IGlContext is current + /// + ValueTask GetNextTextureAsync(); + /// + /// Resizes the swapchain to a new pixel size + /// + void Resize(PixelSize size); +} + +[NotClientImplementable, Unstable] +public interface ICompositionGlSwapchainLockedTexture : IDisposable +{ + /// + /// The task you can use to wait for presentation to complete on the render thread + /// + public Task Presented { get; } + + /// + /// The texture you are expected to render into. You can bind it to GL_COLOR_ATTACHMENT0 of your framebuffer. + /// Note that the texture must be unbound before this object is disposed + /// + public int TextureId { get; } +} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/Composition/OpenGLCompositionInterop.cs b/src/Avalonia.OpenGL/Composition/OpenGLCompositionInterop.cs new file mode 100644 index 0000000000..b41b73ee58 --- /dev/null +++ b/src/Avalonia.OpenGL/Composition/OpenGLCompositionInterop.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL.Composition; + +public static class OpenGLCompositionInterop +{ + /// + /// Attempts to create an OpenGL context that is usable with the provided compositor + /// + public static async ValueTask TryCreateCompatibleGlContextAsync(this Compositor compositor, + OpenGLCompositionInteropContextCreationOptions? options = null) + { + compositor.Dispatcher.VerifyAccess(); + options ??= new(); + var gpuInteropTask = compositor.TryGetCompositionGpuInterop(); + + var contextSharing = + (IOpenGlTextureSharingRenderInterfaceContextFeature?) + await compositor.TryGetRenderInterfaceFeature( + typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); + var interop = await gpuInteropTask; + + if (interop == null) + return null; + + if (contextSharing != null) + { + // If context sharing is advertised, we should always go for it + var context = contextSharing.CreateSharedContext(options.VersionPreferences); + if (context == null) + return null; + return new CompositionGlContextViaContextSharing(compositor, context, interop, contextSharing); + } + + if (interop.DeviceLuid == null && interop.DeviceUuid == null) + return null; + + if (AvaloniaLocator.Current.GetService() is {} factory) + { + IGlContext context; + try + { + context = factory.CreateContext(options.VersionPreferences); + } + catch + { + return null; + } + + bool success = false; + try + { + var externalObjects = context.TryGetFeature(); + if (externalObjects == null) + return null; + + var luidMatch = interop.DeviceLuid != null + && externalObjects.DeviceLuid != null && + interop.DeviceLuid.SequenceEqual(externalObjects.DeviceLuid); + var uuidMatch = interop.DeviceLuid != null + && externalObjects.DeviceLuid != null && + interop.DeviceLuid.SequenceEqual(externalObjects.DeviceLuid); + + if (!uuidMatch && !luidMatch) + return null; + + foreach (var imageType in externalObjects.SupportedExportableExternalImageTypes) + { + if(!interop.SupportedImageHandleTypes.Contains(imageType)) + continue; + + var clientCaps = externalObjects.GetSynchronizationCapabilities(imageType); + var serverCaps = interop.GetSynchronizationCapabilities(imageType); + var matchingCaps = clientCaps & serverCaps; + + var syncMode = + matchingCaps.HasFlag(CompositionGpuImportedImageSynchronizationCapabilities.Automatic) + ? CompositionGpuImportedImageSynchronizationCapabilities.Automatic + : matchingCaps.HasFlag(CompositionGpuImportedImageSynchronizationCapabilities.KeyedMutex) + ? CompositionGpuImportedImageSynchronizationCapabilities.KeyedMutex + : matchingCaps.HasFlag(CompositionGpuImportedImageSynchronizationCapabilities + .Semaphores) + ? CompositionGpuImportedImageSynchronizationCapabilities.Semaphores + : default; + if (syncMode == default) + continue; + + if (syncMode == CompositionGpuImportedImageSynchronizationCapabilities.Semaphores) + { + var semaphoreType = externalObjects.SupportedExportableExternalSemaphoreTypes + .FirstOrDefault(interop.SupportedSemaphoreTypes.Contains); + if(semaphoreType == null) + continue; + success = true; + return new CompositionGlContextViaExternalObjects(compositor, context, interop, + externalObjects, imageType, syncMode, semaphoreType); + } + success = true; + return new CompositionGlContextViaExternalObjects(compositor, context, interop, + externalObjects, + imageType, syncMode, null); + } + } + finally + { + if(!success) + context.Dispose(); + } + } + return null; + } +} + +public class OpenGLCompositionInteropContextCreationOptions +{ + public List? VersionPreferences { get; set; } +} \ No newline at end of file diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index ff86c79ea3..8516eb09a8 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -9,12 +9,15 @@ using Avalonia.Rendering.Composition; using Avalonia.VisualTree; using Avalonia.Platform; using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.ExceptionServices; +using Avalonia.OpenGL.Composition; +using Avalonia.Threading; namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control { - private CompositionSurfaceVisual? _visual; private readonly Action _update; private bool _updateQueued; private Task? _initialization; @@ -27,6 +30,36 @@ namespace Avalonia.OpenGL.Controls _update = Update; } + private bool ExecUserCode(Action cb) + { + try + { + cb(); + return true; + } + catch (Exception e) + { + var info = ExceptionDispatchInfo.Capture(e); + Dispatcher.UIThread.Post(() => info.Throw()); + return false; + } + } + + private bool ExecUserCode(Action cb, T arg) + { + try + { + cb(arg); + return true; + } + catch (Exception e) + { + var info = ExceptionDispatchInfo.Capture(e); + Dispatcher.UIThread.Post(() => info.Throw()); + return false; + } + } + private void DoCleanup() { if (_initialization is { Status: TaskStatus.RanToCompletion } && _resources != null) @@ -35,7 +68,7 @@ namespace Avalonia.OpenGL.Controls { using (_resources.Context.EnsureCurrent()) { - OnOpenGlDeinit(_resources.Context.GlInterface); + ExecUserCode(OnOpenGlDeinit, _resources.Context.GlInterface); } } catch(Exception e) @@ -45,10 +78,7 @@ namespace Avalonia.OpenGL.Controls } } - ElementComposition.SetElementChildVisual(this, null); - _updateQueued = false; - _visual = null; _resources?.DisposeAsync(); _resources = null; _initialization = null; @@ -66,72 +96,15 @@ namespace Avalonia.OpenGL.Controls _compositor = (this.GetVisualRoot()?.Renderer as IRendererWithCompositor)?.Compositor; RequestNextFrameRendering(); } - - [MemberNotNullWhen(true, nameof(_resources))] - private bool EnsureInitializedCore( - ICompositionGpuInterop interop, - IOpenGlTextureSharingRenderInterfaceContextFeature? contextSharingFeature) - { - var surface = _compositor!.CreateDrawingSurface(); - - IGlContext? ctx = null; - try - { - if (contextSharingFeature?.CanCreateSharedContext == true) - _resources = OpenGlControlBaseResources.TryCreate(surface, interop, contextSharingFeature); - - if(_resources == null) - { - var contextFactory = AvaloniaLocator.Current.GetRequiredService(); - ctx = contextFactory.CreateContext(null); - if (ctx.TryGetFeature(out var externalObjects)) - _resources = OpenGlControlBaseResources.TryCreate(ctx, surface, interop, externalObjects); - else - ctx.Dispose(); - } - - if(_resources == null) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: current platform does not support multithreaded context sharing and shared memory"); - return false; - } - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: {exception}", e); - ctx?.Dispose(); - return false; - } - - _visual = _compositor.CreateSurfaceVisual(); - _visual.Size = new Vector(Bounds.Width, Bounds.Height); - _visual.Surface = _resources.Surface; - ElementComposition.SetElementChildVisual(this, _visual); - return true; - - } - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - if (_visual != null && change.Property == BoundsProperty) - { - _visual.Size = new Vector(Bounds.Width, Bounds.Height); - RequestNextFrameRendering(); - } - - base.OnPropertyChanged(change); - } - private void ContextLost() { _initialization = null; _resources?.DisposeAsync(); - OnOpenGlLost(); + _resources = null; + ExecUserCode(OnOpenGlLost); } - [MemberNotNullWhen(true, nameof(_resources))] private bool EnsureInitialized() { if (_initialization != null) @@ -145,14 +118,21 @@ namespace Avalonia.OpenGL.Controls if (_initialization is { IsCompleted: false }) return false; - if (_resources!.Context.IsLost) - ContextLost(); - else - return true; - } + if (_resources != null) + { + if (_resources.Context.IsLost) + ContextLost(); + else + return true; + } + } + _initialization = InitializeAsync(); + if (_initialization.Status == TaskStatus.RanToCompletion) + return true; + async void ContinueOnInitialization() { try @@ -169,7 +149,6 @@ namespace Avalonia.OpenGL.Controls return false; } - private void Update() { @@ -178,44 +157,55 @@ namespace Avalonia.OpenGL.Controls return; if(!EnsureInitialized()) return; - using (_resources.BeginDraw(GetPixelSize(visualRoot))) - OnOpenGlRender(_resources.Context.GlInterface, _resources.Fbo); + using (_resources!.BeginDraw(FramebufferPixelSize)) + ExecUserCode(OpenGlRender); } private async Task InitializeAsync() { + _resources = null; if (_compositor == null) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", "Unable to obtain Compositor instance"); return false; } - - var gpuInteropTask = _compositor.TryGetCompositionGpuInterop(); - var contextSharingFeature = - (IOpenGlTextureSharingRenderInterfaceContextFeature?) - await _compositor.TryGetRenderInterfaceFeature( - typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); - var interop = await gpuInteropTask; + _resources = await OpenGlControlBaseResources.TryCreateAsync(_compositor, this, FramebufferPixelSize); + if (_resources == null) + return false; + - if (interop == null) + var success = false; + try + { + using (_resources.Context.EnsureCurrent()) + return success = ExecUserCode(OnOpenGlInit, _resources.Context.GlInterface); + } + catch(Exception e) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Compositor backend doesn't support GPU interop"); + "EnsureCurrent failed: {Exception}", e); + return false; } - - if (!EnsureInitializedCore(interop, contextSharingFeature)) + finally { - DoCleanup(); - return false; + if(!success) + await _resources.DisposeAsync(); } - using (_resources.Context.MakeCurrent()) - OnOpenGlInit(_resources.Context.GlInterface); - - return true; + } + + protected PixelSize FramebufferPixelSize + { + get + { + if (VisualRoot == null) + return new(1, 1); + var size = PixelSize.FromSize(Bounds.Size, VisualRoot.RenderScaling); + return new PixelSize(Math.Max(1, size.Width), Math.Max(1, size.Height)); + } } [Obsolete("Use RequestNextFrameRendering()"), EditorBrowsable(EditorBrowsableState.Never)] @@ -231,13 +221,6 @@ namespace Avalonia.OpenGL.Controls _compositor.RequestCompositionUpdate(_update); } } - - private PixelSize GetPixelSize(IRenderRoot visualRoot) - { - var scaling = visualRoot.RenderScaling; - return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), - Math.Max(1, (int)(Bounds.Height * scaling))); - } protected virtual void OnOpenGlInit(GlInterface gl) { @@ -253,6 +236,8 @@ namespace Avalonia.OpenGL.Controls { } + + private void OpenGlRender() => OnOpenGlRender(_resources!.Context.GlInterface, _resources.Fbo); protected abstract void OnOpenGlRender(GlInterface gl, int fb); } diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs index be862a3c52..76bdd57b60 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading.Tasks; using Avalonia.Logging; +using Avalonia.OpenGL.Composition; using Avalonia.Platform; using Avalonia.Reactive; using Avalonia.Rendering.Composition; @@ -10,65 +11,63 @@ namespace Avalonia.OpenGL.Controls; internal class OpenGlControlBaseResources : IAsyncDisposable { + private readonly ICompositionGlContext _context; + private readonly ICompositionGlSwapchain _swapchain; private int _depthBuffer; public int Fbo { get; private set; } private PixelSize _depthBufferSize; - public CompositionDrawingSurface Surface { get; } - private readonly CompositionOpenGlSwapchain _swapchain; - public IGlContext Context { get; private set; } + public IGlContext Context => _context.Context; - public static OpenGlControlBaseResources? TryCreate(CompositionDrawingSurface surface, - ICompositionGpuInterop interop, - IOpenGlTextureSharingRenderInterfaceContextFeature feature) + + public OpenGlControlBaseResources(ICompositionGlContext context, ICompositionGlSwapchain swapchain) { - IGlContext? context; - try - { - context = feature.CreateSharedContext(); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); - return null; - } + _context = context; + _swapchain = swapchain; + using (Context.EnsureCurrent()) + Fbo = Context.GlInterface.GenFramebuffer(); + } + public static async Task TryCreateAsync(Compositor compositor, Visual visual, PixelSize initialSize) + { + var context = await compositor.TryCreateCompatibleGlContextAsync(); + if (context == null) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create additional OpenGL context."); + "Compositor backend doesn't support OpenGL interop"); return null; } - - return new OpenGlControlBaseResources(context, surface, interop, feature, null); - } - - public static OpenGlControlBaseResources? TryCreate(IGlContext context, CompositionDrawingSurface surface, - ICompositionGpuInterop interop, IGlContextExternalObjectsFeature externalObjects) - { - if (!interop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes - .D3D11TextureGlobalSharedHandle) - || !externalObjects.SupportedExportableExternalImageTypes.Contains( - KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle)) - return null; - return new OpenGlControlBaseResources(context, surface, interop, null, externalObjects); - } - - private OpenGlControlBaseResources(IGlContext context, - CompositionDrawingSurface surface, - ICompositionGpuInterop interop, - IOpenGlTextureSharingRenderInterfaceContextFeature? feature, - IGlContextExternalObjectsFeature? externalObjects - ) - { - Context = context; - Surface = surface; - using (context.MakeCurrent()) - Fbo = context.GlInterface.GenFramebuffer(); - _swapchain = - feature != null ? - new CompositionOpenGlSwapchain(context, interop, Surface, feature) : - new CompositionOpenGlSwapchain(context, interop, Surface, externalObjects); + + bool success = false; + try + { + var swapchain = context.CreateSwapchain(visual, initialSize); + try + { + try + { + var rv = new OpenGlControlBaseResources(context, swapchain); + success = true; + return rv; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: {exception}", e); + return null; + } + } + finally + { + if (!success) + await swapchain.DisposeAsync(); + } + } + finally + { + if(!success) + await context.DisposeAsync(); + } } private void UpdateDepthRenderbuffer(PixelSize size) @@ -76,6 +75,8 @@ internal class OpenGlControlBaseResources : IAsyncDisposable if (size == _depthBufferSize && _depthBuffer != 0) return; + _swapchain.Resize(size); + var gl = Context.GlInterface; gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); if (_depthBuffer != 0) gl.DeleteRenderbuffer(_depthBuffer); @@ -93,7 +94,7 @@ internal class OpenGlControlBaseResources : IAsyncDisposable public IDisposable BeginDraw(PixelSize size) { var restoreContext = Context.EnsureCurrent(); - IDisposable? imagePresent = null; + ICompositionGlSwapchainLockedTexture? texture = null; var success = false; try { @@ -101,7 +102,7 @@ internal class OpenGlControlBaseResources : IAsyncDisposable Context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, Fbo); UpdateDepthRenderbuffer(size); - imagePresent = _swapchain.BeginDraw(size, out var texture); + texture = _swapchain.GetNextTextureIgnoringQueueLimits(); gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.TextureId, 0); var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); @@ -114,24 +115,13 @@ internal class OpenGlControlBaseResources : IAsyncDisposable } success = true; - return Disposable.Create(() => - { - try - { - Context.GlInterface.Flush(); - imagePresent.Dispose(); - } - finally - { - restoreContext.Dispose(); - } - }); + return texture; } finally { if (!success) { - imagePresent?.Dispose(); + texture?.Dispose(); restoreContext.Dispose(); } } @@ -159,13 +149,9 @@ internal class OpenGlControlBaseResources : IAsyncDisposable { // } - - Surface.Dispose(); await _swapchain.DisposeAsync(); - Context.Dispose(); - - Context = null!; + await _context.DisposeAsync(); } } } diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index ae729968ba..2249a57c91 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -8,7 +8,7 @@ using static Avalonia.OpenGL.Egl.EglConsts; namespace Avalonia.OpenGL.Egl { - public class EglContext : IGlContext + public class EglContext : IGlContext, IGlContextWithIsCurrentCheck { private readonly EglDisplay _disp; private readonly EglInterface _egl; diff --git a/src/Avalonia.OpenGL/IGlContext.cs b/src/Avalonia.OpenGL/IGlContext.cs index 63e766f448..bf1ac4cb9b 100644 --- a/src/Avalonia.OpenGL/IGlContext.cs +++ b/src/Avalonia.OpenGL/IGlContext.cs @@ -17,6 +17,12 @@ namespace Avalonia.OpenGL IGlContext? CreateSharedContext(IEnumerable? preferredVersions = null); } + // TODO12: Merge with IGlContext + public interface IGlContextWithIsCurrentCheck : IGlContext + { + bool IsCurrent { get; } + } + public interface IGlPlatformSurfaceRenderTargetFactory { bool CanRenderToSurface(IGlContext context, object surface); diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index 4801befbed..c7696b92f8 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -9,7 +9,7 @@ using Avalonia.Reactive; namespace Avalonia.X11.Glx { - internal class GlxContext : IGlContext + internal class GlxContext : IGlContext, IGlContextWithIsCurrentCheck { public IntPtr Handle { get; } public GlxInterface Glx { get; } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs index 999818d709..f243da73f4 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -10,7 +10,7 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32.OpenGl { - internal class WglContext : IGlContext + internal class WglContext : IGlContext, IGlContextWithIsCurrentCheck { private readonly object _lock = new(); private readonly WglContext? _sharedWith; @@ -57,7 +57,7 @@ namespace Avalonia.Win32.OpenGl public int SampleCount => 0; public int StencilSize { get; } - private bool IsCurrent => wglGetCurrentContext() == _context && wglGetCurrentDC() == _dc; + public bool IsCurrent => wglGetCurrentContext() == _context && wglGetCurrentDC() == _dc; public IDisposable MakeCurrent() { if (IsLost)