Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add GLCanvasElement checks for minimum available OpenGL version #18850

Merged
6 changes: 5 additions & 1 deletion doc/articles/controls/GLCanvasElement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 a `LoadedSuccessfully` 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 loading, i.e. its `IsLoaded` property is true. When the element is not loaded, the value of `LoadedSuccessfully` will be null.

## 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.
92 changes: 77 additions & 15 deletions src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Silk.NET.OpenGL;
using Microsoft.UI.Xaml;
Expand All @@ -19,7 +20,6 @@
#endif

#if WINAPPSDK
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
#else
using Uno.Foundation.Extensibility;
Expand All @@ -40,14 +40,15 @@ 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
// 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 +89,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 +108,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 +118,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 +202,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,8 +220,16 @@ private void OnClosed(object _, object __)
public void Invalidate() => NativeDispatcher.Main.Enqueue(Render, NativeDispatcherPriority.Idle);
#endif

/// <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, the value will be null.
ramezgerges marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public bool? LoadedSuccessfully { get; private set; }
ramezgerges marked this conversation as resolved.
Show resolved Hide resolved

private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
LoadedSuccessfully = false;

_nativeOpenGlWrapper = GetOrCreateNativeOpenGlWrapper(XamlRoot!, _getWindowFunc);

if (_nativeOpenGlWrapper is null)
Expand All @@ -180,7 +239,7 @@ private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)

_gl = GL.GetApi(this);

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

LoadedSuccessfully = true;
}

private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs)
{
LoadedSuccessfully = null;
if (_nativeOpenGlWrapper is null)
{
return;
Expand Down Expand Up @@ -256,7 +318,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 +352,7 @@ private void UpdateFramebuffer()

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