Skip to content

Commit

Permalink
Merge pull request #18850 from ramezgerges/glcanvaselement_minimum_ogl
Browse files Browse the repository at this point in the history
chore: add GLCanvasElement checks for minimum available OpenGL version
  • Loading branch information
ramezgerges authored Nov 25, 2024
2 parents 8a65542 + 809d33b commit 7288875
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 17 deletions.
8 changes: 6 additions & 2 deletions doc/articles/controls/GLCanvasElement.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ uid: Uno.Controls.GLCanvasElement
---

> [!IMPORTANT]
> This functionality is only available on WinAppSDK and Skia Desktop (`netX.0-desktop`) targets that are running on platforms with support for hardware acceleration. On Windows and Linux, OpenGL is used directly and on macOS, Metal is used through the [ANGLE](https://en.wikipedia.org/wiki/ANGLE_(software)) library.
> This functionality is only available on WinAppSDK and Skia Desktop (`netX.0-desktop`) targets that are running on platforms with support for hardware acceleration. On Windows and Linux, OpenGL 3.0+ is used directly and on macOS, Metal is used through the [ANGLE](https://en.wikipedia.org/wiki/ANGLE_(software)) library.
`GLCanvasElement` is a control for drawing 3D graphics with OpenGL. It can be enabled by adding the [`GLCanvas` UnoFeature](xref:Uno.Features.Uno.Sdk). The OpenGL APIs provided are provided by [Silk.NET](https://dotnet.github.io/Silk.NET/).

Expand All @@ -23,7 +23,7 @@ These three abstract methods take a `Silk.NET.OpenGL.GL` parameter that can be u

### The GLCanvasElement constructor

The protected constructor requires a `Func<Window>` argument that fetches the `Microsoft.UI.Xaml.Window` object that the `GLCanvasElement` belongs to. This function is required because WinUI doesn't yet provide a way to get the `Window` of a `FrameworkElement`. This parameter is ignored on Uno Platform and must be set to null. This function is only called while the `GLCanvasElement` is still in the visual tree.
The protected constructor requires a `Func<Window>` argument that fetches the `Microsoft.UI.Xaml.Window` object that the `GLCanvasElement` belongs to. This function is required because WinUI doesn't yet provide a way to get the `Window` of a `FrameworkElement`. This parameter is ignored on Uno Platform and can be set to null. This function is only called while the `GLCanvasElement` is still in the visual tree.

### The `Init` method

Expand All @@ -41,6 +41,10 @@ On MacOS, since OpenGL support is not natively present, we use [ANGLE](https://e

Additionally, `GLCanvasElement` has an `Invalidate` method that requests a redrawing of the `GLCanvasElement`, calling `RenderOverride` in the process. Note that `RenderOverride` will only be called once per `Invalidate` call and the output will be saved to be used in future frames. To update the output, you must call `Invalidate`. If you need to continuously update the output (e.g. in an animation), you can add an `Invalidate` call inside `RenderOverride`.

## Detecting errors

To detect errors in initializing the OpenGL environment, `GLCanvasElement` exposes an `IsGLInitializedProperty` dependency property that shows whether or nor the loading of the element and its OpenGL setup were successful. This property is only valid when the element is loaded, i.e. its `IsLoaded` property is true. When the element is not loaded, the value of `IsGLInitialized` will be null. `GLCanvasElement` implements `INotifyPropertyChanged`, so you can use this property in a data bindings, for example to set the visibility of a control as a fallback. Attempting to change this property is illegal.

## How to use Silk.NET

To learn more about using [Silk.NET](https://www.nuget.org/packages/Silk.NET.OpenGL/) as a C# binding for OpenGL, see the examples in the Silk.NET repository [here](https://github.com/dotnet/Silk.NET/tree/main/examples/CSharp). Note that the windowing and inputs APIs in Silk.NET are not relevant to `GLCanvasElement`, since we only use Silk.NET as an OpenGL binding library, not a windowing library.
Expand Down
Binary file removed src/AddIns/Uno.WinUI.Graphics3DGL/Assets/error.png
Binary file not shown.
124 changes: 109 additions & 15 deletions src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
using Microsoft.UI.Xaml;
Expand All @@ -19,7 +22,6 @@
#endif

#if WINAPPSDK
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
#else
using Uno.Foundation.Extensibility;
Expand All @@ -40,14 +42,17 @@ namespace Uno.WinUI.Graphics3DGL;
public abstract partial class GLCanvasElement : Grid, INativeContext
{
private const int BytesPerPixel = 4;
private static readonly BitmapImage _fallbackImage = new BitmapImage(new Uri("ms-appx:///Assets/error.png"));
private static readonly Dictionary<XamlRoot, INativeOpenGLWrapper> _xamlRootToWrapper = new();
private static readonly Dictionary<XamlRoot, INativeOpenGLWrapper?> _xamlRootToWrapper = new();

private static readonly (int major, int minor) _minVersion = (3, 0);

private readonly Func<Window>? _getWindowFunc;

// valid if and only if _loadedAtleastOnce and OpenGL is available on the running platform
private bool _changingGlInitialized;

// valid if and only if GLCanvasElement was loaded at least once and OpenGL is available on the running platform
private INativeOpenGLWrapper? _nativeOpenGlWrapper;
// These are valid if and only if IsLoaded
// These are valid if and only if IsLoaded and _nativeOpenGlWrapper is not null
private GL? _gl;
private WriteableBitmap? _backBuffer;
private FrameBufferDetails? _details;
Expand Down Expand Up @@ -88,8 +93,6 @@ public abstract partial class GLCanvasElement : Grid, INativeContext
/// </remarks>
protected abstract void RenderOverride(GL gl);

/// <param name="width">The width of the backing framebuffer.</param>
/// <param name="height">The height of the backing framebuffer.</param>
/// <param name="getWindowFunc">A function that returns the Window object that this element belongs to. This parameter is only used on WinUI. On Uno Platform, it can be set to null.</param>
#if WINAPPSDK
protected GLCanvasElement(Func<Window> getWindowFunc)
Expand All @@ -109,7 +112,7 @@ protected GLCanvasElement(Func<Window>? getWindowFunc)
SizeChanged += (_, _) => UpdateFramebuffer();
}

private static INativeOpenGLWrapper? GetOrCreateNativeOpenGlWrapper(XamlRoot xamlRoot, Func<Window>? getWindowFunc)
private static unsafe INativeOpenGLWrapper? GetOrCreateNativeOpenGlWrapper(XamlRoot xamlRoot, Func<Window>? getWindowFunc)
{
try
{
Expand All @@ -119,14 +122,66 @@ protected GLCanvasElement(Func<Window>? getWindowFunc)
#if WINAPPSDK
nativeOpenGlWrapper = new WinUINativeOpenGLWrapper(xamlRoot, getWindowFunc!);
#else
if (!ApiExtensibility.CreateInstance<INativeOpenGLWrapper>(xamlRoot, out nativeOpenGlWrapper))
if (!ApiExtensibility.CreateInstance(xamlRoot, out nativeOpenGlWrapper))
{
throw new InvalidOperationException($"Couldn't create a {nameof(INativeOpenGLWrapper)} object. Make sure you are running on a platform with OpenGL support.");
if (typeof(GLCanvasElement).Log().IsEnabled(LogLevel.Error))
{
typeof(GLCanvasElement).Log().Error($"Couldn't create a {nameof(INativeOpenGLWrapper)} object. Make sure you are running on a platform with OpenGL support.");
}

_xamlRootToWrapper[xamlRoot] = null;
return null;
}
#endif

var abort = false;
using (nativeOpenGlWrapper.MakeCurrent())
{
var glGetString = (delegate* unmanaged[Cdecl]<GLEnum, byte*>)nativeOpenGlWrapper.GetProcAddress("glGetString");

var glVersionBytePtr = glGetString(GLEnum.Version);
var glVersionString = Marshal.PtrToStringUTF8((IntPtr)glVersionBytePtr);

if (typeof(GLCanvasElement).Log().IsEnabled(LogLevel.Information))
{
typeof(GLCanvasElement).Log().Info($"{nameof(GLCanvasElement)} created an OpenGL context with a version string = '{glVersionString}'.");
}

if (glVersionString?.Contains("ANGLE", StringComparison.Ordinal) ?? false)
{
if (typeof(GLCanvasElement).Log().IsEnabled(LogLevel.Warning))
{
typeof(GLCanvasElement).Log().Warn($"{nameof(GLCanvasElement)} is using an ANGLE implementation, ignoring minimum version checks.");
}
}
else
{
var glGetIntegerv = (delegate* unmanaged[Cdecl]<GLEnum, int*, void>)nativeOpenGlWrapper.GetProcAddress("glGetIntegerv");
int major, minor;
glGetIntegerv(GLEnum.MajorVersion, &major);
glGetIntegerv(GLEnum.MinorVersion, &minor);

if (major < _minVersion.major || (major == _minVersion.major && minor < _minVersion.minor))
{
if (typeof(GLCanvasElement).Log().IsEnabled(LogLevel.Error))
{
typeof(GLCanvasElement).Log().Error($"{nameof(GLCanvasElement)} requires at least {_minVersion.major}.{_minVersion.minor}, but found {major}.{minor}.");
}

abort = true;
}
}
}

if (abort)
{
nativeOpenGlWrapper.Dispose();
nativeOpenGlWrapper = null;
}

_xamlRootToWrapper.Add(xamlRoot, nativeOpenGlWrapper);
}

return nativeOpenGlWrapper;
}
catch (Exception e)
Expand All @@ -151,8 +206,8 @@ private void OnClosed(object _, object __)
}
if (_xamlRootToWrapper.Remove(XamlRoot!, out var wrapper))
{
using var _ = wrapper.MakeCurrent();
wrapper.Dispose();
using var makeCurrentDisposable = wrapper?.MakeCurrent();
wrapper?.Dispose();
}
});
}
Expand All @@ -169,18 +224,54 @@ private void OnClosed(object _, object __)
public void Invalidate() => NativeDispatcher.Main.Enqueue(Render, NativeDispatcherPriority.Idle);
#endif

public static DependencyProperty IsGLInitializedProperty { get; } =
DependencyProperty.Register(
nameof(IsGLInitialized),
typeof(bool?),
typeof(GLCanvasElement),
new PropertyMetadata(null, (PropertyChangedCallback)((dO, _) =>
{
var @this = (GLCanvasElement)dO;
if (!@this._changingGlInitialized)
{
throw new InvalidOperationException($"{nameof(GLCanvasElement)}.{nameof(IsGLInitializedProperty)} is read-only.");
}

// We should have arrived here from set_IsGLInitialized, so we could put this line at the end of the
// setter. Instead, we set it to false here to prevent users from calling SetValue.IsGLInitializedProperty
// _inside_ a call to GLCanvasElement.set_IsGLInitialized. This way, if a user intercepts this
// change (e.g. with SubscribeToPropertyChanged) and attempts to make a nested SetValue call, we still
// explode in their face.
@this._changingGlInitialized = false;
})));

/// <summary>
/// Indicates whether this element was loaded successfully or not, including the OpenGL context creation and setup.
/// This property is only valid when the element is loaded. When the element is not loaded in the visual tree, the value will be null.
/// </summary>
public bool? IsGLInitialized
{
get => (bool?)GetValue(IsGLInitializedProperty);
private set
{
_changingGlInitialized = true;
SetValue(IsGLInitializedProperty, value);
}
}

private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
_nativeOpenGlWrapper = GetOrCreateNativeOpenGlWrapper(XamlRoot!, _getWindowFunc);

if (_nativeOpenGlWrapper is null)
{
IsGLInitialized = false;
return;
}

_gl = GL.GetApi(this);

using (_nativeOpenGlWrapper!.MakeCurrent())
using (_nativeOpenGlWrapper.MakeCurrent())
{
UpdateFramebuffer();
Init(_gl);
Expand All @@ -200,10 +291,13 @@ private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
fe.Unloaded += OnClosed;
}

IsGLInitialized = true;
}

private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
IsGLInitialized = null;
if (_nativeOpenGlWrapper is null)
{
return;
Expand Down Expand Up @@ -256,7 +350,7 @@ private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs)

private void UpdateFramebuffer()
{
if (!IsLoaded)
if (!IsLoaded || _nativeOpenGlWrapper is null)
{
return;
}
Expand Down Expand Up @@ -290,7 +384,7 @@ private void UpdateFramebuffer()

private unsafe void Render()
{
if (!IsLoaded)
if (!IsLoaded || _nativeOpenGlWrapper is null)
{
return;
}
Expand Down

0 comments on commit 7288875

Please sign in to comment.