From 508fad9d2c333fdca885dbb652665fd994f07d2f Mon Sep 17 00:00:00 2001 From: Lobster Uberlord Date: Wed, 15 Jun 2022 17:27:16 +0700 Subject: [PATCH] Enable use of Skia Raster backend for HTML canvas in Blazor To enable the raster backend set CustomGpuFactory to null in the existing SkiaOptions, by default Avalonia will use the GPU/GL Skia backend. --- samples/ControlCatalog.Web/App.razor.cs | 1 + .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 57 ++++++++---- .../BlazorSkiaRasterSurface.cs | 91 +++++++++++++++++++ .../Avalonia.Web.Blazor/BlazorSkiaSurface.cs | 2 +- .../Avalonia.Web.Blazor/IBlazorSkiaSurface.cs | 9 ++ .../RazorViewTopLevelImpl.cs | 7 +- 6 files changed, 149 insertions(+), 18 deletions(-) create mode 100644 src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs create mode 100644 src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs diff --git a/samples/ControlCatalog.Web/App.razor.cs b/samples/ControlCatalog.Web/App.razor.cs index c0b7ddbe1e..560e8079a6 100644 --- a/samples/ControlCatalog.Web/App.razor.cs +++ b/samples/ControlCatalog.Web/App.razor.cs @@ -11,6 +11,7 @@ public partial class App { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); }) + //.With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering .SetupWithSingleViewLifetime(); base.OnParametersSet(); diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index b575bc6dbb..0e5580ebe4 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -37,6 +37,7 @@ namespace Avalonia.Web.Blazor private const SKColorType ColorType = SKColorType.Rgba8888; private bool _initialised; + private bool _useGL; [Inject] private IJSRuntime Js { get; set; } = null!; @@ -261,25 +262,44 @@ namespace Avalonia.Web.Blazor _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); Console.WriteLine("Interop created"); - _jsGlInfo = _interop.InitGL(); - - Console.WriteLine("jsglinfo created - init gl"); + + var skiaOptions = AvaloniaLocator.Current.GetService(); + _useGL = skiaOptions?.CustomGpuFactory != null; - // create the SkiaSharp context - if (_context == null) + if (_useGL) + { + _jsGlInfo = _interop.InitGL(); + Console.WriteLine("jsglinfo created - init gl"); + } + else { - Console.WriteLine("create glcontext"); - _glInterface = GRGlInterface.Create(); - _context = GRContext.CreateGl(_glInterface); - - var options = AvaloniaLocator.Current.GetService(); - // bump the default resource cache limit - _context.SetResourceCacheLimit(options?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); - Console.WriteLine("glcontext created and resource limit set"); + var rasterInitialized = _interop.InitRaster(); + Console.WriteLine("raster initialized: {0}", rasterInitialized); } - _topLevelImpl.SetSurface(_context, _jsGlInfo, ColorType, - new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); + if (_useGL) + { + // create the SkiaSharp context + if (_context == null) + { + Console.WriteLine("create glcontext"); + _glInterface = GRGlInterface.Create(); + _context = GRContext.CreateGl(_glInterface); + + + // bump the default resource cache limit + _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); + Console.WriteLine("glcontext created and resource limit set"); + } + + _topLevelImpl.SetSurface(_context, _jsGlInfo!, ColorType, + new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); + } + else + { + _topLevelImpl.SetSurface(ColorType, + new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + } _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); @@ -301,7 +321,12 @@ namespace Avalonia.Web.Blazor private void OnRenderFrame() { - if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0 || _jsGlInfo == null) + if (_useGL && (_jsGlInfo == null)) + { + Console.WriteLine("nothing to render"); + return; + } + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) { Console.WriteLine("nothing to render"); return; diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs new file mode 100644 index 0000000000..a286affe04 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs @@ -0,0 +1,91 @@ +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable + { + public SKColorType ColorType { get; set; } + + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + + private FramebufferData? _fbData; + private readonly Action _blitCallback; + private readonly Action _onDisposeAction; + + public BlazorSkiaRasterSurface( + SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + ColorType = colorType; + Size = size; + Scaling = scaling; + _blitCallback = blitCallback; + _onDisposeAction = Blit; + } + + public void Dispose() + { + _fbData?.Dispose(); + _fbData = null; + } + + public ILockedFramebuffer Lock() + { + var bytesPerPixel = 4; // TODO: derive from ColorType + var dpi = Scaling * 96.0; + var width = (int)(Size.Width * Scaling); + var height = (int)(Size.Height * Scaling); + + if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height) + { + _fbData?.Dispose(); + _fbData = new FramebufferData(width, height, bytesPerPixel); + } + + var pixelFormat = ColorType.ToPixelFormat(); + var data = _fbData.Value; + return new LockedFramebuffer( + data.Address, data.Size, data.RowBytes, + new Vector(dpi, dpi), pixelFormat, _onDisposeAction); + } + + private void Blit() + { + if (_fbData != null) + { + var data = _fbData.Value; + _blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height)); + } + } + + private readonly struct FramebufferData + { + private readonly byte[]? _data; + private readonly GCHandle _dataHandle; + + public PixelSize Size { get; } + + public int RowBytes { get; } + + public IntPtr Address => _dataHandle.AddrOfPinnedObject(); + + public FramebufferData(int width, int height, int bytesPerPixel) + { + Size = new PixelSize(width, height); + RowBytes = width * bytesPerPixel; + _data = new byte[width * height * bytesPerPixel]; + _dataHandle = GCHandle.Alloc(_data, GCHandleType.Pinned); + } + + public void Dispose() + { + _dataHandle.Free(); + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs index 512309cfe3..fb49df338b 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs @@ -3,7 +3,7 @@ using SkiaSharp; namespace Avalonia.Web.Blazor { - internal class BlazorSkiaSurface + internal class BlazorSkiaSurface : IBlazorSkiaSurface { public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) { diff --git a/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs new file mode 100644 index 0000000000..5463893e27 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Web.Blazor +{ + internal interface IBlazorSkiaSurface + { + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs index a8a1a970dc..e240f1554e 100644 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs @@ -16,7 +16,7 @@ namespace Avalonia.Web.Blazor internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost { private Size _clientSize; - private BlazorSkiaSurface? _currentSurface; + private IBlazorSkiaSurface? _currentSurface; private IInputRoot? _inputRoot; private readonly Stopwatch _sw = Stopwatch.StartNew(); private readonly AvaloniaView _avaloniaView; @@ -40,6 +40,11 @@ namespace Avalonia.Web.Blazor new BlazorSkiaSurface(context, glInfo, colorType, size, scaling, GRSurfaceOrigin.BottomLeft); } + internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + _currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); + } + public void SetClientSize(SKSize size, double dpi) { var newSize = new Size(size.Width, size.Height);