From 086fe5267ff4ac65d8877f7108dfeef21121eb14 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 13 Mar 2024 01:23:24 +0600 Subject: [PATCH] Don't create a layer if the previous frame is retained by the render target (#14924) * Don't create a layer if the previous frame is retained by the render target * D2D * compile * Only check PreviousFrameIsRetained if not using layer * ABI --- samples/ControlCatalog.NetCore/Program.cs | 8 +- src/Avalonia.Base/Platform/IRenderTarget.cs | 27 ++++ .../Platform/RenderTargetProperties.cs | 30 ++++ .../Platform/RetainedFramebuffer.cs | 54 +++++++ .../Rendering/Composition/Compositor.cs | 5 +- .../Server/ServerCompositionTarget.cs | 86 +++++++---- .../Composition/Server/ServerCompositor.cs | 4 +- .../Avalonia.Controls.csproj | 2 + .../Surfaces/IFramebufferPlatformSurface.cs | 42 ++++++ src/Avalonia.X11/X11Framebuffer.cs | 64 --------- src/Avalonia.X11/X11FramebufferSurface.cs | 59 +++++++- src/Avalonia.X11/X11Platform.cs | 8 ++ src/Avalonia.X11/X11Window.cs | 2 +- .../LinuxFramebufferPlatform.cs | 4 + .../Output/FbDevBackBuffer.cs | 18 ++- .../Output/FbDevOutputOptions.cs | 28 ++++ .../Output/FbdevOutput.cs | 44 +++++- .../Avalonia.Skia/FramebufferRenderTarget.cs | 33 ++++- .../Imaging/WicRenderTargetBitmapImpl.cs | 1 + .../Composition/DirectFbCompositionTests.cs | 135 ++++++++++++++++++ tests/Avalonia.RenderTests/TestBase.cs | 23 +-- ...ised_advertized-False_initial.expected.png | Bin 0 -> 633 bytes ...ised_advertized-False_updated.expected.png | Bin 0 -> 639 bytes ...tised_advertized-True_initial.expected.png | Bin 0 -> 633 bytes ...tised_advertized-True_updated.expected.png | Bin 0 -> 358 bytes 25 files changed, 547 insertions(+), 130 deletions(-) create mode 100644 src/Avalonia.Base/Platform/RenderTargetProperties.cs create mode 100644 src/Avalonia.Base/Platform/RetainedFramebuffer.cs delete mode 100644 src/Avalonia.X11/X11Framebuffer.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs create mode 100644 tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs create mode 100644 tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_initial.expected.png create mode 100644 tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_updated.expected.png create mode 100644 tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_initial.expected.png create mode 100644 tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index fa5b78c278..b3372ee0f8 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -9,6 +9,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Fonts.Inter; using Avalonia.Headless; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.LogicalTree; using Avalonia.Rendering.Composition; using Avalonia.Threading; @@ -52,8 +53,11 @@ namespace ControlCatalog.NetCore } if (s_useFramebuffer) { - SilenceConsole(); - return builder.StartLinuxFbDev(args, scaling: GetScaling()); + SilenceConsole(); + return builder.StartLinuxFbDev(args, new FbDevOutputOptions() + { + Scaling = GetScaling() + }); } else if (args.Contains("--vnc")) { diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs index a1f33db6c7..39504ac9fb 100644 --- a/src/Avalonia.Base/Platform/IRenderTarget.cs +++ b/src/Avalonia.Base/Platform/IRenderTarget.cs @@ -24,4 +24,31 @@ namespace Avalonia.Platform /// public bool IsCorrupted { get; } } + + [PrivateApi] + public interface IRenderTargetWithProperties : IRenderTarget + { + RenderTargetProperties Properties { get; } + + /// + /// Creates an for a rendering session. + /// + /// Apply DPI reported by the render target as a hidden transform matrix + /// Returns various properties about the returned drawing context + IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, out RenderTargetDrawingContextProperties properties); + } + + internal static class RenderTargetExtensions + { + public static IDrawingContextImpl CreateDrawingContextWithProperties( + this IRenderTarget renderTarget, + bool useScaledDrawing, + out RenderTargetDrawingContextProperties properties) + { + if (renderTarget is IRenderTargetWithProperties target) + return target.CreateDrawingContext(useScaledDrawing, out properties); + properties = default; + return renderTarget.CreateDrawingContext(useScaledDrawing); + } + } } diff --git a/src/Avalonia.Base/Platform/RenderTargetProperties.cs b/src/Avalonia.Base/Platform/RenderTargetProperties.cs new file mode 100644 index 0000000000..33b117ff1e --- /dev/null +++ b/src/Avalonia.Base/Platform/RenderTargetProperties.cs @@ -0,0 +1,30 @@ +using Avalonia.Metadata; + +namespace Avalonia.Platform; + +[PrivateApi] +public struct RenderTargetProperties +{ + /// + /// Indicates that render target contents are preserved between CreateDrawingContext calls. + /// Notable examples are retained CPU-memory framebuffers and + /// swapchains with DXGI_SWAP_EFFECT_SEQUENTIAL/DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL + /// + public bool RetainsPreviousFrameContents { get; init; } + + /// + /// Indicates that the render target can be used without CreateLayer + /// It's currently not true for every render target, since with OpenGL rendering we often use + /// framebuffers without a stencil attachment that is required for clipping with Skia + /// + public bool IsSuitableForDirectRendering { get; init; } +} + +[PrivateApi] +public struct RenderTargetDrawingContextProperties +{ + /// + /// Indicates that the drawing context targets a surface that preserved its contents since the previous frame + /// + public bool PreviousFrameIsRetained { get; init; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/RetainedFramebuffer.cs b/src/Avalonia.Base/Platform/RetainedFramebuffer.cs new file mode 100644 index 0000000000..e5ca1070dc --- /dev/null +++ b/src/Avalonia.Base/Platform/RetainedFramebuffer.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Metadata; +using Avalonia.Platform.Internal; + +namespace Avalonia.Platform; + +internal class RetainedFramebuffer : IDisposable +{ + public PixelSize Size { get; } + public int RowBytes { get; } + public PixelFormat Format { get; } + public IntPtr Address => _blob?.Address ?? throw new ObjectDisposedException(nameof(RetainedFramebuffer)); + private UnmanagedBlob? _blob; + + static PixelFormat ValidateKnownFormat(PixelFormat format) => format.BitsPerPixel % 8 == 0 + ? format + : throw new ArgumentOutOfRangeException(nameof(format)); + + public RetainedFramebuffer(PixelSize size, PixelFormat format) : this(size, ValidateKnownFormat(format), + format.BitsPerPixel / 8 * size.Width) + { + + } + + public RetainedFramebuffer(PixelSize size, PixelFormat format, int rowBytes) + { + if (size.Width <= 0 || size.Height <= 0) + throw new ArgumentOutOfRangeException(nameof(size)); + if (size.Width * (format.BitsPerPixel / 8) > rowBytes) + throw new ArgumentOutOfRangeException(nameof(rowBytes)); + Size = size; + RowBytes = rowBytes; + Format = format; + _blob = new UnmanagedBlob(RowBytes * size.Height); + } + + public ILockedFramebuffer Lock(Vector dpi, Action blit) + { + if (_blob == null) + throw new ObjectDisposedException(nameof(RetainedFramebuffer)); + return new LockedFramebuffer(_blob.Address, Size, RowBytes, dpi, Format, () => + { + blit(this); + GC.KeepAlive(this); + }); + } + + public void Dispose() + { + _blob?.Dispose(); + _blob = null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index a3a8e0e97a..48553d3b91 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -77,14 +77,15 @@ namespace Avalonia.Rendering.Composition internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu, bool useUiThreadForSynchronousCommits, ICompositorScheduler scheduler, bool reclaimBuffersImmediately, - Dispatcher dispatcher) + Dispatcher dispatcher, CompositionOptions? options = null) { + options ??= AvaloniaLocator.Current.GetService() ?? new(); Loop = loop; UseUiThreadForSynchronousCommits = useUiThreadForSynchronousCommits; Dispatcher = dispatcher; _batchMemoryPool = new(reclaimBuffersImmediately); _batchObjectPool = new(reclaimBuffersImmediately); - _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool); + _server = new ServerCompositor(loop, gpu, options, _batchObjectPool, _batchMemoryPool); _triggerCommitRequested = () => scheduler.CommitRequested(this); DefaultEasing = new SplineEasing(new KeySpline(0.25, 0.1, 0.25, 1.0)); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index e39ffc5e7f..8d374cb15e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -30,6 +30,7 @@ namespace Avalonia.Rendering.Composition.Server private IDrawingContextLayerImpl? _layer; private bool _updateRequested; private bool _redrawRequested; + private bool _fullRedrawRequested; private bool _disposed; private readonly HashSet _attachedVisuals = new(); private readonly Queue _adornerUpdateQueue = new(); @@ -116,6 +117,7 @@ namespace Avalonia.Rendering.Composition.Server partial void DeserializeChangesExtra(BatchStreamReader c) { _redrawRequested = true; + _fullRedrawRequested = true; } public void Render() @@ -175,46 +177,56 @@ namespace Avalonia.Rendering.Composition.Server if (!_redrawRequested) return; _redrawRequested = false; - using (var targetContext = _renderTarget.CreateDrawingContext(false)) + + var renderTargetWithProperties = _renderTarget as IRenderTargetWithProperties; + + + var needLayer = DebugOverlays != RendererDebugOverlays.None // Check if we don't need overlays + // Check if render target can be rendered to directly and preserves the previous frame + || !(renderTargetWithProperties?.Properties.RetainsPreviousFrameContents == true + && renderTargetWithProperties?.Properties.IsSuitableForDirectRendering == true); + + using (var renderTargetContext = _renderTarget.CreateDrawingContextWithProperties(false, out var properties)) { - if (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted) + if(needLayer && (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted)) { _layer?.Dispose(); _layer = null; - _layer = targetContext.CreateLayer(PixelSize); + _layer = renderTargetContext.CreateLayer(PixelSize); _layerSize = PixelSize; DirtyRects.AddRect(new PixelRect(_layerSize)); } + else if (!needLayer) + { + _layer?.Dispose(); + _layer = null; + } + + if (_fullRedrawRequested || (!needLayer && !properties.PreviousFrameIsRetained)) + { + DirtyRects.AddRect(new PixelRect(_layerSize)); + _fullRedrawRequested = false; + } if (!DirtyRects.IsEmpty) { - var useLayerClip = Compositor.Options.UseSaveLayerRootClip ?? - Compositor.RenderInterface.GpuContext != null; - using (var context = _layer.CreateDrawingContext(false)) + if (_layer != null) { - using (DirtyRects.BeginDraw(context)) + using (var context = _layer.CreateDrawingContext(false)) + RenderRootToContextWithClip(context, Root); + + renderTargetContext.Clear(Colors.Transparent); + renderTargetContext.Transform = Matrix.Identity; + if (_layer.CanBlit) + _layer.Blit(renderTargetContext); + else { - context.Clear(Colors.Transparent); - if (useLayerClip) - context.PushLayer(DirtyRects.CombinedRect.ToRect(1)); - - - Root.Render(new CompositorDrawingContextProxy(context), null, DirtyRects); - - if (useLayerClip) - context.PopLayer(); + var rect = new PixelRect(default, PixelSize).ToRect(1); + renderTargetContext.DrawBitmap(_layer, 1, rect, rect); } } - } - - targetContext.Clear(Colors.Transparent); - targetContext.Transform = Matrix.Identity; - if (_layer.CanBlit) - _layer.Blit(targetContext); - else - { - var rect = new PixelRect(default, PixelSize).ToRect(1); - targetContext.DrawBitmap(_layer, 1, rect, rect); + else + RenderRootToContextWithClip(renderTargetContext, Root); } if (DebugOverlays != RendererDebugOverlays.None) @@ -225,7 +237,7 @@ namespace Avalonia.Rendering.Composition.Server RenderTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds); } - DrawOverlays(targetContext, PixelSize.ToSize(Scaling)); + DrawOverlays(renderTargetContext, PixelSize.ToSize(Scaling)); } RenderedVisuals = 0; @@ -234,6 +246,26 @@ namespace Avalonia.Rendering.Composition.Server } } + void RenderRootToContextWithClip(IDrawingContextImpl context, ServerCompositionVisual root) + { + var useLayerClip = Compositor.Options.UseSaveLayerRootClip ?? + Compositor.RenderInterface.GpuContext != null; + + using (DirtyRects.BeginDraw(context)) + { + context.Clear(Colors.Transparent); + if (useLayerClip) + context.PushLayer(DirtyRects.CombinedRect.ToRect(1)); + + + root.Render(new CompositorDrawingContextProxy(context), null, DirtyRects); + + if (useLayerClip) + context.PopLayer(); + } + } + + private void DrawOverlays(IDrawingContextImpl targetContext, Size logicalSize) { if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index c9f4474654..a471fc765b 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -38,11 +38,13 @@ namespace Avalonia.Rendering.Composition.Server internal static readonly object RenderThreadDisposeStartMarker = new(); internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new(); - public CompositionOptions Options { get; } = AvaloniaLocator.Current.GetService() ?? new(); + public CompositionOptions Options { get; } public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, + CompositionOptions options, BatchStreamObjectPool batchObjectPool, BatchStreamMemoryPool batchMemoryPool) { + Options = options; _renderLoop = renderLoop; RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics); RenderInterface.ContextDisposed += RT_OnContextDisposed; diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index aa48786e4e..2583872bd7 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -13,6 +13,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs index 22ee322e36..a717bfcd22 100644 --- a/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs +++ b/src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs @@ -21,6 +21,23 @@ namespace Avalonia.Controls.Platform.Surfaces /// ILockedFramebuffer Lock(); } + + [PrivateApi] + public interface IFramebufferRenderTargetWithProperties : IFramebufferRenderTarget + { + /// + /// Provides a framebuffer descriptor for drawing. + /// + /// + /// Contents should be drawn on actual window after disposing + /// + ILockedFramebuffer Lock(out FramebufferLockProperties properties); + + bool RetainsFrameContents { get; } + } + + [PrivateApi] + public record struct FramebufferLockProperties(bool PreviousFrameIsRetained); /// /// For simple cases when framebuffer is always available @@ -41,4 +58,29 @@ namespace Avalonia.Controls.Platform.Surfaces public ILockedFramebuffer Lock() => _lockFramebuffer(); } + + internal class FuncRetainedFramebufferRenderTarget : IFramebufferRenderTargetWithProperties + { + public delegate ILockedFramebuffer LockDelegate(out FramebufferLockProperties properties); + private readonly LockDelegate _lockFramebuffer; + + public FuncRetainedFramebufferRenderTarget(LockDelegate lockFramebuffer) + { + _lockFramebuffer = lockFramebuffer; + } + + public void Dispose() + { + // No-op + } + + public ILockedFramebuffer Lock() => _lockFramebuffer(out _); + + public ILockedFramebuffer Lock(out FramebufferLockProperties properties) + { + return _lockFramebuffer(out properties); + } + + public bool RetainsFrameContents => true; + } } diff --git a/src/Avalonia.X11/X11Framebuffer.cs b/src/Avalonia.X11/X11Framebuffer.cs deleted file mode 100644 index 02cfe4a517..0000000000 --- a/src/Avalonia.X11/X11Framebuffer.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.IO; -using Avalonia.Platform; -using Avalonia.Platform.Internal; -using SkiaSharp; -using static Avalonia.X11.XLib; -namespace Avalonia.X11 -{ - internal class X11Framebuffer : ILockedFramebuffer - { - private readonly IntPtr _display; - private readonly IntPtr _xid; - private readonly int _depth; - private UnmanagedBlob _blob; - - public X11Framebuffer(IntPtr display, IntPtr xid, int depth, int width, int height, double factor) - { - // HACK! Please fix renderer, should never ask for 0x0 bitmap. - width = Math.Max(1, width); - height = Math.Max(1, height); - - _display = display; - _xid = xid; - _depth = depth; - Size = new PixelSize(width, height); - RowBytes = width * 4; - Dpi = new Vector(96, 96) * factor; - Format = PixelFormat.Bgra8888; - _blob = new UnmanagedBlob(RowBytes * height); - Address = _blob.Address; - } - - public void Dispose() - { - var image = new XImage(); - int bitsPerPixel = 32; - image.width = Size.Width; - image.height = Size.Height; - image.format = 2; //ZPixmap; - image.data = Address; - image.byte_order = 0;// LSBFirst; - image.bitmap_unit = bitsPerPixel; - image.bitmap_bit_order = 0;// LSBFirst; - image.bitmap_pad = bitsPerPixel; - image.depth = _depth; - image.bytes_per_line = RowBytes; - image.bits_per_pixel = bitsPerPixel; - XLockDisplay(_display); - XInitImage(ref image); - var gc = XCreateGC(_display, _xid, 0, IntPtr.Zero); - XPutImage(_display, _xid, gc, ref image, 0, 0, 0, 0, (uint) Size.Width, (uint) Size.Height); - XFreeGC(_display, gc); - XSync(_display, true); - XUnlockDisplay(_display); - _blob.Dispose(); - } - - public IntPtr Address { get; } - public PixelSize Size { get; } - public int RowBytes { get; } - public Vector Dpi { get; } - public PixelFormat Format { get; } - } -} diff --git a/src/Avalonia.X11/X11FramebufferSurface.cs b/src/Avalonia.X11/X11FramebufferSurface.cs index 6daa040593..5b878673d4 100644 --- a/src/Avalonia.X11/X11FramebufferSurface.cs +++ b/src/Avalonia.X11/X11FramebufferSurface.cs @@ -9,25 +9,70 @@ namespace Avalonia.X11 private readonly IntPtr _display; private readonly IntPtr _xid; private readonly int _depth; - private readonly Func _scaling; + private readonly bool _retain; + private RetainedFramebuffer? _fb; - public X11FramebufferSurface(IntPtr display, IntPtr xid, int depth, Func scaling) + public X11FramebufferSurface(IntPtr display, IntPtr xid, int depth, bool retain) { _display = display; _xid = xid; _depth = depth; - _scaling = scaling; + _retain = retain; + } + + void Blit(RetainedFramebuffer fb) + { + var image = new XImage(); + int bitsPerPixel = 32; + image.width = fb.Size.Width; + image.height = fb.Size.Height; + image.format = 2; //ZPixmap; + image.data = fb.Address; + image.byte_order = 0;// LSBFirst; + image.bitmap_unit = bitsPerPixel; + image.bitmap_bit_order = 0;// LSBFirst; + image.bitmap_pad = bitsPerPixel; + image.depth = _depth; + image.bytes_per_line = fb.RowBytes; + image.bits_per_pixel = bitsPerPixel; + XLockDisplay(_display); + XInitImage(ref image); + var gc = XCreateGC(_display, _xid, 0, IntPtr.Zero); + XPutImage(_display, _xid, gc, ref image, 0, 0, 0, 0, (uint)fb.Size.Width, (uint)fb.Size.Height); + XFreeGC(_display, gc); + XSync(_display, true); + XUnlockDisplay(_display); + if (!_retain) + { + _fb?.Dispose(); + _fb = null; + } } - public ILockedFramebuffer Lock() + public ILockedFramebuffer Lock(out FramebufferLockProperties properties) { XLockDisplay(_display); XGetGeometry(_display, _xid, out var root, out var x, out var y, out var width, out var height, out var bw, out var d); XUnlockDisplay(_display); - return new X11Framebuffer(_display, _xid, _depth, width, height, _scaling()); + + var framebufferValid = (_fb != null && _fb.Size.Width == width && _fb.Size.Height == height); + if (!framebufferValid) + { + _fb?.Dispose(); + _fb = null; + _fb = new RetainedFramebuffer(new PixelSize(width, height), PixelFormat.Bgra8888); + } + + properties = new FramebufferLockProperties(framebufferValid); + return _fb.Lock(new Vector(96, 96), Blit); + } + + public IFramebufferRenderTarget CreateFramebufferRenderTarget() + { + return _retain + ? new FuncRetainedFramebufferRenderTarget(Lock) + : new FuncFramebufferRenderTarget(() => Lock(out _)); } - - public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncFramebufferRenderTarget(Lock); } } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 9e1757d4ef..c6856aff2a 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -344,6 +344,14 @@ namespace Avalonia /// public bool? EnableMultiTouch { get; set; } = true; + /// + /// Retain window framebuffer contents if using CPU rendering mode. + /// This will keep an offscreen bitmap for each window with contents of the previous frame + /// While improving performance by saving a blit, it will increase memory consumption + /// if you have many windows + /// + public bool? UseRetainedFramebuffer { get; set; } + public X11PlatformOptions() { try diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index cf1f8c5790..55387057e3 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -192,7 +192,7 @@ namespace Avalonia.X11 var surfaces = new List { new X11FramebufferSurface(_x11.DeferredDisplay, _renderHandle, - depth, () => RenderScaling) + depth, _platform.Options.UseRetainedFramebuffer ?? false) }; if (egl != null) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index f8f6b92b3e..7eb3088cf7 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -166,6 +166,10 @@ public static class LinuxFramebufferPlatformExtensions public static int StartLinuxFbDev(this AppBuilder builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend? inputBackend = default) => StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend); + public static int StartLinuxFbDev(this AppBuilder builder, string[] args, FbDevOutputOptions options, + IInputBackend? inputBackend = default) + => StartLinuxDirect(builder, args, new FbdevOutput(options), inputBackend); + public static int StartLinuxDrm(this AppBuilder builder, string[] args, string? card = null, double scaling = 1, IInputBackend? inputBackend = default) => StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend); public static int StartLinuxDrm(this AppBuilder builder, string[] args, string? card = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null, IInputBackend? inputBackend = default) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs index 7afad13bb6..e3729ddb8b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs @@ -32,17 +32,23 @@ namespace Avalonia.LinuxFramebuffer.Output } } + public static LockedFramebuffer LockFb(IntPtr address, fb_var_screeninfo varInfo, + fb_fix_screeninfo fixedInfo, Vector dpi, Action? dispose) + { + return new LockedFramebuffer(address, + new PixelSize((int)varInfo.xres, (int)varInfo.yres), + (int)fixedInfo.line_length, dpi, + varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 + : varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 + : PixelFormat.Bgra8888, dispose); + } + public ILockedFramebuffer Lock(Vector dpi) { Monitor.Enter(_lock); try { - return new LockedFramebuffer(Address, - new PixelSize((int)_varInfo.xres, (int)_varInfo.yres), - (int)_fixedInfo.line_length, dpi, - _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 - : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 - : PixelFormat.Bgra8888, + return LockFb(Address, _varInfo, _fixedInfo, dpi, () => { try diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs new file mode 100644 index 0000000000..cdf39614d1 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs @@ -0,0 +1,28 @@ +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer.Output; + +public class FbDevOutputOptions +{ + /// + /// The frame buffer device name. + /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set + /// + public string? FileName { get; set; } + /// + /// The required pixel format for the frame buffer. + /// A null value will leave the frame buffer in the current pixel format. + /// Otherwise sets the frame buffer to the required format + /// + public PixelFormat? PixelFormat { get; set; } + /// + /// If set to true, double-buffering will be disabled and scene will be composed directly into mmap-ed memory region + /// While this mode saves a blit, you need to check if it won't cause rendering artifacts your particular device. + /// + public bool RenderDirectlyToMappedMemory { get; set; } + + /// + /// The initial scale factor to use + /// + public double Scaling { get; set; } = 1; +} \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index d85485e7b3..a4bf4fdd47 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -15,6 +15,8 @@ namespace Avalonia.LinuxFramebuffer private IntPtr _mappedLength; private IntPtr _mappedAddress; private FbDevBackBuffer _backBuffer; + private readonly FbDevOutputOptions _options; + private bool _lockedAtLeastOnce; public double Scaling { get; set; } /// @@ -34,16 +36,30 @@ namespace Avalonia.LinuxFramebuffer /// The required pixel format for the frame buffer. /// A null value will leave the frame buffer in the current pixel format. /// Otherwise sets the frame buffer to the required format - public FbdevOutput(string fileName, PixelFormat? format) + public FbdevOutput(string fileName, PixelFormat? format) : this(new FbDevOutputOptions() { - fileName ??= Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; + FileName = fileName, + PixelFormat = format + }) + { + + } + + /// + /// Create a Linux frame buffer device output + /// + /// Options + public FbdevOutput(FbDevOutputOptions options) + { + var fileName = options.FileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; _fd = NativeUnsafeMethods.open(fileName, 2, 0); if (_fd <= 0) throw new Exception("Error: " + Marshal.GetLastWin32Error()); - + _options = options; + Scaling = options.Scaling; try { - Init(format); + Init(options.PixelFormat); } catch { @@ -144,16 +160,32 @@ namespace Avalonia.LinuxFramebuffer } } - public ILockedFramebuffer Lock() + public ILockedFramebuffer Lock() => Lock(out _); + + private ILockedFramebuffer Lock(out FramebufferLockProperties properties) { if (_fd <= 0) throw new ObjectDisposedException("LinuxFramebuffer"); + + var dpi = new Vector(96, 96) * Scaling; + + if (_options.RenderDirectlyToMappedMemory) + { + properties = new FramebufferLockProperties(_lockedAtLeastOnce); + _lockedAtLeastOnce = true; + NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIO_WAITFORVSYNC, null); + return FbDevBackBuffer.LockFb(_mappedAddress, _varInfo, _fixedInfo, dpi, null); + } + + var retained = _lockedAtLeastOnce && _backBuffer != null; + _lockedAtLeastOnce = true; + properties = new FramebufferLockProperties(retained); return (_backBuffer ??= new FbDevBackBuffer(_fd, _fixedInfo, _varInfo, _mappedAddress)) .Lock(new Vector(96, 96) * Scaling); } - public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncFramebufferRenderTarget(Lock); + public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncRetainedFramebufferRenderTarget(Lock); private void ReleaseUnmanagedResources() diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 8b50cd8295..4b8f179184 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -10,7 +10,7 @@ namespace Avalonia.Skia /// /// Skia render target that renders to a framebuffer surface. No gpu acceleration available. /// - internal class FramebufferRenderTarget : IRenderTarget + internal class FramebufferRenderTarget : IRenderTargetWithProperties { private SKImageInfo _currentImageInfo; private IntPtr _currentFramebufferAddress; @@ -18,6 +18,8 @@ namespace Avalonia.Skia private PixelFormatConversionShim? _conversionShim; private IDisposable? _preFramebufferCopyHandler; private IFramebufferRenderTarget? _renderTarget; + private IFramebufferRenderTargetWithProperties? _renderTargetWithProperties; + private bool _hadConversionShim; /// /// Create new framebuffer render target using a target surface. @@ -26,6 +28,7 @@ namespace Avalonia.Skia public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface) { _renderTarget = platformSurface.CreateFramebufferRenderTarget(); + _renderTargetWithProperties = _renderTarget as IFramebufferRenderTargetWithProperties; } /// @@ -33,21 +36,36 @@ namespace Avalonia.Skia { _renderTarget?.Dispose(); _renderTarget = null; + _renderTargetWithProperties = null; FreeSurface(); } + public RenderTargetProperties Properties => new() + { + RetainsPreviousFrameContents = !_hadConversionShim + && _renderTargetWithProperties?.RetainsFrameContents == true, + IsSuitableForDirectRendering = true + }; + + + /// + public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi) => + CreateDrawingContext(scaleDrawingToDpi, out _); + /// - public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi) + public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, out RenderTargetDrawingContextProperties properties) { if (_renderTarget == null) throw new ObjectDisposedException(nameof(FramebufferRenderTarget)); - - var framebuffer = _renderTarget.Lock(); + + FramebufferLockProperties lockProperties = default; + var framebuffer = _renderTargetWithProperties?.Lock(out lockProperties) ?? _renderTarget.Lock(); var framebufferImageInfo = new SKImageInfo(framebuffer.Size.Width, framebuffer.Size.Height, framebuffer.Format.ToSkColorType(), framebuffer.Format == PixelFormat.Rgb565 ? SKAlphaType.Opaque : SKAlphaType.Premul); CreateSurface(framebufferImageInfo, framebuffer); + _hadConversionShim |= _conversionShim != null; var canvas = _framebufferSurface.Canvas; @@ -59,9 +77,14 @@ namespace Avalonia.Skia { Surface = _framebufferSurface, Dpi = framebuffer.Dpi, - ScaleDrawingToDpi = scaleDrawingToDpi + ScaleDrawingToDpi = useScaledDrawing }; + properties = new() + { + PreviousFrameIsRetained = !_hadConversionShim && lockProperties.PreviousFrameIsRetained + }; + return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index 1120366a8e..1855063abf 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Platform; using Avalonia.Rendering; using SharpDX.Direct2D1; +using RenderTargetProperties = SharpDX.Direct2D1.RenderTargetProperties; namespace Avalonia.Direct2D1.Media { diff --git a/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs b/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs new file mode 100644 index 0000000000..cdf213d7c8 --- /dev/null +++ b/tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs @@ -0,0 +1,135 @@ +#if AVALONIA_SKIA +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Threading; +using Avalonia.UnitTests; +using SkiaSharp; +using Xunit; + +namespace Avalonia.Skia.RenderTests; + +public class DirectFbCompositionTests : TestBase +{ + public DirectFbCompositionTests() + : base(@"Composition\DirectFb") + { + } + + class FuncFramebufferSurface : IFramebufferPlatformSurface + { + private readonly Func _cb; + + public FuncFramebufferSurface(Func cb) + { + _cb = cb; + } + + public IFramebufferRenderTarget CreateFramebufferRenderTarget() + { + return _cb(); + } + } + + [Theory, + InlineData(false), + InlineData(true)] + void Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised(bool advertised) + { + var timer = new ManualRenderTimer(); + var compositor = new Compositor(new RenderLoop(timer), null, true, + new DispatcherCompositorScheduler(), true, Dispatcher.UIThread, new CompositionOptions + { + UseRegionDirtyRectClipping = true + }); + + Rectangle r1, r2; + var control = new Canvas + { + Width = 200, Height = 200, Background = Brushes.Yellow, + Children = + { + (r1 = new Rectangle + { + Fill = Brushes.Black, + Width = 40, + Height = 40, + Opacity = 0.6, + [Canvas.LeftProperty] = 40, + [Canvas.TopProperty] = 40, + }), + (r2 = new Rectangle + { + Fill = Brushes.Black, + Width = 40, + Height = 40, + Opacity = 0.6, + [Canvas.LeftProperty] = 120, + [Canvas.TopProperty] = 40, + }), + } + }; + var root = new TestRenderRoot(1, null!); + SKBitmap fb = new SKBitmap(200, 200, SKColorType.Rgba8888, SKAlphaType.Premul); + + ILockedFramebuffer LockFb() => new LockedFramebuffer(fb.GetAddress(0, 0), new(fb.Width, fb.Height), + fb.RowBytes, new Vector(96, 96), PixelFormat.Rgba8888, null); + + bool previousFrameIsRetained = false; + IFramebufferRenderTarget rt = advertised + ? new FuncRetainedFramebufferRenderTarget((out FramebufferLockProperties props) => + { + props = new() { PreviousFrameIsRetained = previousFrameIsRetained }; + return LockFb(); + }) + : new FuncFramebufferRenderTarget(LockFb); + + using var renderer = + new CompositingRenderer(root, compositor, () => new[] { new FuncFramebufferSurface(() => rt) }); + root.Initialize(renderer, control); + control.Measure(new Size(control.Width, control.Height)); + control.Arrange(new Rect(control.DesiredSize)); + renderer.Start(); + Dispatcher.UIThread.RunJobs(); + timer.TriggerTick(); + var image1 = + $"{nameof(Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised)}_advertized-{advertised}_initial"; + SaveFile(fb, image1); + + fb.Erase(SKColor.Empty); + + previousFrameIsRetained = advertised; + + r1.Fill = Brushes.Red; + r2.Fill = Brushes.Green; + Dispatcher.UIThread.RunJobs(); + timer.TriggerTick(); + var image2 = + $"{nameof(Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised)}_advertized-{advertised}_updated"; + SaveFile(fb, image2); + CompareImages(image1, skipImmediate: true); + CompareImages(image2, skipImmediate: true); + + } + + void SaveFile(SKBitmap bmp, string name) + { + Directory.CreateDirectory(OutputPath); + var path = System.IO.Path.Combine(OutputPath, name + ".composited.out.png"); + using var d = bmp.Encode(SKEncodedImageFormat.Png, 100); + using var f = File.Create(path); + d.SaveTo(f); + } + +} +#endif diff --git a/tests/Avalonia.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 06653ff33c..0d9f526a4b 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -151,20 +151,25 @@ namespace Avalonia.Direct2D1.RenderTests var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png"); using (var expected = Image.Load(expectedPath)) - using (var immediate = Image.Load(immediatePath)) - using (var composited = Image.Load(compositedPath)) + using (var immediate = skipImmediate ? null: Image.Load(immediatePath)) + using (var composited = skipCompositor ? null : Image.Load(compositedPath)) { - var immediateError = CompareImages(immediate, expected); - var compositedError = CompareImages(composited, expected); - - if (immediateError > 0.022 && !skipImmediate) + if (!skipImmediate) { - Assert.True(false, immediatePath + ": Error = " + immediateError); + var immediateError = CompareImages(immediate!, expected); + if (immediateError > 0.022) + { + Assert.True(false, immediatePath + ": Error = " + immediateError); + } } - if (compositedError > 0.022 && !skipCompositor) + if (!skipCompositor) { - Assert.True(false, compositedPath + ": Error = " + compositedError); + var compositedError = CompareImages(composited!, expected); + if (compositedError > 0.022) + { + Assert.True(false, compositedPath + ": Error = " + compositedError); + } } } } diff --git a/tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_initial.expected.png b/tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_initial.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..6a90ca262101fc6d0b0bd9b09e05c10c70157f7a GIT binary patch literal 633 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|S$Rr;B4q z#hkZSH}W2K;9)s9{Xtylsxyl-2MKJ_3Yny9~~7qSehJYNXQsxm-RPn|3q_(XzUNV8DGQr>k4=G!_Bou Qz_i5R>FVdQ&MBb@0B)L(3;+NC literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_updated.expected.png b/tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_updated.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a83a8547e8ee20030c5e8c1125d054b43461e4 GIT binary patch literal 639 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|S$pr;B4q z#hkZS4FwN72(TQSe)_{1=NU&1|GCE!v-pEZiSmtsxyl-2MKJ_3Yny9~~7qSehJYNXQsxm-RPn|3q_(XzUNV8DGQr>k4=G!_Bou Qz_i5R>FVdQ&MBb@0B)L(3;+NC literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png b/tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..96fbb171c9691c1316422468c9e32b3c15a70ef7 GIT binary patch literal 358 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|TS1r;B4q z#hka79eEEMFt9kP|M5~@;H1nT%NG4LHGGz1fmP;lpf-Hq$Jvcl2h2Z96@P6jx>H?W zVO#yby#Cw2_c68%WoiN(OpOY(A{g;Gi&D7;`%g?)mDvJV|K_`c%<^>gb6Mw<&;$Ur CT|5~8 literal 0 HcmV?d00001