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; + } }