Skip to content

Commit

Permalink
Merge pull request #18385 from ramezgerges/glcanvaselement_skia_macos
Browse files Browse the repository at this point in the history
Glcanvaselement skia macos
  • Loading branch information
ramezgerges authored Oct 8, 2024
2 parents 3312489 + 1dfa824 commit ed92c1e
Show file tree
Hide file tree
Showing 25 changed files with 575 additions and 207 deletions.
34 changes: 19 additions & 15 deletions doc/articles/controls/GLCanvasElement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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<Window>` 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<Window>` 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`.

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/AddIns/Uno.WinUI.Graphics3DGL/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
angle/
angle_binaries/
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
182 changes: 131 additions & 51 deletions src/AddIns/Uno.WinUI.Graphics3DGL/GLCanvasElement.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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).
/// </remarks>
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<Window>? _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;
Expand Down Expand Up @@ -93,15 +104,7 @@ protected GLCanvasElement(uint width, uint height, Func<Window>? getWindowFunc)
{
_width = width;
_height = height;

#if WINAPPSDK
_nativeOpenGlWrapper = new WinUINativeOpenGLWrapper(getWindowFunc);
#else
if (!ApiExtensibility.CreateInstance<INativeOpenGLWrapper>(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);

Expand All @@ -115,6 +118,44 @@ protected GLCanvasElement(uint width, uint height, Func<Window>? getWindowFunc)
Unloaded += OnUnloaded;
}

private static INativeOpenGLWrapper? CreateNativeOpenGlWrapper(XamlRoot xamlRoot, Func<Window>? getWindowFunc)
{
try
{
#if WINAPPSDK
var nativeOpenGlWrapper = new WinUINativeOpenGLWrapper(xamlRoot, getWindowFunc!);
#else
if (!ApiExtensibility.CreateInstance<INativeOpenGLWrapper>(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();
});
}
}

/// <summary>
/// Invalidates the rendering, and queues a call to <see cref="RenderOverride"/>.
/// <see cref="RenderOverride"/> will only be called once after <see cref="Invalidate"/> and the output will
Expand All @@ -127,35 +168,72 @@ protected GLCanvasElement(uint width, uint height, Func<Window>? getWindowFunc)
public void Invalidate() => NativeDispatcher.Main.Enqueue(Render, NativeDispatcherPriority.Idle);
#endif

/// <remarks>not null after the first <see cref="GLCanvasElement"/> instance is loaded.</remarks>
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);
{
_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);

_renderBuffer = _gl.GenRenderbuffer();
_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);

Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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);
{
Expand All @@ -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. */ }
}
Loading

0 comments on commit ed92c1e

Please sign in to comment.