Browse Source

Reworked the way we are clipping dirty rects with CompositionTarget (#14806)

* Reworked dirty rect tracking to support regions

* Fixed FPS counter
Nikita Tsukanov 1 year ago
parent
commit
4dbb165a7b
52 changed files with 719 additions and 184 deletions
  1. 5 0
      samples/ControlCatalog.NetCore/Program.cs
  2. 4 0
      samples/ControlCatalog/Pages/CompositionPage.axaml
  3. 51 13
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  4. 1 1
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  5. 11 0
      src/Avalonia.Base/PixelSize.cs
  6. 31 2
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  7. 3 0
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  8. 17 0
      src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs
  9. 2 1
      src/Avalonia.Base/Platform/IRenderTarget.cs
  10. 2 1
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  11. 20 12
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs
  12. 46 1
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs
  13. 16 0
      src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs
  14. 1 0
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  15. 1 1
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  16. 103 0
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs
  17. 10 1
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  18. 7 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  19. 4 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  20. 3 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs
  21. 2 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs
  22. 2 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs
  23. 35 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs
  24. 58 63
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  25. 13 8
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  26. 1 0
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  27. 12 7
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  28. 1 1
      src/Avalonia.Base/composition-schema.xml
  29. 3 2
      src/Avalonia.Controls/BorderVisual.cs
  30. 1 1
      src/Avalonia.X11/X11CursorFactory.cs
  31. 1 1
      src/Avalonia.X11/X11IconLoader.cs
  32. 23 3
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  33. 76 19
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  34. 3 2
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  35. 2 1
      src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs
  36. 1 0
      src/Skia/Avalonia.Skia/PictureRenderTarget.cs
  37. 3 0
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  38. 2 1
      src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs
  39. 53 0
      src/Skia/Avalonia.Skia/SkiaRegionImpl.cs
  40. 10 0
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  41. 2 1
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  42. 3 0
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  43. 2 2
      src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs
  44. 4 4
      src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs
  45. 51 7
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  46. 1 1
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  47. 4 3
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
  48. 4 4
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs
  49. 2 2
      src/Windows/Avalonia.Direct2D1/RenderTarget.cs
  50. 2 2
      src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs
  51. 1 1
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  52. 3 3
      tests/Avalonia.UnitTests/TestRoot.cs

+ 5 - 0
samples/ControlCatalog.NetCore/Program.cs

@@ -10,6 +10,7 @@ using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Fonts.Inter;
 using Avalonia.Headless;
 using Avalonia.LogicalTree;
+using Avalonia.Rendering.Composition;
 using Avalonia.Threading;
 
 using ControlCatalog.Pages;
@@ -130,6 +131,10 @@ namespace ControlCatalog.NetCore
                     UseDBusMenu = true,
                     EnableIme = true
                 })
+                .With(new CompositionOptions()
+                {
+                    UseRegionDirtyRectClipping = true
+                })
                 .UseSkia()
                 .WithInterFont()
                 .AfterSetup(builder =>

+ 4 - 0
samples/ControlCatalog/Pages/CompositionPage.axaml

@@ -50,6 +50,10 @@
           <Button Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button>
           <Button Margin="10" Click="ButtonStartCustomVisual">Start</Button>
           <Button Margin="10" Click="ButtonStopCustomVisual">Stop</Button>
+          <CheckBox Margin="10" 
+                    x:Name="PreciseDirtyRectsCheckboxCustomVisual"
+                    IsCheckedChanged="PreciseDirtyRectsCheckboxCustomVisualChanged"
+                    >Precise dirty rects</CheckBox>
         </StackPanel>
         <Control x:Name="CustomVisualHost" />
       </DockPanel>

+ 51 - 13
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@@ -24,7 +24,7 @@ public partial class CompositionPage : UserControl
 
     public CompositionPage()
     {
-        AvaloniaXamlLoader.Load(this);
+        InitializeComponent();
         AttachAnimatedSolidVisual(this.FindControl<Control>("SolidVisualHost")!);
         AttachCustomVisual(this.FindControl<Control>("CustomVisualHost")!);
     }
@@ -206,6 +206,7 @@ public partial class CompositionPage : UserControl
             _customVisual = compositor.CreateCustomVisual(new CustomVisualHandler());
             ElementComposition.SetElementChildVisual(v, _customVisual);
             _customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage);
+            PreciseDirtyRectsCheckboxCustomVisualChanged(this, new());
             Update();
         };
         
@@ -221,10 +222,16 @@ public partial class CompositionPage : UserControl
         private TimeSpan _animationElapsed;
         private TimeSpan? _lastServerTime;
         private bool _running;
+        private bool _preciseDirtyRects;
 
-        public static readonly object StopMessage = new(), StartMessage = new();
-        
-        public override void OnRender(ImmediateDrawingContext drawingContext)
+        public static readonly object StopMessage = new(),
+            StartMessage = new(),
+            UsePreciseDirtyRects = new(),
+            UseNonPreciseDirtyRects = new();
+
+        private List<(Point center, double size, ImmutableSolidColorBrush brush)> _ellipses = new();
+
+        void UpdateRects()
         {
             if (_running)
             {
@@ -232,6 +239,8 @@ public partial class CompositionPage : UserControl
                 _lastServerTime = CompositionNow;
             }
             
+            _ellipses.Clear();
+            
             const int cnt = 20;
             var maxPointSizeX = EffectiveSize.X / (cnt * 1.6);
             var maxPointSizeY = EffectiveSize.Y / 4;
@@ -250,16 +259,22 @@ public partial class CompositionPage : UserControl
                 var posY = (EffectiveSize.Y - pointSize) * (1 + Math.Sin(stage * 3.14 * 3 + sinOffset)) / 2 + pointSize / 2;
                 var opacity = Math.Sin(stage * 3.14);
 
-
-                drawingContext.DrawEllipse(new ImmutableSolidColorBrush(Color.FromArgb(
-                        255, 
-                        (byte)(255 - 255 * colorStage),
-                        (byte)(255 * Math.Abs(0.5 - colorStage) * 2), 
-                        (byte)(255 * colorStage)
-                    ), opacity), null,
-                    new Point(posX, posY), pointSize / 2, pointSize / 2);
+                _ellipses.Add((new Point(posX, posY), pointSize / 2, new ImmutableSolidColorBrush(Color.FromArgb(
+                    255,
+                    (byte)(255 - 255 * colorStage),
+                    (byte)(255 * Math.Abs(0.5 - colorStage) * 2),
+                    (byte)(255 * colorStage)
+                ), opacity)));
             }
+        }
+        
+        public override void OnRender(ImmediateDrawingContext drawingContext)
+        {
+            if (_ellipses.Count == 0)
+                UpdateRects();
             
+            foreach(var e in _ellipses)
+                drawingContext.DrawEllipse(e.brush, null, e.center, e.size, e.size);
         }
 
         public override void OnMessage(object message)
@@ -272,13 +287,29 @@ public partial class CompositionPage : UserControl
             }
             else if (message == StopMessage)
                 _running = false;
+            else if (message == UsePreciseDirtyRects)
+                _preciseDirtyRects = true;
+            else if (message == UseNonPreciseDirtyRects)
+                _preciseDirtyRects = false;
         }
 
+        void InvalidateCurrentEllipseRects()
+        {
+            foreach (var e in _ellipses)
+                Invalidate(new Rect(e.center.X - e.size, e.center.Y - e.size, e.size * 2, e.size * 2));
+        }
+        
         public override void OnAnimationFrameUpdate()
         {
             if (_running)
             {
-                Invalidate();
+                if (_preciseDirtyRects)
+                    InvalidateCurrentEllipseRects();
+                else
+                    Invalidate();
+                UpdateRects();
+                if(_preciseDirtyRects)
+                    InvalidateCurrentEllipseRects();
                 RegisterForNextAnimationFrameUpdate();
             }
         }
@@ -298,6 +329,13 @@ public partial class CompositionPage : UserControl
     {
         _customVisual?.SendHandlerMessage(CustomVisualHandler.StopMessage);
     }
+
+    private void PreciseDirtyRectsCheckboxCustomVisualChanged(object sender, RoutedEventArgs e)
+    {
+        _customVisual?.SendHandlerMessage(PreciseDirtyRectsCheckboxCustomVisual?.IsChecked == true
+            ? CustomVisualHandler.UsePreciseDirtyRects
+            : CustomVisualHandler.UseNonPreciseDirtyRects);
+    }
 }
 
 public class CompositionPageColorItem

+ 1 - 1
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging
         /// <returns>The drawing context.</returns>
         public DrawingContext CreateDrawingContext(bool clear)
         {
-            var platform = PlatformImpl.Item.CreateDrawingContext();
+            var platform = PlatformImpl.Item.CreateDrawingContext(true);
             if(clear)
                 platform.Clear(Colors.Transparent);
             return new PlatformDrawingContext(platform);

+ 11 - 0
src/Avalonia.Base/PixelSize.cs

@@ -166,6 +166,17 @@ namespace Avalonia
         public static PixelSize FromSize(Size size, double scale) => new PixelSize(
             (int)Math.Ceiling(size.Width * scale),
             (int)Math.Ceiling(size.Height * scale));
+        
+        /// <summary>
+        /// A reversible variant of <see cref="FromSize(Size, double)"/> that uses Round instead of Ceiling to make it reversible from ToSize
+        /// </summary>
+        /// <param name="size">The size.</param>
+        /// <param name="scale">The scaling factor.</param>
+        /// <returns>The device-independent size.</returns>
+        internal static PixelSize FromSizeRounded(Size size, double scale) => new PixelSize(
+            (int)Math.Round(size.Width * scale),
+            (int)Math.Round(size.Height * scale));
+
 
         /// <summary>
         /// Converts a <see cref="Size"/> to device pixels using the specified scaling factor.

+ 31 - 2
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@@ -3,6 +3,7 @@ using Avalonia.Media;
 using Avalonia.Utilities;
 using Avalonia.Metadata;
 using Avalonia.Media.Imaging;
+using Avalonia.Media.Immutable;
 
 namespace Avalonia.Platform
 {
@@ -75,6 +76,18 @@ namespace Avalonia.Platform
         /// </remarks>
         void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
             BoxShadows boxShadows = default);
+        
+        /// <summary>
+        /// Draws the specified region with the specified Brush and Pen.
+        /// </summary>
+        /// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
+        /// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
+        /// <param name="region">The region to draw.</param>
+        /// <remarks>
+        /// The brush and the pen can both be null. If the brush is null, then no fill is performed.
+        /// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
+        /// </remarks>
+        void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region);
 
         /// <summary>
         /// Draws an ellipse with the specified Brush and Pen.
@@ -108,7 +121,7 @@ namespace Avalonia.Platform
         /// has to do a format conversion each time a standard render target bitmap is rendered,
         /// but a layer created via this method has no such overhead.
         /// </remarks>
-        IDrawingContextLayerImpl CreateLayer(Size size);
+        IDrawingContextLayerImpl CreateLayer(PixelSize size);
 
         /// <summary>
         /// Pushes a clip rectangle.
@@ -121,12 +134,28 @@ namespace Avalonia.Platform
         /// </summary>
         /// <param name="clip">The clip rounded rectangle</param>
         void PushClip(RoundedRect clip);
-
+        
+        /// <summary>
+        /// Pushes a clip region.
+        /// </summary>
+        /// <param name="region">The clip region</param>
+        void PushClip(IPlatformRenderInterfaceRegion region);
+        
         /// <summary>
         /// Pops the latest pushed clip rectangle.
         /// </summary>
         void PopClip();
 
+        /// <summary>
+        /// Enforces rendering to happen on an intermediate surface
+        /// </summary>
+        void PushLayer(Rect bounds);
+
+        /// <summary>
+        /// Pops the latest pushed intermediate surface layer.
+        /// </summary>
+        void PopLayer();
+
         /// <summary>
         /// Pushes an opacity value.
         /// </summary>

+ 3 - 0
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@@ -199,6 +199,9 @@ namespace Avalonia.Platform
         public PixelFormat DefaultPixelFormat { get; }
 
         bool IsSupportedBitmapPixelFormat(PixelFormat format);
+        
+        bool SupportsRegions { get; }
+        IPlatformRenderInterfaceRegion CreateRegion();
     }
 
     [Unstable, PrivateApi]

+ 17 - 0
src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform;
+
+[Unstable, PrivateApi]
+public interface IPlatformRenderInterfaceRegion : IDisposable
+{
+    void AddRect(PixelRect rect);
+    void Reset();
+    bool IsEmpty { get; }
+    PixelRect Bounds { get; }
+    IList<PixelRect> Rects { get; }
+    bool Intersects(Rect rect);
+    bool Contains(Point pt);
+}

+ 2 - 1
src/Avalonia.Base/Platform/IRenderTarget.cs

@@ -16,7 +16,8 @@ namespace Avalonia.Platform
         /// <summary>
         /// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
         /// </summary>
-        IDrawingContextImpl CreateDrawingContext();
+        /// <param name="useScaledDrawing">Apply DPI reported by the render target as a hidden transform matrix</param>
+        IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing);
         
         /// <summary>
         /// Indicates if the render target is no longer usable and needs to be recreated

+ 2 - 1
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@@ -172,7 +172,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
                 v.SynchronizeCompositionChildVisuals();
         _dirty.Clear();
         _recalculateChildren.Clear();
-        CompositionTarget.Size = _root.ClientSize;
+        
+        CompositionTarget.PixelSize = PixelSize.FromSizeRounded(_root.ClientSize, _root.RenderScaling);
         CompositionTarget.Scaling = _root.RenderScaling;
         
         var commit = _compositor.RequestCommitAsync();

+ 20 - 12
src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs

@@ -1,12 +1,15 @@
 using System.Collections.Generic;
 using System.Numerics;
+using Avalonia.Media;
 using Avalonia.Rendering.Composition.Server;
 using Avalonia.Rendering.Composition.Transport;
+using Avalonia.Threading;
 
 namespace Avalonia.Rendering.Composition;
 
 public sealed class CompositionCustomVisual : CompositionContainerVisual
 {
+    private static readonly ThreadSafeObjectPool<List<object>> s_messageListPool = new(); 
     private List<object>? _messages;
 
     internal CompositionCustomVisual(Compositor compositor, CompositionCustomVisualHandler handler)
@@ -17,21 +20,26 @@ public sealed class CompositionCustomVisual : CompositionContainerVisual
 
     public void SendHandlerMessage(object message)
     {
-        (_messages ??= new()).Add(message);
-        RegisterForSerialization();
+        if (_messages == null)
+        {
+            _messages = s_messageListPool.Get();
+            Compositor.RequestCompositionUpdate(OnCompositionUpdate);
+        }
+        _messages.Add(message);
     }
 
-    private protected override void SerializeChangesCore(BatchStreamWriter writer)
+    private void OnCompositionUpdate()
     {
-        base.SerializeChangesCore(writer);
-        if (_messages == null || _messages.Count == 0)
-            writer.Write(0);
-        else
+        if(_messages == null)
+            return;
+        
+        var messages = _messages;
+        _messages = null;
+        Compositor.PostServerJob(()=>
         {
-            writer.Write(_messages.Count);
-            foreach (var m in _messages)
-                writer.WriteObject(m);
-            _messages.Clear();
-        }
+            ((ServerCompositionCustomVisual)Server).DispatchMessages(messages);
+            messages.Clear();
+            s_messageListPool.ReturnAndSetNull(ref messages);
+        });
     }
 }

+ 46 - 1
src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Collections.Generic;
 using System.Numerics;
+using Avalonia.Collections.Pooled;
 using Avalonia.Media;
 using Avalonia.Rendering.Composition.Server;
 
@@ -8,6 +10,8 @@ namespace Avalonia.Rendering.Composition;
 public abstract class CompositionCustomVisualHandler
 {
     private ServerCompositionCustomVisual? _host;
+    private bool _inRender;
+    private Rect _currentTransformedClip;
 
     public virtual void OnMessage(object message)
     {
@@ -18,7 +22,21 @@ public abstract class CompositionCustomVisualHandler
     {
         
     }
-    
+
+    internal void Render(ImmediateDrawingContext drawingContext, Rect currentTransformedClip)
+    {
+        _inRender = true;
+        _currentTransformedClip = currentTransformedClip;
+        try
+        {
+            OnRender(drawingContext);
+        }
+        finally
+        {
+            _inRender = false;
+        }
+    }
+
     public abstract void OnRender(ImmediateDrawingContext drawingContext);
 
     void VerifyAccess()
@@ -28,6 +46,13 @@ public abstract class CompositionCustomVisualHandler
         _host.Compositor.VerifyAccess();
     }
 
+    void VerifyInRender()
+    {
+        VerifyAccess();
+        if (!_inRender)
+            throw new InvalidOperationException("This API is only available from OnRender");
+    }
+
     protected Vector EffectiveSize
     {
         get
@@ -57,9 +82,29 @@ public abstract class CompositionCustomVisualHandler
         _host!.HandlerInvalidate();
     }
 
+    protected void Invalidate(Rect rc)
+    {
+        VerifyAccess();
+        _host!.HandlerInvalidate(rc);
+    }
+
     protected void RegisterForNextAnimationFrameUpdate()
     {
         VerifyAccess();
         _host!.HandlerRegisterForNextAnimationFrameUpdate();
     }
+
+    protected bool RenderClipContains(Point pt)
+    {
+        VerifyInRender();
+        pt *= _host!.GlobalTransformMatrix;
+        return _currentTransformedClip.Contains(pt) && _host.Root!.DirtyRects.Contains(pt);
+    }
+
+    protected bool RenderClipIntersectes(Rect rc)
+    {
+        VerifyInRender();
+        rc = rc.TransformToAABB(_host!.GlobalTransformMatrix);
+        return _currentTransformedClip.Intersects(rc) && _host.Root!.DirtyRects.Intersects(rc);
+    }
 }

+ 16 - 0
src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs

@@ -0,0 +1,16 @@
+namespace Avalonia.Rendering.Composition;
+
+public class CompositionOptions
+{
+    /// <summary>
+    /// Enables more accurate tracking of dirty rects by utilizing regions if supported by the underlying
+    /// drawing context
+    /// </summary>
+    public bool? UseRegionDirtyRectClipping { get; set; }
+    /// <summary>
+    /// Enforces dirty contents to be rendered into an extra intermediate surface before being applied onto the
+    /// saved frame.
+    /// Required as a workaround for Skia bug https://issues.skia.org/issues/327877721
+    /// </summary>
+    public bool? UseSaveLayerRootClip { get; set; }
+}

+ 1 - 0
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@@ -29,6 +29,7 @@ namespace Avalonia.Rendering.Composition
         /// <returns></returns>
         public PooledList<CompositionVisual>? TryHitTest(Point point, CompositionVisual? root, Func<CompositionVisual, bool>? filter)
         {
+            point *= Scaling;
             Server.Readback.NextRead();
             root ??= Root;
             if (root == null)

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Rendering.Composition.Server
             {
                 var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' ';
                 var run = _runs[effectiveChar - FirstChar];
-                context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0);
+                context.Transform = Matrix.CreateTranslation(offset, 0.0) * originalTransform;
                 context.DrawGlyphRun(foreground, run.PlatformImpl.Item);
                 offset += run.Bounds.Width;
             }

+ 103 - 0
src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs

@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+using Avalonia.Platform;
+using Avalonia.Reactive;
+
+namespace Avalonia.Rendering.Composition.Server;
+
+internal interface IDirtyRectTracker
+{
+    void AddRect(PixelRect rect);
+    IDisposable BeginDraw(IDrawingContextImpl ctx);
+    bool IsEmpty { get; }
+    bool Intersects(Rect rect);
+    bool Contains(Point pt);
+    void Reset();
+    void Visualize(IDrawingContextImpl context);
+    PixelRect CombinedRect { get; }
+    IList<PixelRect> Rects { get; }
+}
+
+internal class DirtyRectTracker : IDirtyRectTracker
+{
+    private PixelRect _rect;
+    private Rect _doubleRect;
+    private PixelRect[] _rectsForApi = new PixelRect[1];
+    private Random _random = new();
+    public void AddRect(PixelRect rect)
+    {
+        _rect = _rect.Union(rect);
+    }
+    
+    public IDisposable BeginDraw(IDrawingContextImpl ctx)
+    {
+        ctx.PushClip(_rect.ToRect(1));
+        _doubleRect = _rect.ToRect(1);
+        return Disposable.Create(ctx.PopClip);
+    }
+
+    public bool IsEmpty => _rect.Width == 0 | _rect.Height == 0;
+    public bool Intersects(Rect rect) => _doubleRect.Intersects(rect);
+    public bool Contains(Point pt) => _rect.Contains(PixelPoint.FromPoint(pt, 1));
+
+    public void Reset() => _rect = default;
+    public void Visualize(IDrawingContextImpl context)
+    {
+        context.DrawRectangle(
+            new ImmutableSolidColorBrush(
+                new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
+            null, _doubleRect);
+    }
+
+    public PixelRect CombinedRect => _rect;
+
+    public IList<PixelRect> Rects
+    {
+        get
+        {
+            if (_rect.Width == 0 || _rect.Height == 0)
+                return Array.Empty<PixelRect>();
+            _rectsForApi[0] = _rect;
+            return _rectsForApi;
+        }
+    }
+}
+
+internal class RegionDirtyRectTracker : IDirtyRectTracker
+{
+    private readonly IPlatformRenderInterfaceRegion _region;
+    private Random _random = new();
+
+    public RegionDirtyRectTracker(IPlatformRenderInterface platformRender)
+    {
+        _region = platformRender.CreateRegion();
+    }
+
+    public void AddRect(PixelRect rect) => _region.AddRect(rect);
+
+    public IDisposable BeginDraw(IDrawingContextImpl ctx)
+    {
+        ctx.PushClip(_region);
+        return Disposable.Create(ctx.PopClip);
+    }
+
+    public bool IsEmpty => _region.IsEmpty;
+    public bool Intersects(Rect rect) => _region.Intersects(rect);
+    public bool Contains(Point pt) => _region.Contains(pt);
+
+    public void Reset() => _region.Reset();
+
+    public void Visualize(IDrawingContextImpl context)
+    {
+        context.DrawRegion(
+            new ImmutableSolidColorBrush(
+                new Color(150, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
+            null, _region);
+    }
+
+    public PixelRect CombinedRect => _region.Bounds;
+    public IList<PixelRect> Rects => _region.Rects;
+}

+ 10 - 1
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@@ -77,6 +77,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
         _impl.DrawRectangle(brush, pen, rect, boxShadows);
     }
 
+    public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region) =>
+        _impl.DrawRegion(brush, pen, region);
+
     public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
     {
         _impl.DrawEllipse(brush, pen, rect);
@@ -87,7 +90,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
         _impl.DrawGlyphRun(foreground, glyphRun);
     }
 
-    public IDrawingContextLayerImpl CreateLayer(Size size)
+    public IDrawingContextLayerImpl CreateLayer(PixelSize size)
     {
         return _impl.CreateLayer(size);
     }
@@ -102,11 +105,17 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
         _impl.PushClip(clip);
     }
 
+    public void PushClip(IPlatformRenderInterfaceRegion region) => _impl.PushClip(region);
+
     public void PopClip()
     {
         _impl.PopClip();
     }
 
+    public void PushLayer(Rect bounds) => _impl.PushLayer(bounds);
+
+    public void PopLayer() => _impl.PopLayer();
+
     public void PushOpacity(double opacity, Rect? bounds)
     {
         _impl.PushOpacity(opacity, bounds);

+ 7 - 6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@@ -16,26 +16,27 @@ namespace Avalonia.Rendering.Composition.Server
         private Rect? _transformedContentBounds;
         private IImmutableEffect? _oldEffect;
         
-        protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+        protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+            IDirtyRectTracker dirtyRects)
         {
-            base.RenderCore(canvas, currentTransformedClip);
+            base.RenderCore(canvas, currentTransformedClip, dirtyRects);
 
             foreach (var ch in Children)
             {
-                ch.Render(canvas, currentTransformedClip);
+                ch.Render(canvas, currentTransformedClip, dirtyRects);
             }
         }
 
-        public override UpdateResult Update(ServerCompositionTarget root)
+        public override UpdateResult Update(ServerCompositionTarget root, Matrix parentCombinedTransform)
         {
-            var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root);
+            var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root, parentCombinedTransform);
             foreach (var child in Children)
             {
                 if (child.AdornedVisual != null)
                     root.EnqueueAdornerUpdate(child);
                 else
                 {
-                    var res = child.Update(root);
+                    var res = child.Update(root, GlobalTransformMatrix);
                     oldInvalidated |= res.InvalidatedOld;
                     newInvalidated |= res.InvalidatedNew;
                     combinedBounds = Rect.Union(combinedBounds, res.Bounds);

+ 4 - 2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@@ -40,13 +40,15 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
         base.DeserializeChangesCore(reader, committedAt);
     }
 
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+        IDirtyRectTracker dirtyRects)
     {
         if (_renderCommands != null)
         {
             _renderCommands.Render(canvas);
         }
-        base.RenderCore(canvas, currentTransformedClip);
+
+        base.RenderCore(canvas, currentTransformedClip, dirtyRects);
     }
     
     public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated();

+ 3 - 2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs

@@ -5,7 +5,8 @@ namespace Avalonia.Rendering.Composition.Server;
 
 internal partial class ServerCompositionExperimentalAcrylicVisual
 {
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+        IDirtyRectTracker dirtyRects)
     {
         var cornerRadius = CornerRadius;
         canvas.DrawRectangle(
@@ -15,7 +16,7 @@ internal partial class ServerCompositionExperimentalAcrylicVisual
                 cornerRadius.TopLeft, cornerRadius.TopRight,
                 cornerRadius.BottomRight, cornerRadius.BottomLeft));
 
-        base.RenderCore(canvas, currentTransformedClip);
+        base.RenderCore(canvas, currentTransformedClip, dirtyRects);
     }
 
     public override Rect OwnContentBounds => new(0, 0, Size.X, Size.Y);

+ 2 - 1
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs

@@ -4,7 +4,8 @@ namespace Avalonia.Rendering.Composition.Server;
 
 internal partial class ServerCompositionSolidColorVisual
 {
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+        IDirtyRectTracker dirtyRects)
     {
         canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y));
     }

+ 2 - 1
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs

@@ -4,7 +4,8 @@ namespace Avalonia.Rendering.Composition.Server;
 
 internal partial class ServerCompositionSurfaceVisual
 {
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+        IDirtyRectTracker dirtyRects)
     {
         if (Surface == null)
             return;

+ 35 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Collections.Pooled;
+
+namespace Avalonia.Rendering.Composition.Server;
+
+internal partial class ServerCompositionTarget
+{
+    public readonly IDirtyRectTracker DirtyRects;
+    
+    public void AddDirtyRect(Rect rect)
+    {
+        if (rect.Width == 0 && rect.Height == 0)
+            return;
+        var snapped = PixelRect.FromRect(SnapToDevicePixels(rect, Scaling), 1);
+        DebugEvents?.RectInvalidated(rect);
+        DirtyRects.AddRect(snapped);
+        _redrawRequested = true;
+    }
+    
+    public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
+        
+    private static Rect SnapToDevicePixels(Rect rect, double scale)
+    {
+        return new Rect(
+            new Point(
+                Math.Floor(rect.X * scale) / scale,
+                Math.Floor(rect.Y * scale) / scale),
+            new Point(
+                Math.Ceiling(rect.Right * scale) / scale,
+                Math.Ceiling(rect.Bottom * scale) / scale));
+    }
+    
+    
+}

+ 58 - 63
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Threading;
+using Avalonia.Collections.Pooled;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
 using Avalonia.Media.Immutable;
@@ -25,10 +26,9 @@ namespace Avalonia.Rendering.Composition.Server
         private FpsCounter? _fpsCounter;
         private FrameTimeGraph? _renderTimeGraph;
         private FrameTimeGraph? _layoutTimeGraph;
-        private Rect _dirtyRect;
-        private readonly Random _random = new();
-        private Size _layerSize;
+        private PixelSize _layerSize;
         private IDrawingContextLayerImpl? _layer;
+        private bool _updateRequested;
         private bool _redrawRequested;
         private bool _disposed;
         private readonly HashSet<ServerCompositionVisual> _attachedVisuals = new();
@@ -56,6 +56,11 @@ namespace Avalonia.Rendering.Composition.Server
             _compositor = compositor;
             _surfaces = surfaces;
             _diagnosticTextRenderer = diagnosticTextRenderer;
+            var platformRender = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
+            DirtyRects = compositor.Options.UseRegionDirtyRectClipping == true &&
+                         platformRender?.SupportsRegions == true
+                ? new RegionDirtyRectTracker(platformRender)
+                : new DirtyRectTracker();
             Id = Interlocked.Increment(ref s_nextId);
         }
 
@@ -146,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server
                 return;
             }
 
-            if ((_dirtyRect.Width == 0 && _dirtyRect.Height == 0) && !_redrawRequested)
+            if (DirtyRects.IsEmpty && !_redrawRequested && !_updateRequested)
                 return;
 
             Revision++;
@@ -154,39 +159,51 @@ namespace Avalonia.Rendering.Composition.Server
             var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0;
             var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L;
 
+            var transform = Matrix.CreateScale(Scaling, Scaling);
             // Update happens in a separate phase to extend dirty rect if needed
-            Root.Update(this);
+            Root.Update(this, transform);
 
             while (_adornerUpdateQueue.Count > 0)
             {
                 var adorner = _adornerUpdateQueue.Dequeue();
-                adorner.Update(this);
+                adorner.Update(this, transform);
             }
-            
+
+            _updateRequested = false;
             Readback.CompleteWrite(Revision);
 
+            if (!_redrawRequested)
+                return;
             _redrawRequested = false;
-            using (var targetContext = _renderTarget.CreateDrawingContext())
+            using (var targetContext = _renderTarget.CreateDrawingContext(false))
             {
-                var size = Size;
-                var layerSize = size * Scaling;
-                if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
+                if (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted)
                 {
                     _layer?.Dispose();
                     _layer = null;
-                    _layer = targetContext.CreateLayer(size);
-                    _layerSize = layerSize;
-                    _dirtyRect = new Rect(0, 0, size.Width, size.Height);
+                    _layer = targetContext.CreateLayer(PixelSize);
+                    _layerSize = PixelSize;
+                    DirtyRects.AddRect(new PixelRect(_layerSize));
                 }
 
-                if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0)
+                if (!DirtyRects.IsEmpty)
                 {
-                    using (var context = _layer.CreateDrawingContext())
+                    var useLayerClip = Compositor.Options.UseSaveLayerRootClip ??
+                                       Compositor.RenderInterface.GpuContext != null;
+                    using (var context = _layer.CreateDrawingContext(false))
                     {
-                        context.PushClip(_dirtyRect);
-                        context.Clear(Colors.Transparent);
-                        Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect);
-                        context.PopClip();
+                        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();
+                        }
                     }
                 }
 
@@ -195,9 +212,10 @@ namespace Avalonia.Rendering.Composition.Server
                 if (_layer.CanBlit)
                     _layer.Blit(targetContext);
                 else
-                    targetContext.DrawBitmap(_layer, 1,
-                        new Rect(_layerSize),
-                        new Rect(size));
+                {
+                    var rect = new PixelRect(default, PixelSize).ToRect(1);
+                    targetContext.DrawBitmap(_layer, 1, rect, rect);
+                }
 
                 if (DebugOverlays != RendererDebugOverlays.None)
                 {
@@ -206,27 +224,24 @@ namespace Avalonia.Rendering.Composition.Server
                         var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp);
                         RenderTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds);
                     }
-
-                    DrawOverlays(targetContext);
+                    
+                    DrawOverlays(targetContext, PixelSize.ToSize(Scaling));
                 }
 
                 RenderedVisuals = 0;
 
-                _dirtyRect = default;
+                DirtyRects.Reset();
             }
         }
 
-        private void DrawOverlays(IDrawingContextImpl targetContext)
+        private void DrawOverlays(IDrawingContextImpl targetContext, Size logicalSize)
         {
-            if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0)
-            {
-                targetContext.DrawRectangle(
-                    new ImmutableSolidColorBrush(
-                        new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
-                    null,
-                    _dirtyRect);
-            }
+            if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0) 
+                DirtyRects.Visualize(targetContext);
 
+            
+            targetContext.Transform = Matrix.CreateScale(Scaling, Scaling);
+            
             if ((DebugOverlays & RendererDebugOverlays.Fps) != 0)
             {
                 var nativeMem = ByteSizeHelper.ToString((ulong) (
@@ -247,9 +262,13 @@ namespace Avalonia.Rendering.Composition.Server
                 if (graph == null)
                     return;
                 top += 8.0;
-                targetContext.Transform = Matrix.CreateTranslation(Size.Width - graph.Size.Width - 8.0, top);
+                var oldTransform = targetContext.Transform;
+
+                targetContext.Transform = Matrix.CreateTranslation(logicalSize.Width - graph.Size.Width - 8.0, top) *
+                                          oldTransform;
                 graph.Render(targetContext);
                 top += graph.Size.Height;
+                targetContext.Transform = oldTransform;
             }
 
             if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0)
@@ -261,35 +280,11 @@ namespace Avalonia.Rendering.Composition.Server
             {
                 DrawTimeGraph(RenderTimeGraph);
             }
-        }
-
-        public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
-        
-        private static Rect SnapToDevicePixels(Rect rect, double scale)
-        {
-            return new Rect(
-                new Point(
-                    Math.Floor(rect.X * scale) / scale,
-                    Math.Floor(rect.Y * scale) / scale),
-                new Point(
-                    Math.Ceiling(rect.Right * scale) / scale,
-                    Math.Ceiling(rect.Bottom * scale) / scale));
+            
+            targetContext.Transform = Matrix.Identity;
         }
         
-        public void AddDirtyRect(Rect rect)
-        {
-            if (rect.Width == 0 && rect.Height == 0)
-                return;
-            var snapped = SnapToDevicePixels(rect, Scaling);
-            DebugEvents?.RectInvalidated(rect);
-            _dirtyRect = _dirtyRect.Union(snapped);
-            _redrawRequested = true;
-        }
-
-        public void Invalidate()
-        {
-            _redrawRequested = true;
-        }
+        public void RequestUpdate() => _updateRequested = true;
 
         public void Dispose()
         {

+ 13 - 8
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@@ -22,20 +22,25 @@ namespace Avalonia.Rendering.Composition.Server
         private Rect? _transformedClipBounds;
         private Rect _combinedTransformedClipBounds;
 
-        protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+        protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+            IDirtyRectTracker dirtyRects)
         {
         }
 
-        public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+        public void Render(CompositorDrawingContextProxy canvas, Rect? parentTransformedClip, IDirtyRectTracker dirtyRects)
         {
             if (Visible == false || IsVisibleInFrame == false)
                 return;
             if (Opacity == 0)
                 return;
 
-            currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds);
+            var currentTransformedClip = parentTransformedClip.HasValue
+                ? parentTransformedClip.Value.Intersect(_combinedTransformedClipBounds)
+                : _combinedTransformedClipBounds;
             if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0)
                 return;
+            if(!dirtyRects.Intersects(currentTransformedClip))
+                return;
 
             Root!.RenderedVisuals++;
             Root!.DebugEvents?.IncrementRenderedVisuals();
@@ -67,7 +72,7 @@ namespace Avalonia.Rendering.Composition.Server
 
             canvas.RenderOptions = RenderOptions;
 
-            RenderCore(canvas, currentTransformedClip);
+            RenderCore(canvas, currentTransformedClip, dirtyRects);
             
             // Hack to force invalidation of SKMatrix
             canvas.PostTransform = transform;
@@ -116,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server
             }
         }
         
-        public virtual UpdateResult Update(ServerCompositionTarget root)
+        public virtual UpdateResult Update(ServerCompositionTarget root, Matrix parentVisualTransform)
         {
             if (Parent == null && Root == null)
                 return default;
@@ -138,7 +143,7 @@ namespace Avalonia.Rendering.Composition.Server
                 _combinedTransformDirty = false;
             }
 
-            var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix.Identity;
+            var parentTransform = AdornedVisual?.GlobalTransformMatrix ?? parentVisualTransform;
 
             var newTransform = CombinedTransformMatrix * parentTransform;
 
@@ -207,7 +212,7 @@ namespace Avalonia.Rendering.Composition.Server
             _combinedTransformedClipBounds =
                 (AdornerIsClipped ? AdornedVisual?._combinedTransformedClipBounds : null)
                 ?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null)
-                ?? new Rect(Root!.Size);
+                ?? new Rect(Root!.PixelSize.ToSize(1));
 
             if (_transformedClipBounds != null)
                 _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
@@ -301,7 +306,7 @@ namespace Avalonia.Rendering.Composition.Server
         protected override void ValuesInvalidated()
         {
             _isDirtyForUpdate = true;
-            Root?.Invalidate();
+            Root?.RequestUpdate();
         }
 
         public bool IsVisibleInFrame { get; set; }

+ 1 - 0
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@@ -38,6 +38,7 @@ 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 ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
             BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)

+ 12 - 7
src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Numerics;
 using Avalonia.Logging;
 using Avalonia.Media;
@@ -16,15 +17,13 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
         _handler.Attach(this);
     }
 
-    protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
+    public void DispatchMessages(List<object> messages)
     {
-        base.DeserializeChangesCore(reader, committedAt);
-        var count = reader.Read<int>();
-        for (var c = 0; c < count; c++)
+        foreach(var message in messages)
         {
             try
             {
-                _handler.OnMessage(reader.ReadObject()!);
+                _handler.OnMessage(message);
             }
             catch (Exception e)
             {
@@ -58,6 +57,11 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
     }
 
     internal void HandlerInvalidate() => ValuesInvalidated();
+
+    internal void HandlerInvalidate(Rect rc)
+    {
+        Root?.AddDirtyRect(rc.TransformToAABB(GlobalTransformMatrix));
+    }
     
     internal void HandlerRegisterForNextAnimationFrameUpdate()
     {
@@ -66,12 +70,13 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
             Compositor.AddToClock(this);
     }
 
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+        IDirtyRectTracker dirtyRects)
     {
         using var context = new ImmediateDrawingContext(canvas, false);
         try
         {
-            _handler.OnRender(context);
+            _handler.Render(context, currentTransformedClip);
         }
         catch (Exception e)
         {

+ 1 - 1
src/Avalonia.Base/composition-schema.xml

@@ -54,7 +54,7 @@
         <Property Name="DebugOverlays" Type="RendererDebugOverlays"/>
         <Property Name="LastLayoutPassTiming" Type="LayoutPassTiming" Internal="true"/>
         <Property Name="Scaling" Type="double"/>
-        <Property Name="Size" Type="Size" />
+        <Property Name="PixelSize" Type="PixelSize" />
     </Object>
     <KeyFrameAnimation Name="Scalar" Type="float"/>
     <KeyFrameAnimation Name="Double" Type="double"/>

+ 3 - 2
src/Avalonia.Controls/BorderVisual.cs

@@ -45,7 +45,8 @@ class CompositionBorderVisual : CompositionDrawListVisual
         {
         }
 
-        protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip)
+        protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+            IDirtyRectTracker dirtyRects)
         {
             if (ClipToBounds)
             {
@@ -56,7 +57,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
                     canvas.PushClip(new RoundedRect(clipRect, _cornerRadius));
             }
 
-            base.RenderCore(canvas, currentTransformedClip);
+            base.RenderCore(canvas, currentTransformedClip, dirtyRects);
             
             if(ClipToBounds)
                 canvas.PopClip();

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

@@ -115,7 +115,7 @@ namespace Avalonia.X11
                
                 using (var cpuContext = platformRenderInterface.CreateBackendContext(null))
                 using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this }))
-                using (var ctx = renderTarget.CreateDrawingContext())
+                using (var ctx = renderTarget.CreateDrawingContext(true))
                 {
                     var r = new Rect(_pixelSize.ToSize(1)); 
                     ctx.DrawBitmap(bitmap, 1, r, r);

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

@@ -43,7 +43,7 @@ namespace Avalonia.X11
             _bdata = new uint[_width * _height];
             using(var cpuContext = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>().CreateBackendContext(null))
             using(var rt = cpuContext.CreateRenderTarget(new[]{this}))
-            using (var ctx = rt.CreateDrawingContext())
+            using (var ctx = rt.CreateDrawingContext(true))
                 ctx.DrawBitmap(bitmap.PlatformImpl.Item, 1, new Rect(bitmap.Size),
                     new Rect(0, 0, _width, _height));
             Data = new UIntPtr[_width * _height + 2];

+ 23 - 3
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -33,6 +33,8 @@ namespace Avalonia.Headless
 
         public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888;
         public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true;
+        public bool SupportsRegions => false;
+        public IPlatformRenderInterfaceRegion CreateRegion() => throw new NotSupportedException();
 
         public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect);
 
@@ -398,7 +400,7 @@ namespace Avalonia.Headless
 
             }
 
-            public IDrawingContextImpl CreateDrawingContext()
+            public IDrawingContextImpl CreateDrawingContext(bool _)
             {
                 return new HeadlessDrawingContextStub();
             }
@@ -454,7 +456,7 @@ namespace Avalonia.Headless
 
             }
 
-            public IDrawingContextLayerImpl CreateLayer(Size size)
+            public IDrawingContextLayerImpl CreateLayer(PixelSize size)
             {
                 return new HeadlessBitmapStub(size, new Vector(96, 96));
             }
@@ -464,11 +466,24 @@ namespace Avalonia.Headless
 
             }
 
+            public void PushClip(IPlatformRenderInterfaceRegion region)
+            {
+                
+            }
+
             public void PopClip()
             {
 
             }
 
+            public void PushLayer(Rect bounds)
+            {
+            }
+
+            public void PopLayer()
+            {
+            }
+
             public void PushOpacity(double opacity, Rect? rect)
             {
 
@@ -541,6 +556,11 @@ namespace Avalonia.Headless
                 
             }
 
+            public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region)
+            {
+                
+            }
+
             public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
             {
             }
@@ -573,7 +593,7 @@ namespace Avalonia.Headless
 
             }
 
-            public IDrawingContextImpl CreateDrawingContext()
+            public IDrawingContextImpl CreateDrawingContext(bool _)
             {
                 return new HeadlessDrawingContextStub();
             }

+ 76 - 19
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -21,7 +21,9 @@ namespace Avalonia.Skia
         IDrawingContextImplWithEffects
     {
         private IDisposable?[]? _disposables;
-        private readonly Vector _dpi;
+        // TODO: Get rid of this value, it's currently used to calculate intermediate sizes for tile brushes
+        // but does so ignoring the current transform
+        private readonly Vector _intermediateSurfaceDpi;
         private readonly Stack<PaintWrapper> _maskStack = new();
         private readonly Stack<double> _opacityStack = new();
         private readonly Stack<RenderOptions> _renderOptionsStack = new();
@@ -57,7 +59,12 @@ namespace Avalonia.Skia
             public SKSurface? Surface;
 
             /// <summary>
-            /// Dpi of drawings.
+            /// Makes DPI to be applied as a hidden matrix transform
+            /// </summary>
+            public bool ScaleDrawingToDpi;
+            
+            /// <summary>
+            /// Dpi for intermediate surfaces
             /// </summary>
             public Vector Dpi;
 
@@ -180,7 +187,7 @@ namespace Avalonia.Skia
             Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas
                 ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
 
-            _dpi = createInfo.Dpi;
+            _intermediateSurfaceDpi = createInfo.Dpi;
             _disposables = disposables;
             _disableSubpixelTextRendering = createInfo.DisableSubpixelTextRendering;
             _grContext = createInfo.GrContext;
@@ -191,10 +198,12 @@ namespace Avalonia.Skia
 
             _session = createInfo.CurrentSession;
 
-            if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
+            
+            if (createInfo.ScaleDrawingToDpi && !createInfo.Dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
             {
                 _postTransform =
-                    Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y);
+                    Matrix.CreateScale(createInfo.Dpi.X / SkiaPlatform.DefaultDpi.X,
+                        createInfo.Dpi.Y / SkiaPlatform.DefaultDpi.Y);
             }
 
             Transform = Matrix.Identity;
@@ -526,6 +535,32 @@ namespace Avalonia.Skia
                 SKRoundRectCache.Shared.Return(skRoundRect);
         }
 
+        /// <inheritdoc />
+        public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region)
+        {
+            var r = (SkiaRegionImpl)region;
+            if(r.IsEmpty)
+                return;
+            CheckLease();
+            
+            if (brush != null)
+            {
+                using (var fill = CreatePaint(_fillPaint, brush, r.Bounds.ToRect(1)))
+                {
+                    Canvas.DrawRegion(r.Region, fill.Paint);
+                }
+            }
+
+            if (pen is not null
+                && TryCreatePaint(_strokePaint, pen, r.Bounds.ToRect(1).Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
+            {
+                using (stroke)
+                {
+                    Canvas.DrawRegion(r.Region, stroke.Paint);
+                }
+            }
+        }
+
         /// <inheritdoc />
         public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
         {
@@ -591,7 +626,7 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public IDrawingContextLayerImpl CreateLayer(Size size)
+        public IDrawingContextLayerImpl CreateLayer(PixelSize size)
         {
             CheckLease();
             return CreateRenderTarget(size, true);
@@ -629,6 +664,14 @@ namespace Avalonia.Skia
             SKRoundRectCache.Shared.Return(roundRect);
         }
 
+        public void PushClip(IPlatformRenderInterfaceRegion region)
+        {
+            var r = ((SkiaRegionImpl)region).Region;
+            CheckLease();
+            Canvas.Save();
+            Canvas.ClipRegion(r);
+        }
+
         /// <inheritdoc />
         public void PopClip()
         {
@@ -636,6 +679,18 @@ namespace Avalonia.Skia
             Canvas.Restore();
         }
 
+        public void PushLayer(Rect bounds)
+        {
+            CheckLease();
+            Canvas.SaveLayer(bounds.ToSKRect(), null!);
+        }
+
+        public void PopLayer()
+        {
+            CheckLease();
+            Canvas.Restore();
+        }
+
         /// <inheritdoc />
         public void PushOpacity(double opacity, Rect? bounds)
         {
@@ -976,15 +1031,16 @@ namespace Avalonia.Skia
         /// <param name="tileBrushImage">Tile brush image.</param>
         private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Rect targetBox, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
         {
-            var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_dpi), targetBox.Size);
-            var intermediate = CreateRenderTarget(calc.IntermediateSize, false);
+            var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_intermediateSurfaceDpi), targetBox.Size);
+            var intermediate = CreateRenderTarget(
+                PixelSize.FromSizeWithDpi(calc.IntermediateSize, _intermediateSurfaceDpi), false);
 
             paintWrapper.AddDisposable(intermediate);
 
-            using (var context = intermediate.CreateDrawingContext())
+            using (var context = intermediate.CreateDrawingContext(true))
             {
                 var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96));
-                var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi));
+                var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_intermediateSurfaceDpi));
 
                 context.Clear(Colors.Transparent);
                 context.PushClip(calc.IntermediateClip);
@@ -1028,7 +1084,7 @@ namespace Avalonia.Skia
             SKMatrix.Concat(
                 ref paintTransform,
                 tileTransform,
-                SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y)));
+                SKMatrix.CreateScale((float)(96.0 / _intermediateSurfaceDpi.X), (float)(96.0 / _intermediateSurfaceDpi.Y)));
 
             if (tileBrush.Transform is { })
             {
@@ -1066,9 +1122,10 @@ namespace Avalonia.Skia
 
             if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
             {
-                using var intermediate = CreateRenderTarget(intermediateSize, false);
+                using var intermediate = CreateRenderTarget(
+                    PixelSize.FromSizeWithDpi(intermediateSize, _intermediateSurfaceDpi), false);
 
-                using (var ctx = intermediate.CreateDrawingContext())
+                using (var ctx = intermediate.CreateDrawingContext(true))
                 {
                     ctx.RenderOptions = RenderOptions;
                     ctx.Clear(Colors.Transparent);
@@ -1096,7 +1153,7 @@ namespace Avalonia.Skia
             var calc = new TileBrushCalculator(tileBrush, contentSize, targetRect.Size);
             transform *= calc.IntermediateTransform;
             
-            using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi);
+            using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi);
             using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize))
             {
                 ctx.RenderOptions = RenderOptions;
@@ -1127,7 +1184,7 @@ namespace Avalonia.Skia
                         : SKShaderTileMode.Repeat;
 
             paintTransform = SKMatrix.Concat(paintTransform,
-                SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y)));
+                SKMatrix.CreateScale((float)(96.0 / _intermediateSurfaceDpi.X), (float)(96.0 / _intermediateSurfaceDpi.Y)));
             
             if (tileBrush.Transform is { })
             {
@@ -1339,18 +1396,18 @@ namespace Avalonia.Skia
         /// <summary>
         /// Create new render target compatible with this drawing context.
         /// </summary>
-        /// <param name="size">The size of the render target in DIPs.</param>
+        /// <param name="pixelSize">The size of the render target.</param>
+        /// <param name="dpi">The DPI of the render target.</param>
         /// <param name="isLayer">Whether the render target is being created for a layer.</param>
         /// <param name="format">Pixel format.</param>
         /// <returns></returns>
-        private SurfaceRenderTarget CreateRenderTarget(Size size, bool isLayer, PixelFormat? format = null)
+        private SurfaceRenderTarget CreateRenderTarget(PixelSize pixelSize, bool isLayer, PixelFormat? format = null)
         {
-            var pixelSize = PixelSize.FromSizeWithDpi(size, _dpi);
             var createInfo = new SurfaceRenderTarget.CreateInfo
             {
                 Width = pixelSize.Width,
                 Height = pixelSize.Height,
-                Dpi = _dpi,
+                Dpi = _intermediateSurfaceDpi,
                 Format = format,
                 DisableTextLcdRendering = isLayer ? _disableSubpixelTextRendering : true,
                 GrContext = _grContext,

+ 3 - 2
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@@ -37,7 +37,7 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public IDrawingContextImpl CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi)
         {
             if (_renderTarget == null)
                 throw new ObjectDisposedException(nameof(FramebufferRenderTarget));
@@ -58,7 +58,8 @@ namespace Avalonia.Skia
             var createInfo = new DrawingContextImpl.CreateInfo
             {
                 Surface = _framebufferSurface,
-                Dpi = framebuffer.Dpi
+                Dpi = framebuffer.Dpi,
+                ScaleDrawingToDpi = scaleDrawingToDpi
             };
 
             return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer);

+ 2 - 1
src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Skia
             _renderTarget.Dispose();
         }
 
-        public IDrawingContextImpl CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
         {
             var session = _renderTarget.BeginRenderingSession();
 
@@ -30,6 +30,7 @@ namespace Avalonia.Skia
                 GrContext = session.GrContext,
                 Surface = session.SkSurface,
                 Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor,
+                ScaleDrawingToDpi = useScaledDrawing,
                 Gpu = _skiaGpu,
                 CurrentSession =  session
             };

+ 1 - 0
src/Skia/Avalonia.Skia/PictureRenderTarget.cs

@@ -38,6 +38,7 @@ internal class PictureRenderTarget : IDisposable
         var createInfo = new DrawingContextImpl.CreateInfo
         {
             Canvas = canvas,
+            ScaleDrawingToDpi = true,
             Dpi = _dpi,
             DisableSubpixelTextRendering = true,
             GrContext = _grContext,

+ 3 - 0
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -50,6 +50,9 @@ namespace Avalonia.Skia
             || format == PixelFormats.Bgra8888
             || format == PixelFormats.Rgba8888;
 
+        public bool SupportsRegions => true;
+        public IPlatformRenderInterfaceRegion CreateRegion() => new SkiaRegionImpl();
+
         public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
 
         public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2);

+ 2 - 1
src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs

@@ -19,7 +19,8 @@ internal class RenderTargetBitmapImpl : WriteableBitmapImpl,
         _renderTarget = new FramebufferRenderTarget(this);
     }
 
-    public IDrawingContextImpl CreateDrawingContext() => _renderTarget.CreateDrawingContext();
+    IDrawingContextImpl IRenderTarget.CreateDrawingContext(bool useScaledDrawing) =>
+        _renderTarget.CreateDrawingContext(useScaledDrawing);
 
     public bool IsCorrupted => false;
     

+ 53 - 0
src/Skia/Avalonia.Skia/SkiaRegionImpl.cs

@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Platform;
+using SkiaSharp;
+
+namespace Avalonia.Skia;
+
+internal class SkiaRegionImpl : IPlatformRenderInterfaceRegion
+{
+    private SKRegion? _region = new();
+    public SKRegion Region => _region ?? throw new ObjectDisposedException(nameof(SkiaRegionImpl));
+    private bool _rectsValid;
+    private List<PixelRect>? _rects;
+    public void Dispose()
+    {
+        _region?.Dispose();
+        _region = null;
+    }
+
+    public void AddRect(PixelRect rect)
+    {
+        _rectsValid = false;
+        Region.Op(rect.X, rect.Y, rect.Right, rect.Bottom, SKRegionOperation.Union);
+    }
+
+    public void Reset()
+    {
+        _rectsValid = false;
+        Region.SetEmpty();
+    }
+
+    public bool IsEmpty => Region.IsEmpty;
+    public PixelRect Bounds => Region.Bounds.ToAvaloniaPixelRect();
+
+    public IList<PixelRect> Rects
+    {
+        get
+        {
+            _rects ??= new();
+            if (!_rectsValid)
+            {
+                _rects.Clear();
+                using var iter = Region.CreateRectIterator();
+                while (iter.Next(out var rc))
+                    _rects.Add(rc.ToAvaloniaPixelRect());
+            }
+            return _rects;
+        }
+    }
+
+    public bool Intersects(Rect rect) => Region.Intersects(PixelRect.FromRect(rect, 1).ToSKRectI());
+    public bool Contains(Point pt) => Region.Contains((int)pt.X, (int)pt.Y);
+}

+ 10 - 0
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@@ -75,6 +75,11 @@ namespace Avalonia.Skia
         {
             return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom);
         }
+        
+        public static SKRectI ToSKRectI(this PixelRect r)
+        {
+            return new SKRectI(r.X, r.Y, r.Right, r.Bottom);
+        }
 
         public static SKRoundRect ToSKRoundRect(this RoundedRect r)
         {
@@ -95,6 +100,11 @@ namespace Avalonia.Skia
         {
             return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
         }
+        
+        public static PixelRect ToAvaloniaPixelRect(this SKRectI r)
+        {
+            return new PixelRect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
+        }
 
         public static SKMatrix ToSKMatrix(this Matrix m)
         {

+ 2 - 1
src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs

@@ -97,7 +97,7 @@ namespace Avalonia.Skia
         }
 
         /// <inheritdoc />
-        public IDrawingContextImpl CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
         {
             _canvas.RestoreToCount(-1);
             _canvas.ResetMatrix();
@@ -106,6 +106,7 @@ namespace Avalonia.Skia
             {
                 Surface = _surface.Surface,
                 Dpi = Dpi,
+                ScaleDrawingToDpi = useScaledDrawing,
                 DisableSubpixelTextRendering = _disableLcdRendering,
                 GrContext = _grContext,
                 Gpu = _gpu,

+ 3 - 0
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@@ -296,5 +296,8 @@ namespace Avalonia.Direct2D1
         public bool IsSupportedBitmapPixelFormat(PixelFormat format) =>
             format == PixelFormats.Bgra8888 
             || format == PixelFormats.Rgba8888;
+
+        public bool SupportsRegions => false;
+        public IPlatformRenderInterfaceRegion CreateRegion() => throw new NotSupportedException();
     }
 }

+ 2 - 2
src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs

@@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1
             _externalRenderTargetProvider.DestroyRenderTarget();
         }
 
-        public IDrawingContextImpl CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
         {
             var target =  _externalRenderTargetProvider.GetOrCreateRenderTarget();
             _externalRenderTargetProvider.BeforeDrawing();
-            return new DrawingContextImpl( null, target, null, () =>
+            return new DrawingContextImpl( null, target, useScaledDrawing, null, () =>
             {
                 try
                 {

+ 4 - 4
src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs

@@ -25,7 +25,7 @@ namespace Avalonia.Direct2D1
             _target = null;
         }
 
-        public IDrawingContextImpl CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
         {
             if (_target == null)
                 throw new ObjectDisposedException(nameof(FramebufferShimRenderTarget));
@@ -37,7 +37,7 @@ namespace Avalonia.Direct2D1
             }
 
             return new FramebufferShim(locked)
-                .CreateDrawingContext();
+                .CreateDrawingContext(useScaledDrawing);
         }
 
         public bool IsCorrupted => false;
@@ -52,9 +52,9 @@ namespace Avalonia.Direct2D1
                 _target = target;
             }
             
-            public override IDrawingContextImpl CreateDrawingContext()
+            public override IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
             {
-                return base.CreateDrawingContext(() =>
+                return base.CreateDrawingContext(useScaledDrawing, () =>
                 {
                     using (var l = WicImpl.Lock(BitmapLockFlags.Read))
                     {

+ 51 - 7
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -39,6 +39,7 @@ namespace Avalonia.Direct2D1.Media
         public DrawingContextImpl(
             ILayerFactory layerFactory,
             SharpDX.Direct2D1.RenderTarget renderTarget,
+            bool useScaledDrawing,
             SharpDX.DXGI.SwapChain1 swapChain = null,
             Action finishedCallback = null)
         {
@@ -58,6 +59,13 @@ namespace Avalonia.Direct2D1.Media
                 _ownsDeviceContext = true;
             }
 
+            if (!useScaledDrawing)
+            {
+                var scaling = _renderTarget.DotsPerInch.Width / 96;
+                if (!MathUtilities.AreClose(1, scaling))
+                    _postTransform = Matrix.CreateScale(1 / scaling, 1 / scaling);
+            }
+            
             _deviceContext.BeginDraw();
         }
 
@@ -66,8 +74,13 @@ namespace Avalonia.Direct2D1.Media
         /// </summary>
         public Matrix Transform
         {
-            get { return _deviceContext.Transform.ToAvalonia(); }
-            set { _deviceContext.Transform = value.ToDirect2D(); }
+            get { return _transform; }
+            set
+            {
+                _transform = value;
+                _deviceContext.Transform =
+                    (_postTransform.HasValue ? value * _postTransform.Value : value).ToDirect2D();
+            }
         }
 
         public Matrix4x4 Transform4x4
@@ -353,6 +366,11 @@ namespace Avalonia.Direct2D1.Media
             }
         }
 
+        public void DrawRegion(IBrush brush, IPen pen, IPlatformRenderInterfaceRegion region)
+        {
+            throw new NotSupportedException();
+        }
+
         /// <inheritdoc />
         public void DrawEllipse(IBrush brush, IPen pen, Rect rect)
         {
@@ -410,17 +428,17 @@ namespace Avalonia.Direct2D1.Media
             }
         }
 
-        public IDrawingContextLayerImpl CreateLayer(Size size)
+        public IDrawingContextLayerImpl CreateLayer(PixelSize pixelSize)
         {
+            var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height);
             if (_layerFactory != null)
             {
-                return _layerFactory.CreateLayer(size);
+                return _layerFactory.CreateLayer(pixelSize.ToSizeWithDpi(dpi));
             }
             else
             {
                 var platform = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
-                var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height);
-                var pixelSize = PixelSize.FromSizeWithDpi(size, dpi);
+                
                 return (IDrawingContextLayerImpl)platform.CreateRenderTargetBitmap(pixelSize, dpi);
             }
         }
@@ -441,14 +459,40 @@ namespace Avalonia.Direct2D1.Media
             _deviceContext.PushAxisAlignedClip(clip.Rect.ToDirect2D(), AntialiasMode.PerPrimitive);
         }
 
+        public void PushClip(IPlatformRenderInterfaceRegion region)
+        {
+            throw new NotSupportedException();
+        }
+
         public void PopClip()
         {
             _deviceContext.PopAxisAlignedClip();
         }
 
+        public void PushLayer(Rect bounds)
+        {
+            var parameters = new LayerParameters
+            {
+                ContentBounds = bounds.ToDirect2D(),
+                MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
+                Opacity = 1
+            };
+            var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext);
+            _deviceContext.PushLayer(ref parameters, layer);
+
+            _layers.Push(layer);
+        }
+
+        void IDrawingContextImpl.PopLayer()
+        {
+            PopLayer();
+        }
+
         readonly Stack<Layer> _layers = new Stack<Layer>();
         private readonly Stack<Layer> _layerPool = new Stack<Layer>();
         private RenderOptions _renderOptions;
+        private readonly Matrix? _postTransform;
+        private Matrix _transform = Matrix.Identity;
 
         /// <summary>
         /// Pushes an opacity value.
@@ -574,7 +618,7 @@ namespace Avalonia.Direct2D1.Media
                                    CompatibleRenderTargetOptions.None,
                                    pixelSize.ToSizeWithDpi(dpi).ToSharpDX()))
                         {
-                            using (var ctx = new RenderTarget(intermediate).CreateDrawingContext())
+                            using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(true))
                             {
                                 intermediate.Clear(null);
                                 sceneBrushContent.Render(ctx,

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

@@ -98,7 +98,7 @@ namespace Avalonia.Direct2D1.Media
                 CompatibleRenderTargetOptions.None,
                 calc.IntermediateSize.ToSharpDX());
 
-            using (var context = new RenderTarget(result).CreateDrawingContext())
+            using (var context = new RenderTarget(result).CreateDrawingContext(true))
             {
                 var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height);
                 var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi));

+ 4 - 3
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs

@@ -30,9 +30,10 @@ namespace Avalonia.Direct2D1.Media.Imaging
             return new D2DRenderTargetBitmapImpl(bitmapRenderTarget);
         }
 
-        public IDrawingContextImpl CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
         {
-            return new DrawingContextImpl( this, _renderTarget, null, () => Version++);
+            return new DrawingContextImpl( this, _renderTarget, useScaledDrawing, 
+                null, () => Version++);
         }
 
         public bool IsCorrupted => false;
@@ -60,7 +61,7 @@ namespace Avalonia.Direct2D1.Media.Imaging
         {
             using (var wic = new WicRenderTargetBitmapImpl(PixelSize, Dpi))
             {
-                using (var dc = wic.CreateDrawingContext(null))
+                using (var dc = wic.CreateDrawingContext(true, null))
                 {
                     dc.DrawBitmap(
                         this,

+ 4 - 4
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs

@@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media
             base.Dispose();
         }
 
-        public virtual IDrawingContextImpl CreateDrawingContext()
-            => CreateDrawingContext(null);
+        public virtual IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
+            => CreateDrawingContext(useScaledDrawing, null);
 
         public bool IsCorrupted => false;
 
-        public IDrawingContextImpl CreateDrawingContext(Action finishedCallback)
+        public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, Action finishedCallback)
         {
-            return new DrawingContextImpl(null, _renderTarget, finishedCallback: () =>
+            return new DrawingContextImpl(null, _renderTarget, useScaledDrawing, finishedCallback: () =>
                 {
                     Version++;
                     finishedCallback?.Invoke();

+ 2 - 2
src/Windows/Avalonia.Direct2D1/RenderTarget.cs

@@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1
         /// Creates a drawing context for a rendering session.
         /// </summary>
         /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
-        public IDrawingContextImpl CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
         {
-            return new DrawingContextImpl(this, _renderTarget);
+            return new DrawingContextImpl(this, _renderTarget, useScaledDrawing);
         }
 
         public bool IsCorrupted => false;

+ 2 - 2
src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1
         /// Creates a drawing context for a rendering session.
         /// </summary>
         /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
-        public IDrawingContextImpl CreateDrawingContext()
+        public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
         {
             var size = GetWindowSize();
             var dpi = GetWindowDpi();
@@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1
                 Resize();
             }
 
-            return new DrawingContextImpl(this, _deviceContext, _swapChain);
+            return new DrawingContextImpl(this, _deviceContext, useScaledDrawing, _swapChain);
         }
 
         public bool IsCorrupted => false;

+ 1 - 1
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@@ -72,7 +72,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
             var r = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
             using(var cpuContext = r.CreateBackendContext(null))
             using (var target = cpuContext.CreateRenderTarget(new object[] { fb }))
-            using (var ctx = target.CreateDrawingContext())
+            using (var ctx = target.CreateDrawingContext(false))
             {
                 ctx.Clear(Colors.Transparent);
                 ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80));

+ 3 - 3
tests/Avalonia.UnitTests/TestRoot.cs

@@ -79,16 +79,16 @@ namespace Avalonia.UnitTests
         public IRenderTarget CreateRenderTarget()
         {
             var dc = new Mock<IDrawingContextImpl>();
-            dc.Setup(x => x.CreateLayer(It.IsAny<Size>())).Returns(() =>
+            dc.Setup(x => x.CreateLayer(It.IsAny<PixelSize>())).Returns(() =>
             {
                 var layerDc = new Mock<IDrawingContextImpl>();
                 var layer = new Mock<IDrawingContextLayerImpl>();
-                layer.Setup(x => x.CreateDrawingContext()).Returns(layerDc.Object);
+                layer.Setup(x => x.CreateDrawingContext(It.IsAny<bool>())).Returns(layerDc.Object);
                 return layer.Object;
             });
 
             var result = new Mock<IRenderTarget>();
-            result.Setup(x => x.CreateDrawingContext()).Returns(dc.Object);
+            result.Setup(x => x.CreateDrawingContext(It.IsAny<bool>())).Returns(dc.Object);
             return result.Object;
         }