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)