diff --git a/doc/articles/controls/GLCanvasElement.md b/doc/articles/controls/GLCanvasElement.md index ed1c4ec02c57..42c8d2f79b83 100644 --- a/doc/articles/controls/GLCanvasElement.md +++ b/doc/articles/controls/GLCanvasElement.md @@ -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/). @@ -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` 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` 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 @@ -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. diff --git a/src/AddIns/Uno.WinUI.Graphics3DGL/Assets/error.png b/src/AddIns/Uno.WinUI.Graphics3DGL/Assets/error.png deleted file mode 100644 index cceacbc6ee7b..000000000000 Binary files a/src/AddIns/Uno.WinUI.Graphics3DGL/Assets/error.png and /dev/null differ diff --git a/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs b/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs index fe6a994616b5..6cef9512f466 100644 --- a/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs +++ b/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs @@ -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; @@ -19,7 +22,6 @@ #endif #if WINAPPSDK -using System.Runtime.InteropServices; using System.Runtime.InteropServices.WindowsRuntime; #else using Uno.Foundation.Extensibility; @@ -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 _xamlRootToWrapper = new(); + private static readonly Dictionary _xamlRootToWrapper = new(); + + private static readonly (int major, int minor) _minVersion = (3, 0); private readonly Func? _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; @@ -88,8 +93,6 @@ public abstract partial class GLCanvasElement : Grid, INativeContext /// protected abstract void RenderOverride(GL gl); - /// The width of the backing framebuffer. - /// The height of the backing framebuffer. /// 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. #if WINAPPSDK protected GLCanvasElement(Func getWindowFunc) @@ -109,7 +112,7 @@ protected GLCanvasElement(Func? getWindowFunc) SizeChanged += (_, _) => UpdateFramebuffer(); } - private static INativeOpenGLWrapper? GetOrCreateNativeOpenGlWrapper(XamlRoot xamlRoot, Func? getWindowFunc) + private static unsafe INativeOpenGLWrapper? GetOrCreateNativeOpenGlWrapper(XamlRoot xamlRoot, Func? getWindowFunc) { try { @@ -119,14 +122,66 @@ protected GLCanvasElement(Func? getWindowFunc) #if WINAPPSDK nativeOpenGlWrapper = new WinUINativeOpenGLWrapper(xamlRoot, getWindowFunc!); #else - if (!ApiExtensibility.CreateInstance(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])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])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) @@ -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(); } }); } @@ -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; + }))); + + /// + /// 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. + /// + 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); @@ -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; @@ -256,7 +350,7 @@ private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs) private void UpdateFramebuffer() { - if (!IsLoaded) + if (!IsLoaded || _nativeOpenGlWrapper is null) { return; } @@ -290,7 +384,7 @@ private void UpdateFramebuffer() private unsafe void Render() { - if (!IsLoaded) + if (!IsLoaded || _nativeOpenGlWrapper is null) { return; }