Răsfoiți Sursa

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
Nikita Tsukanov 1 an în urmă
părinte
comite
086fe5267f
25 a modificat fișierele cu 547 adăugiri și 130 ștergeri
  1. 6 2
      samples/ControlCatalog.NetCore/Program.cs
  2. 27 0
      src/Avalonia.Base/Platform/IRenderTarget.cs
  3. 30 0
      src/Avalonia.Base/Platform/RenderTargetProperties.cs
  4. 54 0
      src/Avalonia.Base/Platform/RetainedFramebuffer.cs
  5. 3 2
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  6. 59 27
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  7. 3 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  8. 2 0
      src/Avalonia.Controls/Avalonia.Controls.csproj
  9. 42 0
      src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs
  10. 0 64
      src/Avalonia.X11/X11Framebuffer.cs
  11. 52 7
      src/Avalonia.X11/X11FramebufferSurface.cs
  12. 8 0
      src/Avalonia.X11/X11Platform.cs
  13. 1 1
      src/Avalonia.X11/X11Window.cs
  14. 4 0
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  15. 12 6
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs
  16. 28 0
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs
  17. 38 6
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
  18. 28 5
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  19. 1 0
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs
  20. 135 0
      tests/Avalonia.RenderTests/Composition/DirectFbCompositionTests.cs
  21. 14 9
      tests/Avalonia.RenderTests/TestBase.cs
  22. BIN
      tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_initial.expected.png
  23. BIN
      tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_updated.expected.png
  24. BIN
      tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_initial.expected.png
  25. BIN
      tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png

+ 6 - 2
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"))
             {

+ 27 - 0
src/Avalonia.Base/Platform/IRenderTarget.cs

@@ -24,4 +24,31 @@ namespace Avalonia.Platform
         /// </summary>
         public bool IsCorrupted { get; }
     }
+
+    [PrivateApi]
+    public interface IRenderTargetWithProperties : IRenderTarget
+    {
+        RenderTargetProperties Properties { get; }
+
+        /// <summary>
+        /// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
+        /// </summary>
+        /// <param name="useScaledDrawing">Apply DPI reported by the render target as a hidden transform matrix</param>
+        /// <param name="properties">Returns various properties about the returned drawing context</param>
+        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);
+        }
+    }
 }

+ 30 - 0
src/Avalonia.Base/Platform/RenderTargetProperties.cs

@@ -0,0 +1,30 @@
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform;
+
+[PrivateApi]
+public struct RenderTargetProperties
+{
+    /// <summary>
+    /// 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
+    /// </summary>
+    public bool RetainsPreviousFrameContents { get; init; }
+    
+    /// <summary>
+    /// 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 
+    /// </summary>
+    public bool IsSuitableForDirectRendering { get; init; }
+}
+
+[PrivateApi]
+public struct RenderTargetDrawingContextProperties
+{
+    /// <summary>
+    /// Indicates that the drawing context targets a surface that preserved its contents since the previous frame
+    /// </summary>
+    public bool PreviousFrameIsRetained { get; init; }
+}

+ 54 - 0
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<RetainedFramebuffer> 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;
+    }
+}

+ 3 - 2
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<CompositionOptions>() ?? 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));

+ 59 - 27
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<ServerCompositionVisual> _attachedVisuals = new();
         private readonly Queue<ServerCompositionVisual> _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) 

+ 3 - 1
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<CompositionOptions>() ?? new();
+        public CompositionOptions Options { get; }
 
         public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
+            CompositionOptions options,
             BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)
         {
+            Options = options;
             _renderLoop = renderLoop;
             RenderInterface = new PlatformRenderInterfaceContextManager(platformGraphics);
             RenderInterface.ContextDisposed += RT_OnContextDisposed;

+ 2 - 0
src/Avalonia.Controls/Avalonia.Controls.csproj

@@ -13,6 +13,7 @@
   <ItemGroup Label="InternalsVisibleTo">
     <InternalsVisibleTo Include="Avalonia.Controls.ItemsRepeater, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Base.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Markup.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
@@ -23,6 +24,7 @@
     <InternalsVisibleTo Include="Avalonia.Native, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.LinuxFramebuffer, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.DesignerSupport.Remote, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Browser, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />

+ 42 - 0
src/Avalonia.Controls/Platform/Surfaces/IFramebufferPlatformSurface.cs

@@ -21,6 +21,23 @@ namespace Avalonia.Controls.Platform.Surfaces
         /// </remarks>
         ILockedFramebuffer Lock();
     }
+    
+    [PrivateApi]
+    public interface IFramebufferRenderTargetWithProperties : IFramebufferRenderTarget
+    {
+        /// <summary>
+        /// Provides a framebuffer descriptor for drawing.
+        /// </summary>
+        /// <remarks>
+        /// Contents should be drawn on actual window after disposing
+        /// </remarks>
+        ILockedFramebuffer Lock(out FramebufferLockProperties properties);
+        
+        bool RetainsFrameContents { get; }
+    }
+    
+    [PrivateApi]
+    public record struct FramebufferLockProperties(bool PreviousFrameIsRetained);
 
     /// <summary>
     /// 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;
+    }
 }

+ 0 - 64
src/Avalonia.X11/X11Framebuffer.cs

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

+ 52 - 7
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<double> _scaling;
+        private readonly bool _retain;
+        private RetainedFramebuffer? _fb;
 
-        public X11FramebufferSurface(IntPtr display, IntPtr xid, int depth, Func<double> 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);
     }
 }

+ 8 - 0
src/Avalonia.X11/X11Platform.cs

@@ -344,6 +344,14 @@ namespace Avalonia
         /// </remarks>
         public bool? EnableMultiTouch { get; set; } = true;
 
+        /// <summary>
+        /// 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 
+        /// </summary>
+        public bool? UseRetainedFramebuffer { get; set; }
+
         public X11PlatformOptions()
         {
             try

+ 1 - 1
src/Avalonia.X11/X11Window.cs

@@ -192,7 +192,7 @@ namespace Avalonia.X11
             var surfaces = new List<object>
             {
                 new X11FramebufferSurface(_x11.DeferredDisplay, _renderHandle, 
-                   depth, () => RenderScaling)
+                   depth, _platform.Options.UseRetainedFramebuffer ?? false)
             };
             
             if (egl != null)

+ 4 - 0
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)

+ 12 - 6
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

+ 28 - 0
src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs

@@ -0,0 +1,28 @@
+using Avalonia.Platform;
+
+namespace Avalonia.LinuxFramebuffer.Output;
+
+public class FbDevOutputOptions
+{
+    /// <summary>
+    /// The frame buffer device name.
+    /// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set
+    /// </summary>
+    public string? FileName { get; set; }
+    /// <summary>
+    /// 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
+    /// </summary>
+    public PixelFormat? PixelFormat { get; set; }
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public bool RenderDirectlyToMappedMemory { get; set; }
+
+    /// <summary>
+    /// The initial scale factor to use
+    /// </summary>
+    public double Scaling { get; set; } = 1;
+}

+ 38 - 6
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; }
 
         /// <summary>
@@ -34,16 +36,30 @@ namespace Avalonia.LinuxFramebuffer
         /// <param name="format">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</param>
-        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
+        })
+        {
+            
+        }
+
+        /// <summary>
+        /// Create a Linux frame buffer device output
+        /// </summary>
+        /// <param name="options">Options</param>
+        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()

+ 28 - 5
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Skia
     /// <summary>
     /// Skia render target that renders to a framebuffer surface. No gpu acceleration available.
     /// </summary>
-    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;
 
         /// <summary>
         /// 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;
         }
 
         /// <inheritdoc />
@@ -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
+        };
+
+
+        /// <inheritdoc />
+        public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi) =>
+            CreateDrawingContext(scaleDrawingToDpi, out _);
+
         /// <inheritdoc />
-        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);
         }
 

+ 1 - 0
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
 {

+ 135 - 0
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<IFramebufferRenderTarget> _cb;
+
+        public FuncFramebufferSurface(Func<IFramebufferRenderTarget> 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

+ 14 - 9
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<Rgba32>(expectedPath))
-            using (var immediate = Image.Load<Rgba32>(immediatePath))
-            using (var composited = Image.Load<Rgba32>(compositedPath))
+            using (var immediate = skipImmediate ? null: Image.Load<Rgba32>(immediatePath))
+            using (var composited = skipCompositor ? null : Image.Load<Rgba32>(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);
+                    }
                 }
             }
         }

BIN
tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_initial.expected.png


BIN
tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-False_updated.expected.png


BIN
tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_initial.expected.png


BIN
tests/TestFiles/Skia/Composition/DirectFb/Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised_advertized-True_updated.expected.png