Browse Source

Use separate LtrbRect type for renderer calculations (#15112)

Nikita Tsukanov 1 year ago
parent
commit
5b59f6c0bc
20 changed files with 410 additions and 106 deletions
  1. 4 4
      src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs
  2. 268 0
      src/Avalonia.Base/Platform/LtrbRect.cs
  3. 1 1
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs
  4. 1 1
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs
  5. 14 8
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs
  6. 22 20
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs
  7. 4 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  8. 2 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  9. 2 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs
  10. 2 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs
  11. 2 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs
  12. 14 14
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs
  13. 3 3
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  14. 31 30
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  15. 5 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  16. 2 1
      src/Avalonia.Controls/BorderVisual.cs
  17. 2 2
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  18. 10 7
      src/Skia/Avalonia.Skia/SkiaRegionImpl.cs
  19. 20 0
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  20. 1 1
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

+ 4 - 4
src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs

@@ -7,11 +7,11 @@ namespace Avalonia.Platform;
 [Unstable, PrivateApi]
 public interface IPlatformRenderInterfaceRegion : IDisposable
 {
-    void AddRect(PixelRect rect);
+    void AddRect(LtrbPixelRect rect);
     void Reset();
     bool IsEmpty { get; }
-    PixelRect Bounds { get; }
-    IList<PixelRect> Rects { get; }
-    bool Intersects(Rect rect);
+    LtrbPixelRect Bounds { get; }
+    IList<LtrbPixelRect> Rects { get; }
+    bool Intersects(LtrbRect rect);
     bool Contains(Point pt);
 }

+ 268 - 0
src/Avalonia.Base/Platform/LtrbRect.cs

@@ -0,0 +1,268 @@
+// ReSharper disable CompareOfFloatsByEqualityOperator
+
+using System;
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform;
+
+/// <summary>
+/// This struct is essentially the same thing as MilRectD
+/// Unlike our "normal" Rect which is more human-readable and human-usable
+/// this struct is optimized for actual processing that doesn't really care
+/// about Width and Height but pretty much always only cares about
+/// Right and Bottom edge coordinates
+///
+/// Not having to constantly convert between Width/Height and Right/Bottom for no actual reason
+/// saves us some perf
+///
+/// This structure is intended to be mostly internal, but it's exposed as a PrivateApi type so it can
+/// be passed to the drawing backend when needed
+/// </summary>
+[PrivateApi]
+public struct LtrbRect
+{
+    public double Left, Top, Right, Bottom;
+
+    internal LtrbRect(double x, double y, double right, double bottom)
+    {
+        Left = x;
+        Top = y;
+        Right = right;
+        Bottom = bottom;
+    }
+
+    internal LtrbRect(Rect rc)
+    {
+        rc = rc.Normalize();
+        Left = rc.X;
+        Top = rc.Y;
+        Right = rc.Right;
+        Bottom = rc.Bottom;
+    }
+
+    internal bool IsZeroSize => Left == Right && Top == Bottom;
+
+    internal LtrbRect Intersect(LtrbRect rect)
+    {
+        var newLeft = (rect.Left > Left) ? rect.Left : Left;
+        var newTop = (rect.Top > Top) ? rect.Top : Top;
+        var newRight = (rect.Right < Right) ? rect.Right : Right;
+        var newBottom = (rect.Bottom < Bottom) ? rect.Bottom : Bottom;
+
+        if ((newRight > newLeft) && (newBottom > newTop))
+        {
+            return new LtrbRect(newLeft, newTop, newRight, newBottom);
+        }
+        else
+        {
+            return default;
+        }
+    }
+
+    internal bool Intersects(LtrbRect rect)
+    {
+        return (rect.Left < Right) && (Left < rect.Right) && (rect.Top < Bottom) && (Top < rect.Bottom);
+    }
+
+    internal Rect ToRect() => new(Left, Top, Right - Left, Bottom - Top);
+
+    internal LtrbRect Inflate(Thickness thickness)
+    {
+        return new LtrbRect(Left - thickness.Left, Top - thickness.Top, Right + thickness.Right,
+            Bottom + thickness.Bottom);
+    }
+    
+    public static bool operator ==(LtrbRect left, LtrbRect right)=>
+        left.Left == right.Left && left.Top == right.Top && left.Right == right.Right && left.Bottom == right.Bottom;
+
+    public static bool operator !=(LtrbRect left, LtrbRect right) =>
+        left.Left != right.Left || left.Top != right.Top || left.Right != right.Right || left.Bottom != right.Bottom;
+    
+    public bool Equals(LtrbRect other) =>
+        other.Left == Left && other.Top == Top && other.Right == Right && other.Bottom == Bottom;
+    
+    public bool Equals(ref LtrbRect other) =>
+        other.Left == Left && other.Top == Top && other.Right == Right && other.Bottom == Bottom;
+
+    internal Point TopLeft => new Point(Left, Top);
+    internal Point TopRight => new Point(Right, Top);
+    internal Point BottomLeft => new Point(Left, Bottom);
+    internal Point BottomRight => new Point(Right, Bottom);
+    
+    internal LtrbRect TransformToAABB(Matrix matrix)
+    {
+        ReadOnlySpan<Point> points = stackalloc Point[4]
+        {
+            TopLeft.Transform(matrix),
+            TopRight.Transform(matrix),
+            BottomRight.Transform(matrix),
+            BottomLeft.Transform(matrix)
+        };
+
+        var left = double.MaxValue;
+        var right = double.MinValue;
+        var top = double.MaxValue;
+        var bottom = double.MinValue;
+
+        foreach (var p in points)
+        {
+            if (p.X < left) left = p.X;
+            if (p.X > right) right = p.X;
+            if (p.Y < top) top = p.Y;
+            if (p.Y > bottom) bottom = p.Y;
+        }
+
+        return new LtrbRect(left, top, right, bottom);
+    }
+    
+    /// <summary>
+    /// Perform _WPF-like_ union operation
+    /// </summary>
+    private LtrbRect FullUnionCore(LtrbRect rect)
+    {
+        var x1 = Math.Min(Left, rect.Left);
+        var x2 = Math.Max(Right, rect.Right);
+        var y1 = Math.Min(Top, rect.Top);
+        var y2 = Math.Max(Bottom, rect.Bottom);
+
+        return new(x1, y1, x2, y2);
+    }
+    
+    internal static LtrbRect? FullUnion(LtrbRect? left, LtrbRect? right)
+    {
+        if (left == null)
+            return right;
+        if (right == null)
+            return left;
+        return right.Value.FullUnionCore(left.Value);
+    }
+    
+    internal static LtrbRect? FullUnion(LtrbRect? left, Rect? right)
+    {
+        if (right == null)
+            return left;
+        if (left == null)
+            return new(right.Value);
+        return left.Value.FullUnionCore(new(right.Value));
+    }
+
+    public override bool Equals(object? obj)
+    {
+        if (obj is LtrbRect other)
+            return Equals(other);
+        return false;
+    }
+
+    public override int GetHashCode()
+    {
+        unchecked
+        {
+            int hash = 17;
+            hash = (hash * 23) + Left.GetHashCode();
+            hash = (hash * 23) + Top.GetHashCode();
+            hash = (hash * 23) + Right.GetHashCode();
+            hash = (hash * 23) + Bottom.GetHashCode();
+            return hash;
+        }
+    }
+}
+
+/// <summary>
+/// This struct is essentially the same thing as RECT from win32 API
+/// Unlike our "normal" PixelRect which is more human-readable and human-usable
+/// this struct is optimized for actual processing that doesn't really care
+/// about Width and Height but pretty much always only cares about
+/// Right and Bottom edge coordinates
+///
+/// Not having to constantly convert between Width/Height and Right/Bottom for no actual reason
+/// saves us some perf
+///
+/// This structure is intended to be mostly internal, but it's exposed as a PrivateApi type so it can
+/// be passed to the drawing backend when needed
+/// </summary>
+[PrivateApi]
+public struct LtrbPixelRect
+{
+    public int Left, Top, Right, Bottom;
+
+    internal LtrbPixelRect(int x, int y, int right, int bottom)
+    {
+        Left = x;
+        Top = y;
+        Right = right;
+        Bottom = bottom;
+    }
+
+    internal LtrbPixelRect(PixelSize size)
+    {
+        Left = 0;
+        Top = 0;
+        Right = size.Width;
+        Bottom = size.Height;
+    }
+
+    internal bool IsEmpty => Left == Right && Top == Bottom;
+
+    internal PixelRect ToPixelRect() => new(Left, Top, Right - Left, Bottom - Top);
+    internal LtrbPixelRect Union(LtrbPixelRect rect)
+    {
+        if (IsEmpty)
+            return rect;
+        if (rect.IsEmpty)
+            return this;
+        var x1 = Math.Min(Left, rect.Left);
+        var x2 = Math.Max(Right, rect.Right);
+        var y1 = Math.Min(Top, rect.Top);
+        var y2 = Math.Max(Bottom, rect.Bottom);
+
+        return new(x1, y1, x2, y2);
+    }
+
+    internal Rect ToRectWithNoScaling() => new(Left, Top, (Right - Left), (Bottom - Top));
+
+    internal bool Contains(int x, int y)
+    {
+        return x >= Left && x <= Right && y >= Top && y <= Bottom;
+    }
+
+    internal static LtrbPixelRect FromRectWithNoScaling(LtrbRect rect) =>
+        new((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right),
+            (int)Math.Ceiling(rect.Bottom));
+    
+    public static bool operator ==(LtrbPixelRect left, LtrbPixelRect right)=>
+        left.Left == right.Left && left.Top == right.Top && left.Right == right.Right && left.Bottom == right.Bottom;
+
+    public static bool operator !=(LtrbPixelRect left, LtrbPixelRect right) =>
+        left.Left != right.Left || left.Top != right.Top || left.Right != right.Right || left.Bottom != right.Bottom;
+    
+    public bool Equals(LtrbPixelRect other) =>
+        other.Left == Left && other.Top == Top && other.Right == Right && other.Bottom == Bottom;
+
+    public override bool Equals(object? obj)
+    {
+        if (obj is LtrbPixelRect other)
+            return Equals(other);
+        return false;
+    }
+
+    public override int GetHashCode()
+    {
+        unchecked
+        {
+            int hash = 17;
+            hash = (hash * 23) + Left.GetHashCode();
+            hash = (hash * 23) + Top.GetHashCode();
+            hash = (hash * 23) + Right.GetHashCode();
+            hash = (hash * 23) + Bottom.GetHashCode();
+            return hash;
+        }
+    }
+
+    internal Rect ToRectUnscaled() => new(Left, Top, Right - Left, Bottom - Top);
+
+    internal static LtrbPixelRect FromRectUnscaled(LtrbRect rect)
+    {
+        return new LtrbPixelRect((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right),
+            (int)Math.Ceiling(rect.Bottom));
+    }
+}

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

@@ -105,6 +105,6 @@ public abstract class CompositionCustomVisualHandler
     {
         VerifyInRender();
         rc = rc.TransformToAABB(_host!.GlobalTransformMatrix);
-        return _currentTransformedClip.Intersects(rc) && _host.Root!.DirtyRects.Intersects(rc);
+        return _currentTransformedClip.Intersects(rc) && _host.Root!.DirtyRects.Intersects(new (rc));
     }
 }

+ 1 - 1
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs

@@ -19,7 +19,7 @@ internal class CompositionRenderDataSceneBrushContent : ISceneBrushContent
     }
 
     public ITileBrush Brush { get; }
-    public Rect Rect => _rect ?? (RenderData.Server?.Bounds ?? default);
+    public Rect Rect => _rect ?? (RenderData.Server?.Bounds?.ToRect() ?? default);
 
     public double Opacity => Brush.Opacity;
     public ITransform? Transform => Brush.Transform;

+ 14 - 8
src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs

@@ -14,7 +14,7 @@ class ServerCompositionRenderData : SimpleServerRenderResource
 {
     private PooledInlineList<IRenderDataItem> _items;
     private PooledInlineList<IServerRenderResource> _referencedResources;
-    private Rect? _bounds;
+    private LtrbRect? _bounds;
     private bool _boundsValid;
     private static readonly ThreadSafeObjectPool<Collector> s_resourceHashSetPool = new();
 
@@ -67,7 +67,7 @@ class ServerCompositionRenderData : SimpleServerRenderResource
         }
     }
 
-    public Rect? Bounds
+    public LtrbRect? Bounds
     {
         get
         {
@@ -80,25 +80,31 @@ class ServerCompositionRenderData : SimpleServerRenderResource
         }
     }
 
-    private Rect? CalculateRenderBounds()
+    private LtrbRect? CalculateRenderBounds()
     {
-        Rect? totalBounds = null;
+        LtrbRect? totalBounds = null;
         foreach (var item in _items) 
-            totalBounds = Rect.Union(totalBounds, item.Bounds);
+            totalBounds = LtrbRect.FullUnion(totalBounds, item.Bounds);
         
         return ApplyRenderBoundsRounding(totalBounds);
     }
 
     public static Rect? ApplyRenderBoundsRounding(Rect? rect)
+    {
+        if (rect == null)
+            return null;
+        return ApplyRenderBoundsRounding(new LtrbRect(rect.Value))?.ToRect();
+    }
+    
+    public static LtrbRect? ApplyRenderBoundsRounding(LtrbRect? rect)
     {
         if (rect != null)
         {
             var r = rect.Value;
             // I don't believe that it's correct to do here (rather than in CompositionVisual),
             // but it's the old behavior, so I'm keeping it for now
-            return new Rect(
-                new Point(Math.Floor(r.X), Math.Floor(r.Y)),
-                new Point(Math.Ceiling(r.Right), Math.Ceiling(r.Bottom)));
+            return new LtrbRect(Math.Floor(r.Left), Math.Floor(r.Top),
+                Math.Ceiling(r.Right), Math.Ceiling(r.Bottom));
         }
 
         return null;

+ 22 - 20
src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs

@@ -10,38 +10,40 @@ namespace Avalonia.Rendering.Composition.Server;
 
 internal interface IDirtyRectTracker
 {
-    void AddRect(PixelRect rect);
+    void AddRect(LtrbPixelRect rect);
     IDisposable BeginDraw(IDrawingContextImpl ctx);
     bool IsEmpty { get; }
-    bool Intersects(Rect rect);
+    bool Intersects(LtrbRect rect);
     bool Contains(Point pt);
     void Reset();
     void Visualize(IDrawingContextImpl context);
-    PixelRect CombinedRect { get; }
-    IList<PixelRect> Rects { get; }
+    LtrbPixelRect CombinedRect { get; }
+    IList<LtrbPixelRect> Rects { get; }
 }
 
 internal class DirtyRectTracker : IDirtyRectTracker
 {
-    private PixelRect _rect;
+    private LtrbPixelRect _rect;
     private Rect _doubleRect;
-    private PixelRect[] _rectsForApi = new PixelRect[1];
+    private LtrbRect _normalRect;
+    private LtrbPixelRect[] _rectsForApi = new LtrbPixelRect[1];
     private Random _random = new();
-    public void AddRect(PixelRect rect)
+    public void AddRect(LtrbPixelRect rect)
     {
         _rect = _rect.Union(rect);
     }
     
     public IDisposable BeginDraw(IDrawingContextImpl ctx)
     {
-        ctx.PushClip(_rect.ToRect(1));
-        _doubleRect = _rect.ToRect(1);
+        ctx.PushClip(_rect.ToRectWithNoScaling());
+        _doubleRect = _rect.ToRectWithNoScaling();
+        _normalRect = new(_doubleRect);
         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 bool IsEmpty => _rect.IsEmpty;
+    public bool Intersects(LtrbRect rect) => _normalRect.Intersects(rect);
+    public bool Contains(Point pt) => _rect.Contains((int)pt.X, (int)pt.Y);
 
     public void Reset() => _rect = default;
     public void Visualize(IDrawingContextImpl context)
@@ -52,14 +54,14 @@ internal class DirtyRectTracker : IDirtyRectTracker
             null, _doubleRect);
     }
 
-    public PixelRect CombinedRect => _rect;
+    public LtrbPixelRect CombinedRect => _rect;
 
-    public IList<PixelRect> Rects
+    public IList<LtrbPixelRect> Rects
     {
         get
         {
-            if (_rect.Width == 0 || _rect.Height == 0)
-                return Array.Empty<PixelRect>();
+            if (_rect.IsEmpty)
+                return Array.Empty<LtrbPixelRect>();
             _rectsForApi[0] = _rect;
             return _rectsForApi;
         }
@@ -76,7 +78,7 @@ internal class RegionDirtyRectTracker : IDirtyRectTracker
         _region = platformRender.CreateRegion();
     }
 
-    public void AddRect(PixelRect rect) => _region.AddRect(rect);
+    public void AddRect(LtrbPixelRect rect) => _region.AddRect(rect);
 
     public IDisposable BeginDraw(IDrawingContextImpl ctx)
     {
@@ -85,7 +87,7 @@ internal class RegionDirtyRectTracker : IDirtyRectTracker
     }
 
     public bool IsEmpty => _region.IsEmpty;
-    public bool Intersects(Rect rect) => _region.Intersects(rect);
+    public bool Intersects(LtrbRect rect) => _region.Intersects(rect);
     public bool Contains(Point pt) => _region.Contains(pt);
 
     public void Reset() => _region.Reset();
@@ -98,6 +100,6 @@ internal class RegionDirtyRectTracker : IDirtyRectTracker
             null, _region);
     }
 
-    public PixelRect CombinedRect => _region.Bounds;
-    public IList<PixelRect> Rects => _region.Rects;
+    public LtrbPixelRect CombinedRect => _region.Bounds;
+    public IList<LtrbPixelRect> Rects => _region.Rects;
 }

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

@@ -13,10 +13,10 @@ namespace Avalonia.Rendering.Composition.Server
     internal partial class ServerCompositionContainerVisual : ServerCompositionVisual
     {
         public ServerCompositionVisualCollection Children { get; private set; } = null!;
-        private Rect? _transformedContentBounds;
+        private LtrbRect? _transformedContentBounds;
         private IImmutableEffect? _oldEffect;
         
-        protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+        protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip,
             IDirtyRectTracker dirtyRects)
         {
             base.RenderCore(canvas, currentTransformedClip, dirtyRects);
@@ -39,7 +39,7 @@ namespace Avalonia.Rendering.Composition.Server
                     var res = child.Update(root, GlobalTransformMatrix);
                     oldInvalidated |= res.InvalidatedOld;
                     newInvalidated |= res.InvalidatedNew;
-                    combinedBounds = Rect.Union(combinedBounds, res.Bounds);
+                    combinedBounds = LtrbRect.FullUnion(combinedBounds, res.Bounds);
                 }
             }
             
@@ -63,7 +63,7 @@ namespace Avalonia.Rendering.Composition.Server
             return new(_transformedContentBounds, oldInvalidated, newInvalidated);
         }
 
-        void AddEffectPaddedDirtyRect(IImmutableEffect effect, Rect transformedBounds)
+        void AddEffectPaddedDirtyRect(IImmutableEffect effect, LtrbRect transformedBounds)
         {
             var padding = effect.GetEffectOutputPadding();
             if (padding == default)

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

@@ -27,7 +27,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
 #endif
     }
 
-    public override Rect OwnContentBounds => _renderCommands?.Bounds ?? default;
+    public override LtrbRect OwnContentBounds => _renderCommands?.Bounds ?? default;
 
     protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
     {
@@ -40,7 +40,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
         base.DeserializeChangesCore(reader, committedAt);
     }
 
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip,
         IDirtyRectTracker dirtyRects)
     {
         if (_renderCommands != null)

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

@@ -5,7 +5,7 @@ namespace Avalonia.Rendering.Composition.Server;
 
 internal partial class ServerCompositionExperimentalAcrylicVisual
 {
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip,
         IDirtyRectTracker dirtyRects)
     {
         var cornerRadius = CornerRadius;
@@ -19,7 +19,7 @@ internal partial class ServerCompositionExperimentalAcrylicVisual
         base.RenderCore(canvas, currentTransformedClip, dirtyRects);
     }
 
-    public override Rect OwnContentBounds => new(0, 0, Size.X, Size.Y);
+    public override LtrbRect OwnContentBounds => new(0, 0, Size.X, Size.Y);
 
     public ServerCompositionExperimentalAcrylicVisual(ServerCompositor compositor, Visual v) : base(compositor, v)
     {

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

@@ -1,10 +1,11 @@
 using Avalonia.Media.Immutable;
+using Avalonia.Platform;
 
 namespace Avalonia.Rendering.Composition.Server;
 
 internal partial class ServerCompositionSolidColorVisual
 {
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect 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

@@ -1,10 +1,11 @@
+using Avalonia.Platform;
 using Avalonia.Utilities;
 
 namespace Avalonia.Rendering.Composition.Server;
 
 internal partial class ServerCompositionSurfaceVisual
 {
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip,
         IDirtyRectTracker dirtyRects)
     {
         if (Surface == null)

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

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Collections.Pooled;
+using Avalonia.Platform;
 
 namespace Avalonia.Rendering.Composition.Server;
 
@@ -8,27 +9,26 @@ internal partial class ServerCompositionTarget
 {
     public readonly IDirtyRectTracker DirtyRects;
     
-    public void AddDirtyRect(Rect rect)
+    public void AddDirtyRect(LtrbRect rect)
     {
-        if (rect.Width == 0 && rect.Height == 0)
+        if (rect.IsZeroSize)
             return;
-        var snapped = PixelRect.FromRect(SnapToDevicePixels(rect, Scaling), 1);
-        DebugEvents?.RectInvalidated(rect);
+        var snapped = LtrbPixelRect.FromRectWithNoScaling(SnapToDevicePixels(rect, Scaling));
+        DebugEvents?.RectInvalidated(rect.ToRect());
         DirtyRects.AddRect(snapped);
         _redrawRequested = true;
     }
-    
-    public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
+
+    public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(new(rect), Scaling).ToRect();
+    public LtrbRect SnapToDevicePixels(LtrbRect rect) => SnapToDevicePixels(rect, Scaling);
         
-    private static Rect SnapToDevicePixels(Rect rect, double scale)
+    private static LtrbRect SnapToDevicePixels(LtrbRect 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));
+        return new LtrbRect(
+            Math.Floor(rect.Left * scale) / scale,
+            Math.Floor(rect.Top * scale) / scale,
+            Math.Ceiling(rect.Right * scale) / scale,
+            Math.Ceiling(rect.Bottom * scale) / scale);
     }
     
     

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

@@ -158,7 +158,7 @@ namespace Avalonia.Rendering.Composition.Server
                     _layer = null;
                     _layer = renderTargetContext.CreateLayer(PixelSize);
                     _layerSize = PixelSize;
-                    DirtyRects.AddRect(new PixelRect(_layerSize));
+                    DirtyRects.AddRect(new LtrbPixelRect(_layerSize));
                 }
                 else if (!needLayer)
                 {
@@ -168,7 +168,7 @@ namespace Avalonia.Rendering.Composition.Server
 
                 if (_fullRedrawRequested || (!needLayer && !properties.PreviousFrameIsRetained))
                 {
-                    DirtyRects.AddRect(new PixelRect(_layerSize));
+                    DirtyRects.AddRect(new LtrbPixelRect(_layerSize));
                     _fullRedrawRequested = false;
                 }
 
@@ -212,7 +212,7 @@ namespace Avalonia.Rendering.Composition.Server
             {
                 context.Clear(Colors.Transparent);
                 if (useLayerClip)
-                    context.PushLayer(DirtyRects.CombinedRect.ToRect(1));
+                    context.PushLayer(DirtyRects.CombinedRect.ToRectUnscaled());
 
 
                 root.Render(new CompositorDrawingContextProxy(context), null, DirtyRects);

+ 31 - 30
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@@ -17,17 +17,17 @@ namespace Avalonia.Rendering.Composition.Server
     partial class ServerCompositionVisual : ServerObject
     {
         private bool _isDirtyForUpdate;
-        private Rect _oldOwnContentBounds;
+        private LtrbRect _oldOwnContentBounds;
         private bool _isBackface;
-        private Rect? _transformedClipBounds;
-        private Rect _combinedTransformedClipBounds;
+        private LtrbRect? _transformedClipBounds;
+        private LtrbRect _combinedTransformedClipBounds;
 
-        protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+        protected virtual void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip,
             IDirtyRectTracker dirtyRects)
         {
         }
 
-        public void Render(CompositorDrawingContextProxy canvas, Rect? parentTransformedClip, IDirtyRectTracker dirtyRects)
+        public void Render(CompositorDrawingContextProxy canvas, LtrbRect? parentTransformedClip, IDirtyRectTracker dirtyRects)
         {
             if (Visible == false || IsVisibleInFrame == false)
                 return;
@@ -37,7 +37,7 @@ namespace Avalonia.Rendering.Composition.Server
             var currentTransformedClip = parentTransformedClip.HasValue
                 ? parentTransformedClip.Value.Intersect(_combinedTransformedClipBounds)
                 : _combinedTransformedClipBounds;
-            if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0)
+            if (currentTransformedClip.IsZeroSize)
                 return;
             if(!dirtyRects.Intersects(currentTransformedClip))
                 return;
@@ -52,7 +52,7 @@ namespace Avalonia.Rendering.Composition.Server
                 canvas.PostTransform = Matrix.Identity;
                 canvas.Transform = Matrix.Identity;
                 if (AdornerIsClipped)
-                    canvas.PushClip(AdornedVisual._combinedTransformedClipBounds);
+                    canvas.PushClip(AdornedVisual._combinedTransformedClipBounds.ToRect());
             }
             var transform = GlobalTransformMatrix;
             canvas.PostTransform = transform;
@@ -68,7 +68,7 @@ namespace Avalonia.Rendering.Composition.Server
             if (Opacity != 1)
                 canvas.PushOpacity(Opacity, ClipToBounds ? boundsRect : null);
             if (ClipToBounds && !HandlesClipToBounds)
-                canvas.PushClip(Root!.SnapToDevicePixels(boundsRect));
+                canvas.PushClip(boundsRect);
             if (Clip != null) 
                 canvas.PushGeometryClip(Clip);
             if (OpacityMaskBrush != null)
@@ -117,7 +117,7 @@ namespace Avalonia.Rendering.Composition.Server
         public Matrix CombinedTransformMatrix { get; private set; } = Matrix.Identity;
         public Matrix GlobalTransformMatrix { get; private set; }
 
-        public record struct UpdateResult(Rect? Bounds, bool InvalidatedOld, bool InvalidatedNew)
+        public record struct UpdateResult(LtrbRect? Bounds, bool InvalidatedOld, bool InvalidatedNew)
         {
             public UpdateResult() : this(null, false, false)
             {
@@ -178,7 +178,7 @@ namespace Avalonia.Rendering.Composition.Server
             if (ownBounds != _oldOwnContentBounds || positionChanged)
             {
                 _oldOwnContentBounds = ownBounds;
-                if (ownBounds.Width == 0 && ownBounds.Height == 0)
+                if (ownBounds.IsZeroSize)
                     TransformedOwnContentBounds = default;
                 else
                     TransformedOwnContentBounds =
@@ -187,22 +187,23 @@ namespace Avalonia.Rendering.Composition.Server
 
             if (_clipSizeDirty || positionChanged)
             {
-                Rect? transformedVisualBounds = null;
-                Rect? transformedClipBounds = null;
-                
+                LtrbRect? transformedVisualBounds = null;
+                LtrbRect? transformedClipBounds = null;
+
                 if (ClipToBounds)
-                    transformedVisualBounds = new Rect(new Size(Size.X, Size.Y)).TransformToAABB(GlobalTransformMatrix);
-                
-                 if (Clip != null)
-                     transformedClipBounds = Clip.Bounds.TransformToAABB(GlobalTransformMatrix);
-
-                 if (transformedVisualBounds != null && transformedClipBounds != null)
-                     _transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value);
-                 else if (transformedVisualBounds != null)
-                     _transformedClipBounds = transformedVisualBounds;
-                 else if (transformedClipBounds != null)
-                     _transformedClipBounds = transformedClipBounds;
-                 else
+                    transformedVisualBounds =
+                        new LtrbRect(0, 0, Size.X, Size.Y).TransformToAABB(GlobalTransformMatrix);
+
+                if (Clip != null)
+                    transformedClipBounds = new LtrbRect(Clip.Bounds).TransformToAABB(GlobalTransformMatrix);
+
+                if (transformedVisualBounds != null && transformedClipBounds != null)
+                    _transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value);
+                else if (transformedVisualBounds != null)
+                    _transformedClipBounds = transformedVisualBounds;
+                else if (transformedClipBounds != null)
+                    _transformedClipBounds = transformedClipBounds;
+                else
                      _transformedClipBounds = null;
                  
                 _clipSizeDirty = false;
@@ -211,7 +212,7 @@ namespace Avalonia.Rendering.Composition.Server
             _combinedTransformedClipBounds =
                 (AdornerIsClipped ? AdornedVisual?._combinedTransformedClipBounds : null)
                 ?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null)
-                ?? new Rect(Root!.PixelSize.ToSize(1));
+                ?? new LtrbRect(0, 0, Root!.PixelSize.Width, Root!.PixelSize.Height);
 
             if (_transformedClipBounds != null)
                 _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
@@ -221,7 +222,7 @@ namespace Avalonia.Rendering.Composition.Server
             IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
                                       && Visible
                                       && !_isBackface
-                                      && (_combinedTransformedClipBounds.Width != 0 || _combinedTransformedClipBounds.Height != 0);
+                                      && !(_combinedTransformedClipBounds.IsZeroSize);
 
             IsVisibleInFrame = IsHitTestVisibleInFrame
                                && _parent?.IsVisibleInFrame != false
@@ -253,7 +254,7 @@ namespace Avalonia.Rendering.Composition.Server
             return new(TransformedOwnContentBounds, invalidateNewBounds, invalidateOldBounds);
         }
 
-        protected void AddDirtyRect(Rect rc)
+        protected void AddDirtyRect(LtrbRect rc)
         {
             if (rc == default)
                 return;
@@ -311,7 +312,7 @@ namespace Avalonia.Rendering.Composition.Server
         public bool IsVisibleInFrame { get; set; }
         public bool IsHitTestVisibleInFrame { get; set; }
         public double EffectiveOpacity { get; set; }
-        public Rect TransformedOwnContentBounds { get; set; }
-        public virtual Rect OwnContentBounds => new Rect(0, 0, Size.X, Size.Y);
+        public LtrbRect TransformedOwnContentBounds { get; set; }
+        public virtual LtrbRect OwnContentBounds => new (0, 0, Size.X, Size.Y);
     }
 }

+ 5 - 4
src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Numerics;
 using Avalonia.Logging;
 using Avalonia.Media;
+using Avalonia.Platform;
 using Avalonia.Rendering.Composition.Transport;
 
 namespace Avalonia.Rendering.Composition.Server;
@@ -41,7 +42,7 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
             Compositor.Animations.RemoveFromClock(this);
     }
 
-    public override Rect OwnContentBounds => _handler.GetRenderBounds();
+    public override LtrbRect OwnContentBounds => new(_handler.GetRenderBounds());
 
     protected override void OnAttachedToRoot(ServerCompositionTarget target)
     {
@@ -60,7 +61,7 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
 
     internal void HandlerInvalidate(Rect rc)
     {
-        Root?.AddDirtyRect(rc.TransformToAABB(GlobalTransformMatrix));
+        Root?.AddDirtyRect(new LtrbRect(rc).TransformToAABB(GlobalTransformMatrix));
     }
     
     internal void HandlerRegisterForNextAnimationFrameUpdate()
@@ -70,13 +71,13 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
             Compositor.Animations.AddToClock(this);
     }
 
-    protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+    protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip,
         IDirtyRectTracker dirtyRects)
     {
         using var context = new ImmediateDrawingContext(canvas, false);
         try
         {
-            _handler.Render(context, currentTransformedClip);
+            _handler.Render(context, currentTransformedClip.ToRect());
         }
         catch (Exception e)
         {

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

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Platform;
 using Avalonia.Rendering.Composition;
 using Avalonia.Rendering.Composition.Server;
 using Avalonia.Rendering.Composition.Transport;
@@ -45,7 +46,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
         {
         }
 
-        protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
+        protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip,
             IDirtyRectTracker dirtyRects)
         {
             if (ClipToBounds)

+ 2 - 2
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -545,14 +545,14 @@ namespace Avalonia.Skia
             
             if (brush != null)
             {
-                using (var fill = CreatePaint(_fillPaint, brush, r.Bounds.ToRect(1)))
+                using (var fill = CreatePaint(_fillPaint, brush, r.Bounds.ToRectUnscaled()))
                 {
                     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)
+                && TryCreatePaint(_strokePaint, pen, r.Bounds.ToRectUnscaled().Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
             {
                 using (stroke)
                 {

+ 10 - 7
src/Skia/Avalonia.Skia/SkiaRegionImpl.cs

@@ -10,17 +10,17 @@ internal class SkiaRegionImpl : IPlatformRenderInterfaceRegion
     private SKRegion? _region = new();
     public SKRegion Region => _region ?? throw new ObjectDisposedException(nameof(SkiaRegionImpl));
     private bool _rectsValid;
-    private List<PixelRect>? _rects;
+    private List<LtrbPixelRect>? _rects;
     public void Dispose()
     {
         _region?.Dispose();
         _region = null;
     }
 
-    public void AddRect(PixelRect rect)
+    public void AddRect(LtrbPixelRect rect)
     {
         _rectsValid = false;
-        Region.Op(rect.X, rect.Y, rect.Right, rect.Bottom, SKRegionOperation.Union);
+        Region.Op(rect.Left, rect.Top, rect.Right, rect.Bottom, SKRegionOperation.Union);
     }
 
     public void Reset()
@@ -30,9 +30,9 @@ internal class SkiaRegionImpl : IPlatformRenderInterfaceRegion
     }
 
     public bool IsEmpty => Region.IsEmpty;
-    public PixelRect Bounds => Region.Bounds.ToAvaloniaPixelRect();
+    public LtrbPixelRect Bounds => Region.Bounds.ToAvaloniaLtrbPixelRect();
 
-    public IList<PixelRect> Rects
+    public IList<LtrbPixelRect> Rects
     {
         get
         {
@@ -42,12 +42,15 @@ internal class SkiaRegionImpl : IPlatformRenderInterfaceRegion
                 _rects.Clear();
                 using var iter = Region.CreateRectIterator();
                 while (iter.Next(out var rc))
-                    _rects.Add(rc.ToAvaloniaPixelRect());
+                    _rects.Add(rc.ToAvaloniaLtrbPixelRect());
             }
             return _rects;
         }
     }
 
-    public bool Intersects(Rect rect) => Region.Intersects(PixelRect.FromRect(rect, 1).ToSKRectI());
+    public bool Intersects(LtrbRect rect) => Region.Intersects(
+        new SKRectI((int)rect.Left, (int)rect.Top,
+            (int)Math.Ceiling(rect.Right), (int)Math.Ceiling(rect.Bottom)));
+    
     public bool Contains(Point pt) => Region.Contains((int)pt.X, (int)pt.Y);
 }

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

@@ -76,10 +76,20 @@ namespace Avalonia.Skia
             return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom);
         }
         
+        internal static SKRect ToSKRect(this LtrbRect r)
+        {
+            return new SKRect((float)r.Left, (float)r.Right, (float)r.Right, (float)r.Bottom);
+        }
+        
         public static SKRectI ToSKRectI(this PixelRect r)
         {
             return new SKRectI(r.X, r.Y, r.Right, r.Bottom);
         }
+        
+        internal static SKRectI ToSKRectI(this LtrbPixelRect r)
+        {
+            return new SKRectI(r.Left, r.Top, r.Right, r.Bottom);
+        }
 
         public static SKRoundRect ToSKRoundRect(this RoundedRect r)
         {
@@ -101,10 +111,20 @@ namespace Avalonia.Skia
             return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
         }
         
+        internal static LtrbRect ToAvaloniaLtrbRect(this SKRect r)
+        {
+            return new LtrbRect(r.Left, r.Top, r.Right, r.Bottom);
+        }
+        
         public static PixelRect ToAvaloniaPixelRect(this SKRectI r)
         {
             return new PixelRect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
         }
+        
+        internal static LtrbPixelRect ToAvaloniaLtrbPixelRect(this SKRectI r)
+        {
+            return new LtrbPixelRect(r.Left, r.Top, r.Right, r.Bottom);
+        }
 
         public static SKMatrix ToSKMatrix(this Matrix m)
         {

+ 1 - 1
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

@@ -40,7 +40,7 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
                 if (renderData == null)
                     return null;
                 ForceRender();
-                return renderData.Server.Bounds;
+                return renderData.Server.Bounds?.ToRect();
             }
         }