diff --git a/doc/articles/controls/GLCanvasElement.md b/doc/articles/controls/GLCanvasElement.md index 668504fe6059..403107cfea2a 100644 --- a/doc/articles/controls/GLCanvasElement.md +++ b/doc/articles/controls/GLCanvasElement.md @@ -5,7 +5,7 @@ uid: Uno.Controls.GLCanvasElement ## Uno.WinUI.Graphics3D.GLCanvasElement > [!IMPORTANT] -> This functionality is only available on WinUI and Skia Desktop (`netX.0-desktop`) targets that are running with Desktop OpenGL (not GLES) hardware acceleration. This is also not available on MacOS. +> This functionality is only available on WinUI and Skia Desktop (`netX.0-desktop`) targets that are running on platforms with support for hardware acceleration. On Windows and Linux, this means having (desktop) OpenGL support. On Mac OS X, this means having Vulkan support (to be used by [ANGLE](https://en.wikipedia.org/wiki/ANGLE_(software))). `GLCanvasElement` is a `Grid` for drawing 3D graphics with OpenGL. This class comes as a part of the `Uno.WinUI.Graphics3D` package. @@ -21,17 +21,17 @@ protected abstract void OnDestroy(GL gl); The protected constructor has `width` and `height` parameters, which decide the resolution of the offscreen framebuffer that the `GLCanvasElement` will draw onto. Note that these parameters are unrelated to the final size of the drawing in the window. After drawing (using `RenderOverride`) is done, the output is resized to fit the arranged size of the `GLCanvasElement`. You can control the final size just like any other `Grid`, e.g. using `MeasureOverride`, `ArrangeOverride`, the `Width/Height` properties, etc. -On WinUI, the protected constructor additionally 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 provide a way to get the `Window` of a `FrameworkElement`. This paramater is ignored on Uno Platform and can be set to null. This function is only called while the `GLCanvasElement` is loaded. +The protected constructor additionally 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 provide a way to get the `Window` of a `FrameworkElement`. This paramater 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 3 abstract methods above all take a `Silk.NET.OpenGL.GL` parameter that can be used to make OpenGL calls. -The `Init` method is a regular OpenGL setup method that you can use to set up the needed OpenGL objects, like textures, Vertex Array Buffers (VAOs), Element Array Buffers (EBOs), etc. - -The `OnDestroy` method is the complement of `Init` and is used to clean up any allocated resources. +The `Init` method is a regular OpenGL setup method that you can use to set up the needed OpenGL objects, like textures, Vertex Array Buffers (VAOs), Element Array Buffers (EBOs), etc. The `OnDestroy` method is the complement of `Init` and is used to clean up any allocated resources. `Init` and `OnDestroy` might be called multiple times alternatingly. In other words, 2 `OnDestroy` calls are guaranteed to have an `Init` call in between and vice versa. The `RenderOverride` is the main render-loop function. When adding your drawing logic in `RenderOverride`, you can assume that the OpenGL viewport rectangle is already set and its dimensions are equal to the `resolution` parameter provided to the `GLCanvasElement` constructor. -To learn more about using Silk.NET 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. +On MacOS, since OpenGL support is not natively present, we use [ANGLE](https://en.wikipedia.org/wiki/ANGLE_(software)) to provide OpenGL ES support. This means that we're actually using OpenGL ES 3.00, not OpenGL. Due to the similarity between desktop OpenGL and OpenGL ES, (almost) all the OpenGL ES functions are present in the `Silk.NET.OpenGL.GL` API surface and therefore we can use the same class to represent both the OpenGL and OpenGL ES APIs. To run the same `GLCanvasElement` subclasses on all supported platforms, make sure to use a subset of functions that are present in both APIs (which is almost all of OpenGL ES). + +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. 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`. @@ -82,12 +82,12 @@ public partial class GLCanvasElementExample : UserControl ```csharp // GLTriangleElement.cs # __SKIA__ || WINAPPSDK +// https://learnopengl.com/Getting-started/Hello-Triangle public class SimpleTriangleGlCanvasElement() -#if __SKIA__ - : GLCanvasElement(1200, 800, null) -#elif WINAPPSDK - // getWindowFunc is usually implemented by having a static property that stores the Window object when creating it (usually in App.cs) and then fetching it in getWindowFunc - : GLCanvasElement(1200, 800, /* your getWindowFunc */) +#if WINAPPSDK + : GLCanvasElement(1200, 800, () => SamplesApp.App.MainWindow) +#else + : GLCanvasElement(1200, 800, null) #endif { private uint _vao; @@ -113,10 +113,14 @@ public class SimpleTriangleGlCanvasElement() gl.VertexAttribPointer(0, 3, GLEnum.Float, false, 3 * sizeof(float), (void*)0); gl.EnableVertexAttribArray(0); - // string.Empty is added so that the version line is not interpreted as a preprocessor command + var slVersion = gl.GetStringS(StringName.ShadingLanguageVersion); + var versionDef = slVersion.Contains("OpenGL ES", StringComparison.InvariantCultureIgnoreCase) + ? "#version 300 es" + : "#version 330"; var vertexCode = $$""" - {{string.Empty}}#version 330 + {{versionDef}} + precision highp float; # for OpenGL ES compatibility layout (location = 0) in vec3 aPosition; out vec4 vertexColor; @@ -128,10 +132,10 @@ public class SimpleTriangleGlCanvasElement() } """; - // string.Empty is added so that the version line is not interpreted as a preprocessor command var fragmentCode = $$""" - {{string.Empty}}#version 330 + {{versionDef}} + precision highp float; # for OpenGL ES compatibility out vec4 out_color; in vec4 vertexColor; diff --git a/src/AddIns/Uno.WinUI.Graphics3DGL/.gitignore b/src/AddIns/Uno.WinUI.Graphics3DGL/.gitignore new file mode 100644 index 000000000000..8d1f1ac147eb --- /dev/null +++ b/src/AddIns/Uno.WinUI.Graphics3DGL/.gitignore @@ -0,0 +1,2 @@ +angle/ +angle_binaries/ \ No newline at end of file diff --git a/src/AddIns/Uno.WinUI.Graphics3DGL/Assets/error.png b/src/AddIns/Uno.WinUI.Graphics3DGL/Assets/error.png new file mode 100644 index 000000000000..cceacbc6ee7b Binary files /dev/null and b/src/AddIns/Uno.WinUI.Graphics3DGL/Assets/error.png differ diff --git a/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs b/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs index bdcf860cab63..ef4bb7be5826 100644 --- a/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs +++ b/src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs @@ -1,18 +1,25 @@ using System; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Logging; +using Silk.NET.OpenGL; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; -using Silk.NET.OpenGL; +using Silk.NET.Core.Contexts; +using Uno.Extensions; +using Uno.Logging; +using Window = Microsoft.UI.Xaml.Window; + +#if !UNO_UWP_BUILD +using Microsoft.UI.Dispatching; +#else +using Windows.System; +#endif #if WINAPPSDK using System.Runtime.InteropServices; using System.Runtime.InteropServices.WindowsRuntime; -using Microsoft.Extensions.Logging; -using Microsoft.UI.Dispatching; -using Uno.Extensions; -using Uno.Logging; #else using Uno.Foundation.Extensibility; using Uno.Graphics; @@ -29,18 +36,22 @@ namespace Uno.WinUI.Graphics3DGL; /// This is only available on WinUI and on skia-based targets running with hardware acceleration. /// This is currently only available on the WPF and X11 targets (and WinUI). /// -public abstract partial class GLCanvasElement : Grid +public abstract partial class GLCanvasElement : Grid, INativeContext { private const int BytesPerPixel = 4; - - private readonly INativeOpenGLWrapper _nativeOpenGlWrapper; + private static readonly BitmapImage _fallbackImage = new BitmapImage(new Uri("ms-appx:///Assets/error.png")); + private static bool? _glAvailable; private readonly uint _width; private readonly uint _height; + private readonly Func? _getWindowFunc; private readonly WriteableBitmap _backBuffer; - // These are valid if and only if IsLoaded + private bool _loadedAtleastOnce; + // valid if and only if _loadedAtleastOnce and _glAvailable + private INativeOpenGLWrapper? _nativeOpenGlWrapper; + // These are valid if and only if IsLoaded and _glAvailable private GL? _gl; private uint _framebuffer; private uint _textureColorBuffer; @@ -93,15 +104,7 @@ protected GLCanvasElement(uint width, uint height, Func? getWindowFunc) { _width = width; _height = height; - -#if WINAPPSDK - _nativeOpenGlWrapper = new WinUINativeOpenGLWrapper(getWindowFunc); -#else - if (!ApiExtensibility.CreateInstance(this, out _nativeOpenGlWrapper!)) - { - throw new InvalidOperationException($"Couldn't create a {nameof(INativeOpenGLWrapper)} object for {nameof(GLCanvasElement)}. Make sure you are running on a platform with {nameof(GLCanvasElement)} support."); - } -#endif + _getWindowFunc = getWindowFunc; _backBuffer = new WriteableBitmap((int)width, (int)height); @@ -115,6 +118,44 @@ protected GLCanvasElement(uint width, uint height, Func? getWindowFunc) Unloaded += OnUnloaded; } + private static INativeOpenGLWrapper? CreateNativeOpenGlWrapper(XamlRoot xamlRoot, Func? getWindowFunc) + { + try + { +#if WINAPPSDK + var nativeOpenGlWrapper = new WinUINativeOpenGLWrapper(xamlRoot, getWindowFunc!); +#else + if (!ApiExtensibility.CreateInstance(xamlRoot, out var nativeOpenGlWrapper)) + { + throw new InvalidOperationException($"Couldn't create a {nameof(INativeOpenGLWrapper)} object. Make sure you are running on a platform with OpenGL support."); + } +#endif + return nativeOpenGlWrapper; + } + catch (Exception e) + { + if (typeof(INativeOpenGLWrapper).Log().IsEnabled(LogLevel.Error)) + { + typeof(INativeOpenGLWrapper).Log().Error($"{nameof(INativeOpenGLWrapper)} creation failed.", e); + } + return null; + } + } + + private void OnClosed(object _, object __) + { + if (GlAvailable!.Value) + { + // OnUnloaded is called after OnClosed, which leads to disposing the context first and then trying to + // delete the framebuffer, etc. and this causes exceptions. + DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => + { + using var _ = _nativeOpenGlWrapper!.MakeCurrent(); + _nativeOpenGlWrapper.Dispose(); + }); + } + } + /// /// Invalidates the rendering, and queues a call to . /// will only be called once after and the output will @@ -127,16 +168,50 @@ protected GLCanvasElement(uint width, uint height, Func? getWindowFunc) public void Invalidate() => NativeDispatcher.Main.Enqueue(Render, NativeDispatcherPriority.Idle); #endif + /// not null after the first instance is loaded. + private bool? GlAvailable + { + get => _glAvailable; + set + { + if (!value!.Value) + { + ((ImageBrush)Background).ImageSource = _fallbackImage; + } + _glAvailable = value; + } + } + private unsafe void OnLoaded(object sender, RoutedEventArgs routedEventArgs) { - _nativeOpenGlWrapper.CreateContext(this); - _gl = (GL)_nativeOpenGlWrapper.CreateGLSilkNETHandle(); + if (!_loadedAtleastOnce) + { + _loadedAtleastOnce = true; + + if ((!GlAvailable ?? false) || CreateNativeOpenGlWrapper(XamlRoot!, _getWindowFunc) is not { } glWrapper) + { + GlAvailable = false; + } + else + { + GlAvailable = true; + _nativeOpenGlWrapper = glWrapper; + } + } + + if (!GlAvailable!.Value) + { + return; + } + + _gl = GL.GetApi(this); + GlAvailable = true; #if WINAPPSDK _pixels = Marshal.AllocHGlobal((int)(_width * _height * BytesPerPixel)); #endif - using (new GLStateDisposable(this)) + using (_nativeOpenGlWrapper!.MakeCurrent()) { _framebuffer = _gl.GenBuffer(); _gl.BindFramebuffer(GLEnum.Framebuffer, _framebuffer); @@ -144,10 +219,12 @@ private unsafe void OnLoaded(object sender, RoutedEventArgs routedEventArgs) _textureColorBuffer = _gl.GenTexture(); _gl.BindTexture(GLEnum.Texture2D, _textureColorBuffer); { - _gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.Rgb, _width, _height, 0, GLEnum.Rgb, GLEnum.UnsignedByte, (void*)0); + _gl.TexImage2D(GLEnum.Texture2D, 0, InternalFormat.Rgb, _width, _height, 0, GLEnum.Rgb, + GLEnum.UnsignedByte, (void*)0); _gl.TexParameterI(GLEnum.Texture2D, GLEnum.TextureMinFilter, (uint)GLEnum.Linear); _gl.TexParameterI(GLEnum.Texture2D, GLEnum.TextureMagFilter, (uint)GLEnum.Linear); - _gl.FramebufferTexture2D(GLEnum.Framebuffer, FramebufferAttachment.ColorAttachment0, GLEnum.Texture2D, _textureColorBuffer, 0); + _gl.FramebufferTexture2D(GLEnum.Framebuffer, FramebufferAttachment.ColorAttachment0, + GLEnum.Texture2D, _textureColorBuffer, 0); } _gl.BindTexture(GLEnum.Texture2D, 0); @@ -155,7 +232,8 @@ private unsafe void OnLoaded(object sender, RoutedEventArgs routedEventArgs) _gl.BindRenderbuffer(GLEnum.Renderbuffer, _renderBuffer); { _gl.RenderbufferStorage(GLEnum.Renderbuffer, InternalFormat.Depth24Stencil8, _width, _height); - _gl.FramebufferRenderbuffer(GLEnum.Framebuffer, GLEnum.DepthStencilAttachment, GLEnum.Renderbuffer, _renderBuffer); + _gl.FramebufferRenderbuffer(GLEnum.Framebuffer, GLEnum.DepthStencilAttachment, + GLEnum.Renderbuffer, _renderBuffer); } _gl.BindRenderbuffer(GLEnum.Renderbuffer, 0); @@ -169,18 +247,31 @@ private unsafe void OnLoaded(object sender, RoutedEventArgs routedEventArgs) _gl.BindFramebuffer(GLEnum.Framebuffer, 0); } + var window = +#if WINAPPSDK + _getWindowFunc!(); +#else + XamlRoot?.HostWindow; +#endif + window!.Closed += OnClosed; + Invalidate(); } private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs) { - Debug.Assert(_gl is not null); // because OnLoaded creates _gl + if (!GlAvailable!.Value) + { + return; + } + + global::System.Diagnostics.Debug.Assert(_gl is not null); // because OnLoaded creates _gl #if WINAPPSDK Marshal.FreeHGlobal(_pixels); #endif - using (new GLStateDisposable(this)) + using (_nativeOpenGlWrapper!.MakeCurrent()) { #if WINAPPSDK if (WindowsRenderingNativeMethods.wglGetCurrentContext() == 0) @@ -206,6 +297,14 @@ private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs) #if WINAPPSDK _pixels = default; #endif + + var window = +#if WINAPPSDK + _getWindowFunc!(); +#else + XamlRoot?.HostWindow; +#endif + window!.Closed -= OnClosed; } private unsafe void Render() @@ -215,9 +314,9 @@ private unsafe void Render() return; } - Debug.Assert(_gl is not null); // because _gl exists if loaded + global::System.Diagnostics.Debug.Assert(_gl is not null); // because _gl exists if loaded - using var _ = new GLStateDisposable(this); + using var _ = _nativeOpenGlWrapper!.MakeCurrent(); _gl!.BindFramebuffer(GLEnum.Framebuffer, _framebuffer); { @@ -244,26 +343,7 @@ private unsafe void Render() } } - private readonly struct GLStateDisposable : IDisposable - { - private readonly GLCanvasElement _glCanvasElement; - private readonly IDisposable _contextDisposable; - - public GLStateDisposable(GLCanvasElement glCanvasElement) - { - _glCanvasElement = glCanvasElement; - var gl = _glCanvasElement._gl; - Debug.Assert(gl is not null); - - _contextDisposable = _glCanvasElement._nativeOpenGlWrapper.MakeCurrent(); - } - - public void Dispose() - { - var gl = _glCanvasElement._gl; - Debug.Assert(gl is not null); - - _contextDisposable.Dispose(); - } - } + IntPtr INativeContext.GetProcAddress(string proc, int? slot) => _nativeOpenGlWrapper!.GetProcAddress(proc); + bool INativeContext.TryGetProcAddress(string proc, [UnscopedRef] out IntPtr addr, int? slot) => _nativeOpenGlWrapper!.TryGetProcAddress(proc, out addr); + void IDisposable.Dispose() { /* Keep this empty. This is only for INativeContext and will be called by Silk.NET, not us. */ } } diff --git a/src/AddIns/Uno.WinUI.Graphics3DGL/Uno.WinUI.Graphics3DGL.csproj b/src/AddIns/Uno.WinUI.Graphics3DGL/Uno.WinUI.Graphics3DGL.csproj index a7eeb6a451e1..43c1da7ef35f 100644 --- a/src/AddIns/Uno.WinUI.Graphics3DGL/Uno.WinUI.Graphics3DGL.csproj +++ b/src/AddIns/Uno.WinUI.Graphics3DGL/Uno.WinUI.Graphics3DGL.csproj @@ -1,7 +1,7 @@  $(NetSkiaPreviousAndCurrent) - $(NetUWPOrWinUI) + $(NetUWPOrWinUI) Uno.WinUI.Graphics3DGL true @@ -18,8 +18,25 @@ + + + + - + + + + $(DefineConstants);ANDROID + + + + + + $(DefineConstants);CROSSRUNTIME + + + + win-x86;win-x64;win-arm64 @@ -39,16 +56,36 @@ - - - - - - - + + $(DefineConstants);UNO_UWP_BUILD + + + + $(DefaultItemExcludes);angle/** + + + + + + + + + + + + + + diff --git a/src/SamplesApp/SamplesApp.Skia.Generic/SamplesApp.Skia.Generic.csproj b/src/SamplesApp/SamplesApp.Skia.Generic/SamplesApp.Skia.Generic.csproj index b32eea49980a..da265941d944 100644 --- a/src/SamplesApp/SamplesApp.Skia.Generic/SamplesApp.Skia.Generic.csproj +++ b/src/SamplesApp/SamplesApp.Skia.Generic/SamplesApp.Skia.Generic.csproj @@ -97,6 +97,16 @@ DestinationFolder="$(MSBuildThisFileDirectory)bin/$(Configuration)/$(TargetFramework)/runtimes/osx/native/" /> + + + <_libGLESdylibPath Include="$(MSBuildThisFileDirectory)../../AddIns/Uno.WinUI.Graphics3DGL/angle_binaries/**/libEGL.*" /> + <_libGLESdylibPath Include="$(MSBuildThisFileDirectory)../../AddIns/Uno.WinUI.Graphics3DGL/angle_binaries/**/libGLESv2.*" /> + + + + <_validationPath Include="Uno.Fonts.Fluent/Fonts/uno-fluentui-assets.ttf" /> diff --git a/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj b/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj index f86014c02a32..24c11fada021 100644 --- a/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj +++ b/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj @@ -18,6 +18,8 @@ + + @@ -26,16 +28,13 @@ + + - - - - - diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Composition/RotatingCubeGlCanvasElement.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Composition/RotatingCubeGlCanvasElement.cs index 0ab7ad230cf1..1a1bea6feca5 100644 --- a/src/SamplesApp/UITests.Shared/Windows_UI_Composition/RotatingCubeGlCanvasElement.cs +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Composition/RotatingCubeGlCanvasElement.cs @@ -22,7 +22,7 @@ namespace UITests.Shared.Windows_UI_Composition { #if WINAPPSDK public class RotatingCubeGlCanvasElement() : GLCanvasElement(1200, 800, () => SamplesApp.App.MainWindow) -#elif __SKIA__ +#else public class RotatingCubeGlCanvasElement() : GLCanvasElement(1200, 800, null) #endif { @@ -67,8 +67,9 @@ public class RotatingCubeGlCanvasElement() : GLCanvasElement(1200, 800, null) 0, 4, 5, }; - private readonly string _vertexShaderSource = "#version 330" + Environment.NewLine + + private readonly string _vertexShaderSource = """ + precision highp float; # for OpenGL ES compatibility layout(location = 0) in vec3 pos; layout(location = 1) in vec3 vertex_color; @@ -83,8 +84,9 @@ void main() { } """; - private readonly string FragmentShaderSource = "#version 330" + Environment.NewLine + + private readonly string FragmentShaderSource = """ + precision highp float; # for OpenGL ES compatibility in vec3 color; @@ -104,7 +106,11 @@ protected override void Init(GL Gl) _vao.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, 6, 0); _vao.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, 6, 3); - _shader = new Shader(Gl, _vertexShaderSource, FragmentShaderSource); + var slVersion = Gl.GetStringS(StringName.ShadingLanguageVersion); + var versionDef = slVersion.Contains("OpenGL ES", StringComparison.InvariantCultureIgnoreCase) + ? "#version 300 es" + : "#version 330"; + _shader = new Shader(Gl, versionDef + Environment.NewLine + _vertexShaderSource, versionDef + Environment.NewLine + FragmentShaderSource); } protected override void OnDestroy(GL Gl) diff --git a/src/SamplesApp/UITests.Shared/Windows_UI_Composition/SimpleTriangleGlCanvasElement.cs b/src/SamplesApp/UITests.Shared/Windows_UI_Composition/SimpleTriangleGlCanvasElement.cs index 27261b1eba4b..50ef09912fcf 100644 --- a/src/SamplesApp/UITests.Shared/Windows_UI_Composition/SimpleTriangleGlCanvasElement.cs +++ b/src/SamplesApp/UITests.Shared/Windows_UI_Composition/SimpleTriangleGlCanvasElement.cs @@ -8,10 +8,10 @@ namespace UITests.Shared.Windows_UI_Composition { // https://learnopengl.com/Getting-started/Hello-Triangle public class SimpleTriangleGlCanvasElement() -#if __SKIA__ - : GLCanvasElement(1200, 800, null) -#elif WINAPPSDK +#if WINAPPSDK : GLCanvasElement(1200, 800, () => SamplesApp.App.MainWindow) +#else + : GLCanvasElement(1200, 800, null) #endif { private uint _vao; @@ -37,10 +37,14 @@ unsafe protected override void Init(GL gl) gl.VertexAttribPointer(0, 3, GLEnum.Float, false, 3 * sizeof(float), (void*)0); gl.EnableVertexAttribArray(0); - // string.Empty is added so that the version line is not interpreted as a preprocessor command + var slVersion = gl.GetStringS(StringName.ShadingLanguageVersion); + var versionDef = slVersion.Contains("OpenGL ES", StringComparison.InvariantCultureIgnoreCase) + ? "#version 300 es" + : "#version 330"; var vertexCode = $$""" - {{string.Empty}}#version 330 + {{versionDef}} + precision highp float; # for OpenGL ES compatibility layout (location = 0) in vec3 aPosition; out vec4 vertexColor; @@ -52,10 +56,10 @@ void main() } """; - // string.Empty is added so that the version line is not interpreted as a preprocessor command var fragmentCode = $$""" - {{string.Empty}}#version 330 + {{versionDef}} + precision highp float; # for OpenGL ES compatibility out vec4 out_color; in vec4 vertexColor; diff --git a/src/SamplesApp/UnoIslandsSamplesApp.Skia/UnoIslandsSamplesApp.Skia.csproj b/src/SamplesApp/UnoIslandsSamplesApp.Skia/UnoIslandsSamplesApp.Skia.csproj index 8ac3b125cf63..3314889496e9 100644 --- a/src/SamplesApp/UnoIslandsSamplesApp.Skia/UnoIslandsSamplesApp.Skia.csproj +++ b/src/SamplesApp/UnoIslandsSamplesApp.Skia/UnoIslandsSamplesApp.Skia.csproj @@ -55,8 +55,6 @@ - - diff --git a/src/Uno.Foundation/AssemblyInfo.cs b/src/Uno.Foundation/AssemblyInfo.cs index 78219411a5c2..27f3cf685913 100644 --- a/src/Uno.Foundation/AssemblyInfo.cs +++ b/src/Uno.Foundation/AssemblyInfo.cs @@ -43,4 +43,5 @@ [assembly: InternalsVisibleTo("Uno.UI.Runtime.WebAssembly")] [assembly: InternalsVisibleTo("Uno.UI.Composition")] [assembly: InternalsVisibleTo("Uno.UI.Dispatching")] +[assembly: InternalsVisibleTo("Uno.WinUI.Graphics3DGL")] [assembly: System.Reflection.AssemblyMetadata("IsTrimmable", "True")] diff --git a/src/Uno.UI-Skia-only.slnf b/src/Uno.UI-Skia-only.slnf index 9f8b8e555e01..9372e80dfb51 100644 --- a/src/Uno.UI-Skia-only.slnf +++ b/src/Uno.UI-Skia-only.slnf @@ -54,6 +54,7 @@ "Uno.UI.Toolkit\\Uno.UI.Toolkit.Skia.csproj", "Uno.UI.XamlHost.Skia.Wpf\\Uno.UI.XamlHost.Skia.Wpf.csproj", "Uno.UI.XamlHost\\Uno.UI.XamlHost.Skia.csproj", + "Uno.UI\\Uno.UI.Reference.csproj", "Uno.UI\\Uno.UI.Skia.csproj", "Uno.UWPSyncGenerator.Reference\\Uno.UWPSyncGenerator.Reference.csproj", "Uno.UWPSyncGenerator\\Uno.UWPSyncGenerator.csproj", diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOpenGLWrapper.Egl.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOpenGLWrapper.Egl.cs new file mode 100644 index 000000000000..b5bb6593db8e --- /dev/null +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOpenGLWrapper.Egl.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2006-2019 Stefanos Apostolopoulos for the Open Toolkit project. +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System.Runtime.InteropServices; +using Microsoft.UI.Xaml; +using Uno.Disposables; +using Uno.Foundation.Extensibility; +using Uno.Foundation.Logging; +using Uno.Graphics; + +namespace Uno.UI.Runtime.Skia.MacOS; + +internal partial class MacOSNativeOpenGLWrapper : INativeOpenGLWrapper +{ + private const string libEGL = "libEGL.dylib"; + + [DllImport(libEGL, EntryPoint = "eglGetDisplay")] + public static extern IntPtr EglGetDisplay(IntPtr display_id); + + [DllImport(libEGL, EntryPoint = "eglInitialize")] + private static extern bool EglInitialize(IntPtr dpy, out int major, out int minor); + + [DllImport(libEGL, EntryPoint = "eglChooseConfig")] + [return: MarshalAs(UnmanagedType.I1)] + private static extern bool EglChooseConfig(IntPtr dpy, int[] attrib_list, [In][Out] IntPtr[] configs, int config_size, out int num_config); + + [DllImport(libEGL, EntryPoint = "eglGetError")] + private static extern ErrorCode EglGetError(); + + [DllImport(libEGL, EntryPoint = "eglCreateContext")] + private static extern IntPtr EglCreateContext(IntPtr dpy, IntPtr config, IntPtr share_context, int[] attrib_list); + + [DllImport(libEGL, EntryPoint = "eglCreatePbufferSurface")] + private static extern IntPtr EglCreatePbufferSurface(IntPtr dpy, IntPtr config, int[] attrib_list); + + [DllImport(libEGL, EntryPoint = "eglDestroySurface")] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool EglDestroySurface(IntPtr dpy, IntPtr surface); + + [DllImport(libEGL, EntryPoint = "eglDestroyContext")] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool EglDestroyContext(IntPtr dpy, IntPtr ctx); + + [DllImport(libEGL, EntryPoint = "eglGetCurrentContext")] + public static extern IntPtr EglGetCurrentContext(); + + [DllImport(libEGL, EntryPoint = "eglGetCurrentDisplay")] + public static extern IntPtr EglGetCurrentDisplay(); + + [DllImport(libEGL, EntryPoint = "eglGetCurrentSurface")] + public static extern IntPtr EglGetCurrentSurface(int readdraw); + + [DllImport(libEGL, EntryPoint = "eglMakeCurrent")] + [return: MarshalAs(UnmanagedType.I1)] + public static extern bool EglMakeCurrent(IntPtr dpy, IntPtr draw, IntPtr read, IntPtr ctx); + + private enum ErrorCode + { + SUCCESS = 0x3000, + NOT_INITIALIZED, + BAD_ACCESS, + BAD_ALLOC, + BAD_ATTRIBUTE, + BAD_CONFIG, + BAD_CONTEXT, + BAD_CURRENT_SURFACE, + BAD_DISPLAY, + BAD_MATCH, + BAD_NATIVE_PIXMAP, + BAD_NATIVE_WINDOW, + BAD_PARAMETER, + BAD_SURFACE, + CONTEXT_LOST + } +} diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOpenGLWrapper.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOpenGLWrapper.cs new file mode 100644 index 000000000000..d430600e0495 --- /dev/null +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacOSNativeOpenGLWrapper.cs @@ -0,0 +1,149 @@ +using System.Runtime.InteropServices; +using Microsoft.UI.Xaml; +using Uno.Disposables; +using Uno.Foundation.Extensibility; +using Uno.Foundation.Logging; +using Uno.Graphics; + +namespace Uno.UI.Runtime.Skia.MacOS; + +// ANGLE implements EGL 1.5 +// https://registry.khronos.org/EGL/api/EGL/egl.h +// Permalink: https://github.com/KhronosGroup/EGL-Registry/blob/29c4314e0ef04c730992d295f91b76635019fbba/api/EGL/egl.h + +internal partial class MacOSNativeOpenGLWrapper : INativeOpenGLWrapper +{ + private static readonly Lazy _libGLES = new Lazy(() => + { + if (!NativeLibrary.TryLoad("libGLESv2.dylib", typeof(MacOSNativeOpenGLWrapper).Assembly, DllImportSearchPath.UserDirectories, out var _handle)) + { + if (typeof(MacOSNativeOpenGLWrapper).Log().IsEnabled(LogLevel.Error)) + { + typeof(MacOSNativeOpenGLWrapper).Log().Error("libGLESv2.dylib was not loaded successfully."); + } + } + return _handle; + }); + + private const int EGL_DEFAULT_DISPLAY = 0; + private const int EGL_NO_CONTEXT = 0; + private const int EGL_ALPHA_SIZE = 0x3021; + private const int EGL_BLUE_SIZE = 0x3022; + private const int EGL_GREEN_SIZE = 0x3023; + private const int EGL_RED_SIZE = 0x3024; + private const int EGL_DEPTH_SIZE = 0x3025; + private const int EGL_STENCIL_SIZE = 0x3026; + private const int EGL_NONE = 0x3038; + private const int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private const int EGL_DRAW = 0x3059; + private const int EGL_READ = 0x305A; + + private IntPtr _eglDisplay; + private IntPtr _glContext; + private IntPtr _pBufferSurface; + + public MacOSNativeOpenGLWrapper(XamlRoot xamlRoot) + { + _eglDisplay = EglGetDisplay(EGL_DEFAULT_DISPLAY); + EglInitialize(_eglDisplay, out var major, out var minor); + if (this.Log().IsEnabled(LogLevel.Information)) + { + this.Log().Info($"Found EGL version {major}.{minor}."); + } + + int[] attribList = + { + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 8, + EGL_STENCIL_SIZE, 1, + EGL_NONE + }; + + var configs = new IntPtr[1]; + var success = EglChooseConfig(_eglDisplay, attribList, configs, configs.Length, out var numConfig); + + if (!success || numConfig < 1) + { + throw new InvalidOperationException($"{nameof(EglChooseConfig)} failed: {EglGetError()}"); + } + + // ANGLE implements GLES 3 + _glContext = EglCreateContext(_eglDisplay, configs[0], EGL_NO_CONTEXT, new[] { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }); + if (_glContext == IntPtr.Zero) + { + throw new InvalidOperationException($"EGL context creation failed: {EglGetError()}"); + } + + _pBufferSurface = EglCreatePbufferSurface(_eglDisplay, configs[0], new[] { EGL_NONE }); + if (_pBufferSurface == IntPtr.Zero) + { + throw new InvalidOperationException($"EGL pbuffer surface creation failed: {EglGetError()}"); + } + } + + public IntPtr GetProcAddress(string proc) + { + if (TryGetProcAddress(proc, out var addr)) + { + return addr; + } + + throw new InvalidOperationException($"A procedure named {proc} was not found in libGLES"); + } + + public bool TryGetProcAddress(string proc, out IntPtr addr) + { + if (_libGLES.Value == IntPtr.Zero) + { + addr = IntPtr.Zero; + return false; + } + return NativeLibrary.TryGetExport(_libGLES.Value, proc, out addr); + } + + public void Dispose() + { + if (_eglDisplay != IntPtr.Zero && _pBufferSurface != IntPtr.Zero) + { + EglDestroySurface(_eglDisplay, _pBufferSurface); + } + if (_eglDisplay != IntPtr.Zero && _glContext != IntPtr.Zero) + { + EglDestroyContext(_eglDisplay, _glContext); + } + + _pBufferSurface = IntPtr.Zero; + _glContext = IntPtr.Zero; + _eglDisplay = IntPtr.Zero; + } + + public IDisposable MakeCurrent() + { + var glContext = EglGetCurrentContext(); + var display = EglGetCurrentDisplay(); + var readSurface = EglGetCurrentSurface(EGL_READ); + var drawSurface = EglGetCurrentSurface(EGL_DRAW); + if (!EglMakeCurrent(_eglDisplay, _pBufferSurface, _pBufferSurface, _glContext)) + { + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().Error($"{nameof(EglMakeCurrent)} failed."); + } + } + return Disposable.Create(() => + { + if (!EglMakeCurrent(display, drawSurface, readSurface, glContext)) + { + if (this.Log().IsEnabled(LogLevel.Error)) + { + this.Log().Error($"{nameof(EglMakeCurrent)} failed."); + } + } + }); + } + + public static void Register() => ApiExtensibility.Register(typeof(INativeOpenGLWrapper), xamlRoot => new MacOSNativeOpenGLWrapper(xamlRoot)); +} diff --git a/src/Uno.UI.Runtime.Skia.MacOS/MacSkiaHost.cs b/src/Uno.UI.Runtime.Skia.MacOS/MacSkiaHost.cs index 94ae216c84e6..c1a90852a4c4 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/MacSkiaHost.cs +++ b/src/Uno.UI.Runtime.Skia.MacOS/MacSkiaHost.cs @@ -28,6 +28,7 @@ static MacSkiaHost() MacOSNativeWindowFactoryExtension.Register(); MacOSSystemNavigationManagerPreviewExtension.Register(); MacOSSystemThemeHelperExtension.Register(); + MacOSNativeOpenGLWrapper.Register(); } public MacSkiaHost(Func appBuilder) diff --git a/src/Uno.UI.Runtime.Skia.MacOS/Uno.UI.Runtime.Skia.MacOS.csproj b/src/Uno.UI.Runtime.Skia.MacOS/Uno.UI.Runtime.Skia.MacOS.csproj index 4a0bec49b565..57562c5c571a 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/Uno.UI.Runtime.Skia.MacOS.csproj +++ b/src/Uno.UI.Runtime.Skia.MacOS/Uno.UI.Runtime.Skia.MacOS.csproj @@ -42,10 +42,10 @@ - + Visible="False" /> diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/.gitignore b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/.gitignore index f1e5aa5f61a6..81e1dc3fd958 100644 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/.gitignore +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/.gitignore @@ -1,2 +1,3 @@ -SkiaSharp.NativeAssets.macOS.*/ -libSkiaSharp.dylib +skiasharp_*_nativeassets.zip +skiasharp_*_nativeassets/ +libSkiaSharp.dylib \ No newline at end of file diff --git a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/getSkiaSharpDylib.sh b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/getSkiaSharpDylib.sh index e8b4e60d5740..42f526967e6b 100755 --- a/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/getSkiaSharpDylib.sh +++ b/src/Uno.UI.Runtime.Skia.MacOS/UnoNativeMac/UnoNativeMac/getSkiaSharpDylib.sh @@ -1,7 +1,10 @@ -#!/bin/bash +#!/usr/bin/env bash VERSION=${VERSION:-2.88.7} -nuget install SkiaSharp.NativeAssets.macOS -Version ${VERSION} -cp SkiaSharp.NativeAssets.macOS.${VERSION}/runtimes/osx/native/libSkiaSharp.dylib . -rm -rf SkiaSharp.NativeAssets.macOS.${VERSION}/ +filename="skiasharp_"${VERSION}"_nativeassets" +if [ ! -f "${filename}"/runtimes/osx/native/libSkiaSharp.dylib ]; then + curl -o "${filename}.zip" -L "https://www.nuget.org/api/v2/package/SkiaSharp.NativeAssets.macOS/${VERSION}" + unzip -d "${filename}" "${filename}.zip" +fi +cp "${filename}/runtimes/osx/native/libSkiaSharp.dylib" . diff --git a/src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfExtensionsRegistrar.cs b/src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfExtensionsRegistrar.cs index f79162569d63..88574a5f1c10 100644 --- a/src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfExtensionsRegistrar.cs +++ b/src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfExtensionsRegistrar.cs @@ -58,7 +58,7 @@ internal static void Register() ApiExtensibility.Register(typeof(IAnalyticsInfoExtension), o => new AnalyticsInfoExtension()); ApiExtensibility.Register(typeof(ISystemNavigationManagerPreviewExtension), o => new SystemNavigationManagerPreviewExtension()); ApiExtensibility.Register(typeof(INativeWebViewProvider), o => new WpfNativeWebViewProvider(o)); - ApiExtensibility.Register(typeof(INativeOpenGLWrapper), _ => new WpfNativeOpenGLWrapper()); + ApiExtensibility.Register(typeof(INativeOpenGLWrapper), xamlRoot => new WpfNativeOpenGLWrapper(xamlRoot)); _registered = true; } diff --git a/src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfNativeOpenGLWrapper.cs b/src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfNativeOpenGLWrapper.cs index b0ebe1220714..513c15c561c2 100644 --- a/src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfNativeOpenGLWrapper.cs +++ b/src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfNativeOpenGLWrapper.cs @@ -3,9 +3,6 @@ using System; using System.Runtime.InteropServices; -using Silk.NET.Core.Contexts; -using Silk.NET.Core.Loader; -using Silk.NET.OpenGL; #if WINDOWS_UWP || WINAPPSDK using Microsoft.UI.Xaml; @@ -30,23 +27,50 @@ namespace Uno.UI.Runtime.Skia.Wpf.Extensions; #endif #if WINDOWS_UWP || WINAPPSDK -internal class WinUINativeOpenGLWrapper(Func getWindowFunc) +internal class WinUINativeOpenGLWrapper #else internal class WpfNativeOpenGLWrapper #endif : INativeOpenGLWrapper { +#if WINDOWS_UWP || WINAPPSDK + private static readonly Type _type = typeof(WinUINativeOpenGLWrapper); +#else + private static readonly Type _type = typeof(WpfNativeOpenGLWrapper); +#endif + private static readonly Lazy _opengl32 = new Lazy(() => + { + if (!NativeLibrary.TryLoad("opengl32.dll", _type.Assembly, DllImportSearchPath.UserDirectories, out var _handle)) + { + if (_type.Log().IsEnabled(LogLevel.Error)) + { + _type.Log().Error("opengl32.dll was not loaded successfully."); + } + } + return _handle; + }); + +#if WINDOWS_UWP || WINAPPSDK + private readonly Func _getWindowFunc; +#endif private nint _hdc; private nint _glContext; - public void CreateContext(UIElement element) +#if WINDOWS_UWP || WINAPPSDK + public WinUINativeOpenGLWrapper(XamlRoot xamlRoot, Func getWindowFunc) { + _getWindowFunc = getWindowFunc; +#else + public WpfNativeOpenGLWrapper(XamlRoot xamlRoot) + { +#endif + #if WINDOWS_UWP || WINAPPSDK - var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(getWindowFunc()); + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(_getWindowFunc()); #else - if (element.XamlRoot?.HostWindow?.NativeWindow is not WpfWindow wpfWindow) + if (xamlRoot.HostWindow?.NativeWindow is not WpfWindow wpfWindow) { - throw new InvalidOperationException($"The XamlRoot and its NativeWindow must be initialized on the element before calling {nameof(CreateContext)}."); + throw new InvalidOperationException($"The XamlRoot and its NativeWindow must be initialized on the element before constructing a {_type.Name}."); } var hwnd = new WindowInteropHelper(wpfWindow).Handle; #endif @@ -75,19 +99,11 @@ public void CreateContext(UIElement element) if (pixelFormat == 0) { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().Error($"ChoosePixelFormat failed"); - } throw new InvalidOperationException("ChoosePixelFormat failed"); } if (WindowsRenderingNativeMethods.SetPixelFormat(_hdc, pixelFormat, ref pfd) == 0) { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().Error($"SetPixelFormat failed"); - } throw new InvalidOperationException("ChoosePixelFormat failed"); } @@ -95,17 +111,33 @@ public void CreateContext(UIElement element) if (_glContext == IntPtr.Zero) { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().Error($"wglCreateContext failed"); - } throw new InvalidOperationException("ChoosePixelFormat failed"); } } - public object CreateGLSilkNETHandle() => GL.GetApi(new WindowsGlNativeContext()); + // https://sharovarskyi.com/blog/posts/csharp-win32-opengl-silknet/ + public bool TryGetProcAddress(string proc, out nint addr) + { + if (_opengl32.Value != IntPtr.Zero && NativeLibrary.TryGetExport(_opengl32.Value, proc, out addr)) + { + return true; + } - public void DestroyContext() + addr = WindowsRenderingNativeMethods.wglGetProcAddress(proc); + return addr != IntPtr.Zero; + } + + public nint GetProcAddress(string proc) + { + if (TryGetProcAddress(proc, out var address)) + { + return address; + } + + throw new InvalidOperationException("No function was found with the name " + proc + "."); + } + + public void Dispose() { if (WindowsRenderingNativeMethods.wglDeleteContext(_glContext) == 0) { @@ -140,42 +172,4 @@ public IDisposable MakeCurrent() } }); } - - // https://sharovarskyi.com/blog/posts/csharp-win32-opengl-silknet/ - private class WindowsGlNativeContext : INativeContext - { - private readonly UnmanagedLibrary _l; - - public WindowsGlNativeContext() - { - _l = new UnmanagedLibrary("opengl32.dll"); - if (_l.Handle == IntPtr.Zero) - { - throw new PlatformNotSupportedException("Unable to load opengl32.dll. Make sure you're running on a system with OpenGL support"); - } - } - - public bool TryGetProcAddress(string proc, out nint addr, int? slot = null) - { - if (_l.TryLoadFunction(proc, out addr)) - { - return true; - } - - addr = WindowsRenderingNativeMethods.wglGetProcAddress(proc); - return addr != IntPtr.Zero; - } - - public nint GetProcAddress(string proc, int? slot = null) - { - if (TryGetProcAddress(proc, out var address, slot)) - { - return address; - } - - throw new InvalidOperationException("No function was found with the name " + proc + "."); - } - - public void Dispose() => _l.Dispose(); - } } diff --git a/src/Uno.UI.Runtime.Skia.Wpf/Uno.UI.Runtime.Skia.Wpf.csproj b/src/Uno.UI.Runtime.Skia.Wpf/Uno.UI.Runtime.Skia.Wpf.csproj index 2ac97c1882cf..704fa9d750ee 100644 --- a/src/Uno.UI.Runtime.Skia.Wpf/Uno.UI.Runtime.Skia.Wpf.csproj +++ b/src/Uno.UI.Runtime.Skia.Wpf/Uno.UI.Runtime.Skia.Wpf.csproj @@ -33,7 +33,6 @@ - diff --git a/src/Uno.UI.Runtime.Skia.X11/Uno.UI.Runtime.Skia.X11.csproj b/src/Uno.UI.Runtime.Skia.X11/Uno.UI.Runtime.Skia.X11.csproj index a4c40812cb32..9fca805f2504 100644 --- a/src/Uno.UI.Runtime.Skia.X11/Uno.UI.Runtime.Skia.X11.csproj +++ b/src/Uno.UI.Runtime.Skia.X11/Uno.UI.Runtime.Skia.X11.csproj @@ -33,8 +33,6 @@ - - diff --git a/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs b/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs index a6d6393c3436..f40b91496cd6 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs @@ -67,7 +67,7 @@ static X11ApplicationHost() ApiExtensibility.Register(typeof(Windows.ApplicationModel.DataTransfer.DragDrop.Core.IDragDropExtension), o => new X11DragDropExtension(o)); - ApiExtensibility.Register(typeof(Uno.Graphics.INativeOpenGLWrapper), _ => new X11NativeOpenGLWrapper()); + ApiExtensibility.Register(typeof(Uno.Graphics.INativeOpenGLWrapper), xamlRoot => new X11NativeOpenGLWrapper(xamlRoot)); } public X11ApplicationHost(Func appBuilder) diff --git a/src/Uno.UI.Runtime.Skia.X11/X11NativeOpenGLWrapper.cs b/src/Uno.UI.Runtime.Skia.X11/X11NativeOpenGLWrapper.cs index 05b90001ad7f..f094f2540b17 100644 --- a/src/Uno.UI.Runtime.Skia.X11/X11NativeOpenGLWrapper.cs +++ b/src/Uno.UI.Runtime.Skia.X11/X11NativeOpenGLWrapper.cs @@ -1,7 +1,7 @@ using System; using Microsoft.UI.Xaml; -using Silk.NET.OpenGL; using Uno.Disposables; +using Uno.Foundation.Logging; using Uno.Graphics; namespace Uno.WinUI.Runtime.Skia.X11; @@ -12,11 +12,11 @@ internal class X11NativeOpenGLWrapper : INativeOpenGLWrapper private IntPtr _glContext; private IntPtr _pBuffer; - public unsafe void CreateContext(UIElement element) + public unsafe X11NativeOpenGLWrapper(XamlRoot xamlRoot) { - if (element.XamlRoot is null || X11Manager.XamlRootMap.GetHostForRoot(element.XamlRoot) is not X11XamlRootHost xamlRootHost) + if (X11Manager.XamlRootMap.GetHostForRoot(xamlRoot) is not X11XamlRootHost xamlRootHost) { - throw new InvalidOperationException($"The XamlRoot and its XamlRootHost must be initialized on the element before calling {nameof(CreateContext)}."); + throw new InvalidOperationException($"The XamlRoot and its XamlRootHost must be initialized on the element before constructing an {nameof(X11NativeOpenGLWrapper)}."); } _display = xamlRootHost.RootX11Window.Display; @@ -34,41 +34,54 @@ public unsafe void CreateContext(UIElement element) (int)X11Helper.None }; - IntPtr bestFbc = IntPtr.Zero; - XVisualInfo* visual = null; - var ptr = GlxInterface.glXChooseFBConfig(_display, XLib.XDefaultScreen(_display), glxAttribs, out var count); - if (ptr == null || *ptr == IntPtr.Zero) + var fbConfigs = GlxInterface.glXChooseFBConfig(_display, XLib.XDefaultScreen(_display), glxAttribs, out var count); + if (fbConfigs == null || *fbConfigs == IntPtr.Zero) { throw new InvalidOperationException($"{nameof(GlxInterface.glXChooseFBConfig)} failed to retrieve GLX framebuffer configurations."); } + using var fbConfigsDisposable = new DisposableStruct(static aa => { _ = XLib.XFree(aa); }, (IntPtr)fbConfigs); + + IntPtr bestFbc = IntPtr.Zero; for (var c = 0; c < count; c++) { - XVisualInfo* visual_ = GlxInterface.glXGetVisualFromFBConfig(_display, ptr[c]); - if (visual_->depth == 32) // 24bit color + 8bit stencil as requested above + XVisualInfo* visual = GlxInterface.glXGetVisualFromFBConfig(_display, fbConfigs[c]); + using var visualDisposable = new DisposableStruct(static aa => { _ = XLib.XFree(aa); }, (IntPtr)visual); + if (visual->depth == 32) // 24bit color + 8bit stencil as requested above { - bestFbc = ptr[c]; - visual = visual_; + bestFbc = fbConfigs[c]; break; } } - if (visual == null) + if (bestFbc == IntPtr.Zero) { - throw new InvalidOperationException("Could not create correct visual window.\n"); + throw new InvalidOperationException("Could not find a suitable framebuffer config.\n"); } _glContext = GlxInterface.glXCreateNewContext(_display, bestFbc, GlxConsts.GLX_RGBA_TYPE, IntPtr.Zero, /* True */ 1); + if (_glContext == IntPtr.Zero) + { + throw new InvalidOperationException($"{nameof(GlxInterface.glXCreateNewContext)} failed."); + } _pBuffer = GlxInterface.glXCreatePbuffer(_display, bestFbc, new[] { (int)X11Helper.None }); + if (_pBuffer == IntPtr.Zero) + { + throw new InvalidOperationException($"{nameof(GlxInterface.glXCreatePbuffer)} failed."); + } } - public object CreateGLSilkNETHandle() => GL.GetApi(GlxInterface.glXGetProcAddress); - - public void DestroyContext() + public void Dispose() { using var lockDisposable = X11Helper.XLock(_display); - GlxInterface.glXDestroyPbuffer(_display, _pBuffer); - GlxInterface.glXDestroyContext(_display, _glContext); + if (_display != IntPtr.Zero && _pBuffer != IntPtr.Zero) + { + GlxInterface.glXDestroyPbuffer(_display, _pBuffer); + } + if (_display != IntPtr.Zero && _glContext != IntPtr.Zero) + { + GlxInterface.glXDestroyContext(_display, _glContext); + } _display = default; _glContext = default; @@ -82,4 +95,11 @@ public IDisposable MakeCurrent() GlxInterface.glXMakeCurrent(_display, _pBuffer, _glContext); return Disposable.Create(() => GlxInterface.glXMakeCurrent(_display, drawable, glContext)); } + + public IntPtr GetProcAddress(string proc) => GlxInterface.glXGetProcAddress(proc); + public bool TryGetProcAddress(string proc, out IntPtr addr) + { + addr = GlxInterface.glXGetProcAddress(proc); + return addr != IntPtr.Zero; + } } diff --git a/src/Uno.UI/Graphics/INativeOpenGLWrapper.cs b/src/Uno.UI/Graphics/INativeOpenGLWrapper.cs index e9575a2b2ffb..015d953dcd5d 100644 --- a/src/Uno.UI/Graphics/INativeOpenGLWrapper.cs +++ b/src/Uno.UI/Graphics/INativeOpenGLWrapper.cs @@ -1,5 +1,6 @@ -using System; -using Microsoft.UI.Xaml; +#nullable enable + +using System; #if WINAPPSDK || WINDOWS_UWP namespace Uno.WinUI.Graphics3DGL; @@ -7,29 +8,12 @@ namespace Uno.WinUI.Graphics3DGL; namespace Uno.Graphics; #endif -internal interface INativeOpenGLWrapper +internal interface INativeOpenGLWrapper : IDisposable { - public delegate IntPtr GLGetProcAddress(string proc); - - /// - /// Creates an OpenGL context for a native window/surface that the - /// belongs to. The - /// will be associated with this element until a corresponding call to . - /// - public void CreateContext(UIElement element); - - /// This should be cast to a Silk.NET.GL - public object CreateGLSilkNETHandle(); + public IntPtr GetProcAddress(string proc); - /// - /// Destroys the context created in . This is only called if a preceding - /// call to is made (after the last call to ). - /// - public void DestroyContext(); + public bool TryGetProcAddress(string proc, out IntPtr addr); - /// - /// Makes the OpenGL context created in the current context for the thread. - /// /// A disposable that restores the OpenGL context to what it was at the time of this method call. public IDisposable MakeCurrent(); }