Skip to content

Commit

Permalink
feat: introduce the ability to draw using raw OpenGL on skia
Browse files Browse the repository at this point in the history
  • Loading branch information
ramezgerges committed Aug 28, 2024
1 parent 1eca500 commit ff6e501
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetPrevious)</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<Import Project="../../targetframework-override.props" />
Expand Down Expand Up @@ -30,6 +31,11 @@
<ProjectReference Include="..\..\Uno.UI.Adapter.Microsoft.Extensions.Logging\Uno.UI.Adapter.Microsoft.Extensions.Logging.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Silk.NET.OpenGL" Version="2.16.0" />
<PackageReference Include="Silk.NET.OpenGLES" Version="2.16.0" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Uno.BenchmarkDotNet" Version="0.11.7-develop" />
<PackageReference Include="Uno.BenchmarkDotNet.Annotations" Version="0.11.7-develop" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetSkiaPreviousAndCurrent)</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<Import Project="../../targetframework-override.props" />
Expand Down Expand Up @@ -69,4 +70,13 @@
<Content Update="@(Content)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<Reference Include="Silk.NET.Core">
<HintPath>..\..\..\..\..\..\.nuget\packages\silk.net.core\2.16.0\lib\net6.0\Silk.NET.Core.dll</HintPath>
</Reference>
<Reference Include="Silk.NET.OpenGL">
<HintPath>..\..\..\..\..\..\.nuget\packages\silk.net.opengl\2.16.0\lib\net5.0\Silk.NET.OpenGL.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,13 @@ internal unsafe void CopyPixels(int pixelWidth, int pixelHeight, ReadOnlyMemory<
}
}

internal void CopyPixels(int pixelWidth, int pixelHeight, IntPtr data)
{
var info = new SKImageInfo(pixelWidth, pixelHeight, SKColorType.Bgra8888, SKAlphaType.Premul);

SetFrameProviderAndOnFrameChanged(FrameProviderFactory.Create(SKImage.FromPixelCopy(info, data, pixelWidth * 4)), null);
}

~SkiaCompositionSurface()
{
SetFrameProviderAndOnFrameChanged(null, null);
Expand Down
5 changes: 5 additions & 0 deletions src/Uno.UI.Runtime.Skia.X11/Uno.UI.Runtime.Skia.X11.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
<PackageReference Include="SkiaSharp.Harfbuzz" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Silk.NET.OpenGL" Version="2.16.0" />
<PackageReference Include="Silk.NET.OpenGLES" Version="2.16.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Uno.UI.Runtime.Skia\Uno.UI.Runtime.Skia.csproj" />
<ProjectReference Include="..\Uno.Foundation\Uno.Foundation.Skia.csproj" TreatAsPackageReference="false" PrivateAssets="all" />
Expand Down
1 change: 1 addition & 0 deletions src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ static X11ApplicationHost()

ApiExtensibility.Register<IXamlRootHost>(typeof(IUnoCorePointerInputSource), o => new X11PointerInputSource(o));
ApiExtensibility.Register<IXamlRootHost>(typeof(IUnoKeyboardInputSource), o => new X11KeyboardInputSource(o));
ApiExtensibility.Register(typeof(XamlRootMap<IXamlRootHost>), _ => X11Manager.XamlRootMap);

ApiExtensibility.Register(typeof(INativeWindowFactoryExtension), _ => new X11NativeWindowFactoryExtension());

Expand Down
1 change: 1 addition & 0 deletions src/Uno.UI.Runtime.Skia.X11/X11OpenGLRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public X11OpenGLRenderer(IXamlRootHost host, X11Window x11window)
void IX11Renderer.Render()
{
using var lockDiposable = X11Helper.XLock(_x11Window.Display);
using var _ = _host.LockGL();

if (_host is X11XamlRootHost { Closed.IsCompleted: true })
{
Expand Down
21 changes: 21 additions & 0 deletions src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Uno.Foundation.Logging;
using Uno.UI.Hosting;
using Microsoft.UI.Xaml;
using Silk.NET.OpenGL;
using SkiaSharp;
using Uno.Disposables;
using Uno.UI;
Expand Down Expand Up @@ -56,6 +57,8 @@ internal partial class X11XamlRootHost : IXamlRootHost

private int _synchronizedShutDownTopWindowIdleCounter;

private readonly object _glLock = new object();

private X11Window? _x11Window;
private X11Window? _x11TopWindow;
private IX11Renderer? _renderer;
Expand Down Expand Up @@ -607,4 +610,22 @@ private void UpdateRendererBackground()
}
}
}

object? IXamlRootHost.GetGL() => GL.GetApi(GlxInterface.glXGetProcAddress);

// To prevent concurrent GL operations breaking the state, you should obtain the lock while
// using GL commands. Make sure to restore all the state to default before unlocking (i.e. unbind
// all used buffers, textures, etc.)
IDisposable IXamlRootHost.LockGL()
{
// we don't use a SemaphoreSlim as it's not reentrant.
Monitor.Enter(_glLock);
return new GLLockDisposable(_glLock);
}

private readonly struct GLLockDisposable(object @lock) : IDisposable
{

public void Dispose() => Monitor.Exit(@lock);
}
}
10 changes: 10 additions & 0 deletions src/Uno.UI/Hosting/IXamlRootHost.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#nullable enable

using System;
using Microsoft.UI.Xaml;
using Uno.Disposables;

namespace Uno.UI.Hosting;

Expand All @@ -9,4 +11,12 @@ internal interface IXamlRootHost
UIElement? RootElement { get; }

void InvalidateRender();

// should be cast to a Silk.NET GL object.
object? GetGL() => null;

// To prevent concurrent GL operations breaking the state, you should obtain the lock while
// using GL commands. Make sure to restore all the state to default before unlocking (i.e. unbind
// all used buffers, textures, etc.)
IDisposable LockGL() => Disposable.Empty;
}
130 changes: 130 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/Image/OpenGLImage.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using System;
using System.Runtime.InteropServices;
using Windows.Foundation;
using Microsoft.UI.Xaml.Media.Imaging;
using Silk.NET.OpenGL;
using Uno.Disposables;
namespace Microsoft.UI.Xaml.Controls;

public abstract class OpenGLImage : Image
{
private const int BytesPerPixel = 4;

private readonly uint _width;
private readonly uint _height;
private bool _firstLoad = true;

private GL _gl;
private uint _framebuffer;
private uint _textureColorBuffer;
private GLImageSource _writableBitmap;
private unsafe readonly void* _pixels;

unsafe protected OpenGLImage(Size resolution)
{
_width = (uint)resolution.Width;
_height = (uint)resolution.Height;
_pixels = (void*)Marshal.AllocHGlobal((int)(_width * _height * BytesPerPixel));
}

unsafe ~OpenGLImage()
{
Marshal.FreeHGlobal((IntPtr)_pixels);
}

protected abstract void OnLoad(GL gl);
protected abstract void OnDestroy(GL gl);
protected abstract void RenderOverride(GL gl);

private unsafe protected override void OnLoaded()
{
base.OnLoaded();

_gl = XamlRoot!.GetGL() as GL ?? throw new InvalidOperationException("Couldn't get the Silk.NET GL handle.");

if (_firstLoad)
{
_firstLoad = false;

using var _1 = XamlRoot?.LockGL();
using var _2 = RestoreGLState();

_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.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.BindTexture(GLEnum.Texture2D, 0);

var rbo = _gl.GenRenderbuffer();
_gl.BindRenderbuffer(GLEnum.Renderbuffer, rbo);
{
_gl.RenderbufferStorage(GLEnum.Renderbuffer, InternalFormat.Depth24Stencil8, _width, _height);
_gl.FramebufferRenderbuffer(GLEnum.Framebuffer, GLEnum.DepthStencilAttachment, GLEnum.Renderbuffer, rbo);

OnLoad(_gl);
}
_gl.BindRenderbuffer(GLEnum.Renderbuffer, 0);

if (_gl.CheckFramebufferStatus(GLEnum.Framebuffer) != GLEnum.FramebufferComplete)
{
throw new InvalidOperationException("Offscreen framebuffer is not complete");
}
}
_gl.BindFramebuffer(GLEnum.Framebuffer, 0);

_writableBitmap = new GLImageSource(_width, _height, _pixels);
Source = _writableBitmap;
}

Render();
}

private unsafe void Render()
{
if (!IsLoaded)
{
return;
}

using var _1 = XamlRoot!.LockGL();
using var _2 = RestoreGLState();

_gl.BindFramebuffer(GLEnum.Framebuffer, _framebuffer);
{
_gl.Viewport(new System.Drawing.Size((int)_width, (int)_height));
RenderOverride(_gl);

_gl.ReadBuffer(GLEnum.ColorAttachment0);
_gl.ReadPixels(0, 0, _width, _height, GLEnum.Bgra, GLEnum.UnsignedByte, _pixels);
_writableBitmap.Render();
}

Invalidate();
}

private IDisposable RestoreGLState()
{
_gl.GetInteger(GLEnum.ArrayBufferBinding, out var oldArrayBuffer);
_gl.GetInteger(GLEnum.VertexArrayBinding, out var oldVertexArray);
_gl.GetInteger(GLEnum.FramebufferBinding, out var oldFramebuffer);
_gl.GetInteger(GLEnum.TextureBinding2D, out var oldTextureColorBuffer);
_gl.GetInteger(GLEnum.RenderbufferBinding, out var oldRbo);
return Disposable.Create(() =>
{
_gl.BindVertexArray((uint)oldVertexArray);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)oldArrayBuffer);
_gl.BindFramebuffer(GLEnum.Framebuffer, (uint)oldFramebuffer);
_gl.BindTexture(GLEnum.Texture2D, (uint)oldTextureColorBuffer);
_gl.BindRenderbuffer(GLEnum.Renderbuffer, (uint)oldRbo);
});
}

public void Invalidate() => DispatcherQueue.TryEnqueue(Render);
}
25 changes: 25 additions & 0 deletions src/Uno.UI/UI/Xaml/Media/Imaging/GLImageSource.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#nullable enable

using System;
using Microsoft.UI.Composition;
using Uno.UI.Xaml.Media;

using WinUICoreServices = Uno.UI.Xaml.Core.CoreServices;

namespace Microsoft.UI.Xaml.Media.Imaging
{
internal unsafe class GLImageSource(uint width, uint height, void* pixels) : ImageSource
{
private SkiaCompositionSurface _surface = new SkiaCompositionSurface();

private protected override bool TryOpenSourceSync(int? targetWidth, int? targetHeight, out ImageData image)
{
_surface.CopyPixels((int)width, (int)height, (IntPtr)pixels);
image = ImageData.FromCompositionSurface(_surface);
InvalidateImageSource();
return image.HasData;
}

public void Render() { InvalidateSource(); }
}
}
24 changes: 24 additions & 0 deletions src/Uno.UI/UI/Xaml/XamlRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
using Windows.Foundation;
using Windows.Graphics.Display;
using Uno.UI.Extensions;
using Windows.UI.Composition;
using Uno.Disposables;
using Uno.Foundation.Extensibility;
using Uno.UI.Hosting;
using Uno.UI.Xaml.Controls;

namespace Microsoft.UI.Xaml;
Expand Down Expand Up @@ -115,4 +119,24 @@ internal IDisposable OpenPopup(Microsoft.UI.Xaml.Controls.Primitives.Popup popup

return VisualTree.PopupRoot.OpenPopup(popup);
}

public object? GetGL()
{
if (ApiExtensibility.CreateInstance<XamlRootMap<IXamlRootHost>>(this, out var map) && map.GetHostForRoot(this) is { } host)
{
return host.GetGL();
}

return null;
}

public IDisposable LockGL()
{
if (ApiExtensibility.CreateInstance<XamlRootMap<IXamlRootHost>>(this, out var map) && map.GetHostForRoot(this) is { } host)
{
return host.LockGL();
}

return Disposable.Empty;
}
}

0 comments on commit ff6e501

Please sign in to comment.