From 5b59f6c0bcdafade4d1ce0f2a5019f306d7a605d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 29 Mar 2024 03:08:02 +0500 Subject: [PATCH] Use separate LtrbRect type for renderer calculations (#15112) --- .../IPlatformRenderInterfaceRegion.cs | 8 +- src/Avalonia.Base/Platform/LtrbRect.cs | 268 ++++++++++++++++++ .../CompositionCustomVisualHandler.cs | 2 +- .../CompositionRenderDataSceneBrushContent.cs | 2 +- .../Drawing/ServerCompositionRenderData.cs | 22 +- .../Composition/Server/DirtyRectTracker.cs | 42 +-- .../ServerCompositionContainerVisual.cs | 8 +- .../Server/ServerCompositionDrawListVisual.cs | 4 +- ...verCompositionExperimentalAcrylicVisual.cs | 4 +- .../ServerCompositionSolidColorVisual.cs | 3 +- .../Server/ServerCompositionSurfaceVisual.cs | 3 +- .../ServerCompositionTarget.DirtyRects.cs | 28 +- .../Server/ServerCompositionTarget.cs | 6 +- .../Server/ServerCompositionVisual.cs | 61 ++-- .../Server/ServerCustomCompositionVisual.cs | 9 +- src/Avalonia.Controls/BorderVisual.cs | 3 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 4 +- src/Skia/Avalonia.Skia/SkiaRegionImpl.cs | 17 +- src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs | 20 ++ .../SceneGraph/DrawOperationTests.cs | 2 +- 20 files changed, 410 insertions(+), 106 deletions(-) create mode 100644 src/Avalonia.Base/Platform/LtrbRect.cs diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs index 682609391c..2692c1c67e 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs +++ b/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 Rects { get; } - bool Intersects(Rect rect); + LtrbPixelRect Bounds { get; } + IList Rects { get; } + bool Intersects(LtrbRect rect); bool Contains(Point pt); } \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/LtrbRect.cs b/src/Avalonia.Base/Platform/LtrbRect.cs new file mode 100644 index 0000000000..188f75c010 --- /dev/null +++ b/src/Avalonia.Base/Platform/LtrbRect.cs @@ -0,0 +1,268 @@ +// ReSharper disable CompareOfFloatsByEqualityOperator + +using System; +using Avalonia.Metadata; + +namespace Avalonia.Platform; + +/// +/// 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 +/// +[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 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); + } + + /// + /// Perform _WPF-like_ union operation + /// + 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; + } + } +} + +/// +/// 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 +/// +[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)); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs index d508d2e860..8cf4cca0a5 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs +++ b/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)); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs index 22a871a98b..4a46b07294 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs +++ b/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; diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs index 97083ccaf5..3745fb54d8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs @@ -14,7 +14,7 @@ class ServerCompositionRenderData : SimpleServerRenderResource { private PooledInlineList _items; private PooledInlineList _referencedResources; - private Rect? _bounds; + private LtrbRect? _bounds; private bool _boundsValid; private static readonly ThreadSafeObjectPool 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; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs b/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs index e9b4071d79..0861384c2f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs +++ b/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 Rects { get; } + LtrbPixelRect CombinedRect { get; } + IList 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 Rects + public IList Rects { get { - if (_rect.Width == 0 || _rect.Height == 0) - return Array.Empty(); + if (_rect.IsEmpty) + return Array.Empty(); _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 Rects => _region.Rects; + public LtrbPixelRect CombinedRect => _region.Bounds; + public IList Rects => _region.Rects; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index 6e6ce31206..6071d7a5f2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/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) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index bf6b71bbd2..060ba2c820 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/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) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs index cd0c8d1092..20acf87d84 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs +++ b/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) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs index ad3b62805f..e38ae57c57 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs +++ b/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)); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs index 5f9936874f..d4b1662bd9 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs +++ b/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) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs index 317452e658..c4d721c733 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs +++ b/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); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index b94829312d..9eef4bd7e2 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/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); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 65f6df6d9b..87243f4024 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/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); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs index 773812773e..68b2823c67 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs +++ b/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) { diff --git a/src/Avalonia.Controls/BorderVisual.cs b/src/Avalonia.Controls/BorderVisual.cs index 85e3a64df3..36a557d5ed 100644 --- a/src/Avalonia.Controls/BorderVisual.cs +++ b/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) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 39a11ede14..0c5cd4382f 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/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) { diff --git a/src/Skia/Avalonia.Skia/SkiaRegionImpl.cs b/src/Skia/Avalonia.Skia/SkiaRegionImpl.cs index 317cd932aa..24bd3c7569 100644 --- a/src/Skia/Avalonia.Skia/SkiaRegionImpl.cs +++ b/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? _rects; + private List? _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 Rects + public IList 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); } \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index 273308cb57..f35fb1e5eb 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/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) { diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 1d55534fca..e9a1cc382d 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/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(); } }