From 508fad9d2c333fdca885dbb652665fd994f07d2f Mon Sep 17 00:00:00 2001 From: Lobster Uberlord Date: Wed, 15 Jun 2022 17:27:16 +0700 Subject: [PATCH 1/7] 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); From 37a1a5553de73c48c80b61347f7273641d4a6891 Mon Sep 17 00:00:00 2001 From: Lobster Uberlord Date: Mon, 4 Jul 2022 19:16:25 +0700 Subject: [PATCH 2/7] Use Marshal.AllocHGlobal to allocate frame buffer --- src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs index a286affe04..603a792de3 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs @@ -65,26 +65,22 @@ namespace Avalonia.Web.Blazor 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 IntPtr Address { get; } 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); + Address = Marshal.AllocHGlobal(width * height * bytesPerPixel); } public void Dispose() { - _dataHandle.Free(); + Marshal.FreeHGlobal(Address); } } } From 49aad04861f19c02ddc6a0780750ad29cc474c3b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 4 Jul 2022 08:56:47 -0400 Subject: [PATCH 3/7] Apply target's TemplatedParent to a Flyout and Tooltip, when it's opened (#8412) * Apply target's TemplatedParent to a Flyout and Tooltip, when it's opened * Add flyout and tooltip leak tests * Fix Flyout_Is_Freed --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 1 + src/Avalonia.Controls/Primitives/Popup.cs | 17 +-- .../Primitives/TemplatedControl.cs | 10 +- src/Avalonia.Controls/ToolTip.cs | 5 +- tests/Avalonia.LeakTests/ControlTests.cs | 105 +++++++++++++++ .../Data/TemplateBindingTests.cs | 127 ++++++++++++++++++ 6 files changed, 242 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 4801fa69f0..1504d2b25f 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -223,6 +223,7 @@ namespace Avalonia.Controls.Primitives { Popup.PlacementTarget = Target = placementTarget; ((ISetLogicalParent)Popup).SetParent(placementTarget); + Popup.SetValue(StyledElement.TemplatedParentProperty, placementTarget.TemplatedParent); } if (Popup.Child == null) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 95e5e25c42..1501d97470 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -860,22 +860,7 @@ namespace Avalonia.Controls.Primitives { if (control != null) { - var templatedParent = TemplatedParent; - - if (control.TemplatedParent == null) - { - control.SetValue(TemplatedParentProperty, templatedParent); - } - - control.ApplyTemplate(); - - if (!(control is IPresenter) && control.TemplatedParent == templatedParent) - { - foreach (IControl child in control.VisualChildren) - { - SetTemplatedParentAndApplyChildTemplates(child); - } - } + TemplatedControl.ApplyTemplatedParent(control, TemplatedParent); } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index db029d38c0..4403bfce51 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -285,7 +285,7 @@ namespace Avalonia.Controls.Primitives Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(this, "Creating control template"); var (child, nameScope) = template.Build(this); - ApplyTemplatedParent(child); + ApplyTemplatedParent(child, this); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); @@ -387,18 +387,18 @@ namespace Avalonia.Controls.Primitives /// Sets the TemplatedParent property for the created template children. /// /// The control. - private void ApplyTemplatedParent(IControl control) + internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent) { - control.SetValue(TemplatedParentProperty, this); + control.SetValue(TemplatedParentProperty, templatedParent); var children = control.LogicalChildren; var count = children.Count; for (var i = 0; i < count; i++) { - if (children[i] is IControl child) + if (children[i] is IStyledElement child) { - ApplyTemplatedParent(child); + ApplyTemplatedParent(child, templatedParent); } } } diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 91c93c87c8..bb18bf4c64 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -271,8 +271,9 @@ namespace Avalonia.Controls _popupHost = OverlayPopupHost.CreatePopupHost(control, null); _popupHost.SetChild(this); ((ISetLogicalParent)_popupHost).SetParent(control); - - _popupHost.ConfigurePosition(control, GetPlacement(control), + ApplyTemplatedParent(this, control.TemplatedParent); + + _popupHost.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); WindowManagerAddShadowHintChanged(_popupHost, false); diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 8c05f2a0a7..6fb7b1448c 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -7,6 +7,7 @@ using System.Reactive.Disposables; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; using Avalonia.Data; @@ -877,6 +878,110 @@ namespace Avalonia.LeakTests } } + [Fact] + public void ToolTip_Is_Freed() + { + using (Start()) + { + Func run = () => + { + var window = new Window(); + var source = new Button + { + Template = new FuncControlTemplate