diff --git a/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj b/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj
index c85c54f88932..21751e42c99d 100644
--- a/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj
+++ b/src/SamplesApp/SamplesApp.Skia/SamplesApp.Skia.csproj
@@ -1,6 +1,7 @@
$(NetPrevious)
+ true
@@ -30,6 +31,11 @@
+
+
+
+
+
diff --git a/src/SamplesApp/UnoIslandsSamplesApp.Skia/UnoIslandsSamplesApp.Skia.csproj b/src/SamplesApp/UnoIslandsSamplesApp.Skia/UnoIslandsSamplesApp.Skia.csproj
index 31941928eb44..ffb952aa2872 100644
--- a/src/SamplesApp/UnoIslandsSamplesApp.Skia/UnoIslandsSamplesApp.Skia.csproj
+++ b/src/SamplesApp/UnoIslandsSamplesApp.Skia/UnoIslandsSamplesApp.Skia.csproj
@@ -1,6 +1,7 @@
$(NetSkiaPreviousAndCurrent)
+ true
@@ -69,4 +70,13 @@
+
+
+ ..\..\..\..\..\..\.nuget\packages\silk.net.core\2.16.0\lib\net6.0\Silk.NET.Core.dll
+
+
+ ..\..\..\..\..\..\.nuget\packages\silk.net.opengl\2.16.0\lib\net5.0\Silk.NET.OpenGL.dll
+
+
+
diff --git a/src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs b/src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs
index 74a10d7c6eda..04607efe9bfa 100644
--- a/src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs
+++ b/src/Uno.UI.Composition/Composition/SkiaCompositionSurface.skia.cs
@@ -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);
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 9fca805f2504..f92a3f4572d1 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
@@ -35,6 +35,11 @@
+
+
+
+
+
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs b/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs
index 1e2f94510ca4..270e61c0936a 100644
--- a/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs
+++ b/src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs
@@ -52,6 +52,7 @@ static X11ApplicationHost()
ApiExtensibility.Register(typeof(IUnoCorePointerInputSource), o => new X11PointerInputSource(o));
ApiExtensibility.Register(typeof(IUnoKeyboardInputSource), o => new X11KeyboardInputSource(o));
+ ApiExtensibility.Register(typeof(XamlRootMap), _ => X11Manager.XamlRootMap);
ApiExtensibility.Register(typeof(INativeWindowFactoryExtension), _ => new X11NativeWindowFactoryExtension());
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11OpenGLRenderer.cs b/src/Uno.UI.Runtime.Skia.X11/X11OpenGLRenderer.cs
index c0ef1d82c2a6..a6cec70d182f 100644
--- a/src/Uno.UI.Runtime.Skia.X11/X11OpenGLRenderer.cs
+++ b/src/Uno.UI.Runtime.Skia.X11/X11OpenGLRenderer.cs
@@ -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 })
{
diff --git a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs
index 820cd3975901..e15b6e2d9c01 100644
--- a/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs
+++ b/src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.cs
@@ -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;
@@ -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;
@@ -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);
+ }
}
diff --git a/src/Uno.UI/Hosting/IXamlRootHost.cs b/src/Uno.UI/Hosting/IXamlRootHost.cs
index d70b6b4f5865..a64d0222764c 100644
--- a/src/Uno.UI/Hosting/IXamlRootHost.cs
+++ b/src/Uno.UI/Hosting/IXamlRootHost.cs
@@ -1,6 +1,8 @@
#nullable enable
+using System;
using Microsoft.UI.Xaml;
+using Uno.Disposables;
namespace Uno.UI.Hosting;
@@ -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;
}
diff --git a/src/Uno.UI/UI/Xaml/Controls/Image/OpenGLImage.skia.cs b/src/Uno.UI/UI/Xaml/Controls/Image/OpenGLImage.skia.cs
new file mode 100644
index 000000000000..a1464269d7a2
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Controls/Image/OpenGLImage.skia.cs
@@ -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);
+}
diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/GLImageSource.skia.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/GLImageSource.skia.cs
new file mode 100644
index 000000000000..8333f8a710f2
--- /dev/null
+++ b/src/Uno.UI/UI/Xaml/Media/Imaging/GLImageSource.skia.cs
@@ -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(); }
+ }
+}
diff --git a/src/Uno.UI/UI/Xaml/XamlRoot.cs b/src/Uno.UI/UI/Xaml/XamlRoot.cs
index 1f49c375e765..75b69ec1639c 100644
--- a/src/Uno.UI/UI/Xaml/XamlRoot.cs
+++ b/src/Uno.UI/UI/Xaml/XamlRoot.cs
@@ -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;
@@ -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>(this, out var map) && map.GetHostForRoot(this) is { } host)
+ {
+ return host.GetGL();
+ }
+
+ return null;
+ }
+
+ public IDisposable LockGL()
+ {
+ if (ApiExtensibility.CreateInstance>(this, out var map) && map.GetHostForRoot(this) is { } host)
+ {
+ return host.LockGL();
+ }
+
+ return Disposable.Empty;
+ }
}