Browse Source

Introduced a convenient API for OpenGL drawing

pull/14653/head
Nikita Tsukanov 2 years ago
parent
commit
f086a27a31
  1. 2
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  2. 195
      src/Avalonia.Base/Rendering/AsyncSwapchainBase.cs
  3. 124
      src/Avalonia.OpenGL/Composition/CompositionGlContext.cs
  4. 45
      src/Avalonia.OpenGL/Composition/CompositionGlContextExtensions.cs
  5. 139
      src/Avalonia.OpenGL/Composition/CompositionGlSwapchain.cs
  6. 77
      src/Avalonia.OpenGL/Composition/CompositionGlSwapchainImages.cs
  7. 66
      src/Avalonia.OpenGL/Composition/ICompositionGlContext.cs
  8. 121
      src/Avalonia.OpenGL/Composition/OpenGLCompositionInterop.cs
  9. 177
      src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs
  10. 122
      src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs
  11. 2
      src/Avalonia.OpenGL/Egl/EglContext.cs
  12. 6
      src/Avalonia.OpenGL/IGlContext.cs
  13. 2
      src/Avalonia.X11/Glx/GlxContext.cs
  14. 4
      src/Windows/Avalonia.Win32/OpenGl/WglContext.cs

2
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);

195
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;
/// <summary>
/// A helper class for composition-backed swapchains, should not be a public API
/// </summary>
abstract class AsyncSwapchainBase<TImage> : IAsyncDisposable where TImage : class
{
private readonly int _queueLen;
private readonly string _logArea;
protected ICompositionGpuInterop Interop { get; }
protected CompositionDrawingSurface Target { get; }
private readonly Queue<QueueElement> _imageQueue = new();
private readonly Queue<ValueTask> _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<int>(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<QueueElement> 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();
}
}

124
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<CompositionGlSwapchain> _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();
}
}

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

139
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<IGlSwapchainImage>, ICompositionGlSwapchain
{
protected readonly IGlContext Context;
private readonly Func<PixelSize, CompositionDrawingSurface, IGlSwapchainImage> _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<PixelSize, CompositionDrawingSurface, IGlSwapchainImage> 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<ICompositionGlSwapchainLockedTexture> 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);
}
}

77
src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs → 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<IGlSwapchainImage>
{
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);
}
}

66
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
{
/// <summary>
/// The associated compositor
/// </summary>
Compositor Compositor { get; }
/// <summary>
/// The OpenGL context
/// </summary>
IGlContext Context { get; }
/// <summary>
/// Creates a swapchain that can draw into provided CompositionDrawingSurface instance
/// </summary>
/// <param name="surface">The surface to draw into</param>
/// <param name="size">The pixel size for the textures generated by the swapchain</param>
/// <param name="onDispose">The callback to be called when the swapchain is about to be disposed</param>
ICompositionGlSwapchain CreateSwapchain(CompositionDrawingSurface surface, PixelSize size, Action? onDispose = null);
}
[NotClientImplementable, Unstable]
public interface ICompositionGlSwapchain : IAsyncDisposable
{
/// <summary>
/// Attempts to get the next texture in the swapchain. If all textures in the swapchain are currently queued for
/// presentation, returns null
/// </summary>
ICompositionGlSwapchainLockedTexture? TryGetNextTexture();
/// <summary>
/// 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
/// </summary>
ICompositionGlSwapchainLockedTexture GetNextTextureIgnoringQueueLimits();
/// <summary>
/// Asynchronously gets the next texture from the swapchain once one becomes available
/// You should not be calling this method while your IGlContext is current
/// </summary>
ValueTask<ICompositionGlSwapchainLockedTexture> GetNextTextureAsync();
/// <summary>
/// Resizes the swapchain to a new pixel size
/// </summary>
void Resize(PixelSize size);
}
[NotClientImplementable, Unstable]
public interface ICompositionGlSwapchainLockedTexture : IDisposable
{
/// <summary>
/// The task you can use to wait for presentation to complete on the render thread
/// </summary>
public Task Presented { get; }
/// <summary>
/// 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
/// </summary>
public int TextureId { get; }
}

121
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
{
/// <summary>
/// Attempts to create an OpenGL context that is usable with the provided compositor
/// </summary>
public static async ValueTask<ICompositionGlContext?> 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<IPlatformGraphicsOpenGlContextFactory>() is {} factory)
{
IGlContext context;
try
{
context = factory.CreateContext(options.VersionPreferences);
}
catch
{
return null;
}
bool success = false;
try
{
var externalObjects = context.TryGetFeature<IGlContextExternalObjectsFeature>();
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<GlVersion>? VersionPreferences { get; set; }
}

177
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<bool>? _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<T>(Action<T> 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<IPlatformGraphicsOpenGlContextFactory>();
ctx = contextFactory.CreateContext(null);
if (ctx.TryGetFeature<IGlContextExternalObjectsFeature>(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<bool> 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);
}

122
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<OpenGlControlBaseResources?> 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();
}
}
}

2
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;

6
src/Avalonia.OpenGL/IGlContext.cs

@ -17,6 +17,12 @@ namespace Avalonia.OpenGL
IGlContext? CreateSharedContext(IEnumerable<GlVersion>? preferredVersions = null);
}
// TODO12: Merge with IGlContext
public interface IGlContextWithIsCurrentCheck : IGlContext
{
bool IsCurrent { get; }
}
public interface IGlPlatformSurfaceRenderTargetFactory
{
bool CanRenderToSurface(IGlContext context, object surface);

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

4
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)

Loading…
Cancel
Save