From aa1fcb4871ce71d55fe33cfd6be3f7e0c67ae2ef Mon Sep 17 00:00:00 2001 From: Matt <5415177+ZehMatt@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:17:51 +0300 Subject: [PATCH] Refactor per-draw render data allocations with a binary opcode stream (#21366) * Add render data opcode enum First step of the Drawing/Nodes binary stream refactor. * Add render data resource table * Add render data stream writer and reader Encodes the opcode stream field-by-field via BinaryPrimitives to avoid unsafe and blittability assumptions. * Add RenderDataStream with recording and replay * Add hit-test walker to RenderDataStream * Add bounds walker to RenderDataStream * Add render data stream serialization * Switch render data to the binary stream * Move node level test coverage to the stream * Delete the render data node classes * Small optimization, stackalloc the render data walker scope stacks * Optimize serialization for blittable types * Encode render data via payload structs, add render data visitor * Rename RenderDataResources.Add to AppendDeserialized * Add effect support to render data stream * Simplify render data read/write with MemoryMarshal --- .../Drawing/CompositionRenderData.cs | 50 +-- .../Composition/Drawing/IRenderDataVisitor.cs | 30 ++ .../ImmediateRenderDataSceneBrushContent.cs | 54 +-- .../Drawing/Nodes/RenderDataBitmapNode.cs | 28 -- .../Drawing/Nodes/RenderDataEllipseNode.cs | 61 --- .../Drawing/Nodes/RenderDataGeometryNode.cs | 29 -- .../Drawing/Nodes/RenderDataGlyphRunNode.cs | 35 -- .../Drawing/Nodes/RenderDataLineNode.cs | 65 --- .../Drawing/Nodes/RenderDataNodes.cs | 294 -------------- .../Drawing/Nodes/RenderDataPushMatrixNode.cs | 27 -- .../Nodes/RenderDataPushOpacityMaskNode.cs | 25 -- .../Drawing/Nodes/RenderDataRectangleNode.cs | 46 --- .../Drawing/RenderDataDrawingContext.cs | 370 ++++++++---------- .../Composition/Drawing/RenderDataOpcode.cs | 22 ++ .../Composition/Drawing/RenderDataPayloads.cs | 132 +++++++ .../Composition/Drawing/RenderDataReader.cs | 42 ++ .../Drawing/RenderDataResources.cs | 54 +++ .../Drawing/RenderDataStream.Bounds.cs | 93 +++++ .../Drawing/RenderDataStream.HitTest.cs | 250 ++++++++++++ .../Drawing/RenderDataStream.Replay.cs | 159 ++++++++ .../Drawing/RenderDataStream.Visit.cs | 177 +++++++++ .../Composition/Drawing/RenderDataStream.cs | 226 +++++++++++ .../Composition/Drawing/RenderDataWriter.cs | 57 +++ .../Drawing/ServerCompositionRenderData.cs | 94 ++--- .../Composition/Transport/BatchStream.cs | 46 +++ .../Rendering/RenderDataEffectNodeTests.cs | 50 --- .../SceneGraph/DrawOperationTests.cs | 76 ---- .../Rendering/SceneGraph/LineNodeTests.cs | 67 ---- .../SceneGraph/RenderDataResourcesTests.cs | 103 +++++ .../SceneGraph/RenderDataStreamBoundsTests.cs | 177 +++++++++ .../SceneGraph/RenderDataStreamEffectTests.cs | 29 ++ ...=> RenderDataStreamEllipseHitTestTests.cs} | 31 +- .../RenderDataStreamHitTestTests.cs | 208 ++++++++++ .../RenderDataStreamLineHitTestTests.cs | 55 +++ .../RenderDataStreamSerializationTests.cs | 98 +++++ .../SceneGraph/RenderDataStreamTests.cs | 228 +++++++++++ .../SceneGraph/RenderDataWriterReaderTests.cs | 237 +++++++++++ 37 files changed, 2642 insertions(+), 1183 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/IRenderDataVisitor.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataOpcode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataPayloads.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataReader.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataResources.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Bounds.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.HitTest.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Replay.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Visit.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataWriter.cs delete mode 100644 tests/Avalonia.Base.UnitTests/Rendering/RenderDataEffectNodeTests.cs delete mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataResourcesTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamBoundsTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEffectTests.cs rename tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/{EllipseNodeTests.cs => RenderDataStreamEllipseHitTestTests.cs} (53%) create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamHitTestTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamLineHitTestTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamSerializationTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamTests.cs create mode 100644 tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataWriterReaderTests.cs diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs index 0ad70d7102..feb0a93f61 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Rendering.Composition.Drawing.Nodes; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; using Avalonia.Utilities; @@ -12,36 +8,33 @@ namespace Avalonia.Rendering.Composition.Drawing; internal class CompositionRenderData : ICompositorSerializable, IDisposable { private readonly Compositor _compositor; + private readonly RenderDataStream _stream; + private PooledInlineList _resources; + private bool _itemsSent; - public CompositionRenderData(Compositor compositor) + public CompositionRenderData(Compositor compositor, RenderDataStream stream) { _compositor = compositor; + _stream = stream; Server = new ServerCompositionRenderData(compositor.Server); } public ServerCompositionRenderData Server { get; } - private PooledInlineList _resources; - private PooledInlineList _items; - private bool _itemsSent; + public void AddResource(ICompositionRenderResource resource) => _resources.Add(resource); - public void Add(IRenderDataItem item) => _items.Add(item); - public void Dispose() { if (!_itemsSent) - { - foreach(var i in _items) - if (i is IDisposable disp) - disp.Dispose(); - } - - _items.Dispose(); - _itemsSent = false; - foreach(var r in _resources) + _stream.DisposeResources(); + + foreach (var r in _resources) r.ReleaseOnCompositor(_compositor); _resources.Dispose(); - + + _stream.Dispose(); + _itemsSent = false; + _compositor.DisposeOnNextBatch(Server); } @@ -49,20 +42,9 @@ internal class CompositionRenderData : ICompositorSerializable, IDisposable public void SerializeChanges(Compositor c, BatchStreamWriter writer) { - writer.Write(_items.Count); - foreach (var item in _items) - writer.WriteObject(item); + _stream.SerializeTo(writer); _itemsSent = true; } - public bool HitTest(Point pt) - { - foreach (var op in _items) - { - if (op.HitTest(pt)) - return true; - } - - return false; - } -} \ No newline at end of file + public bool HitTest(Point pt) => _stream.HitTest(pt); +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/IRenderDataVisitor.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/IRenderDataVisitor.cs new file mode 100644 index 0000000000..3f5d4bc5e4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/IRenderDataVisitor.cs @@ -0,0 +1,30 @@ +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal interface IRenderDataVisitor where TScope : unmanaged +{ + bool StopVisiting { get; } + + void OnDrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2); + void OnDrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, BoxShadows boxShadows); + void OnDrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect); + void OnDrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry); + void OnDrawGlyphRun(IBrush? serverBrush, IRef? glyphRun); + void OnDrawBitmap(IRef? bitmap, double opacity, Rect sourceRect, Rect destRect); + void OnDrawCustom(ICustomDrawOperation? operation); + + TScope OnPushClip(RoundedRect clip); + TScope OnPushGeometryClip(IGeometryImpl? geometry); + TScope OnPushOpacity(double opacity); + TScope OnPushOpacityMask(IBrush? brush, Rect bounds); + TScope OnPushTransform(Matrix matrix); + TScope OnPushRenderOptions(RenderOptions options); + TScope OnPushTextOptions(TextOptions options); + TScope OnPushEffect(IEffect? effect, Rect bounds); + void OnPop(in TScope scope); +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs index 00f75ec8f8..e3b43655bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs @@ -1,32 +1,19 @@ -using System; -using System.Collections.Generic; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Rendering.Composition.Drawing.Nodes; -using Avalonia.Threading; namespace Avalonia.Rendering.Composition.Drawing; internal class ImmediateRenderDataSceneBrushContent : ISceneBrushContent { - private List? _items; - private readonly ThreadSafeObjectPool> _pool; + private RenderDataStream? _stream; - public ImmediateRenderDataSceneBrushContent(ITileBrush brush, List items, Rect? rect, - bool useScalableRasterization, ThreadSafeObjectPool> pool) + public ImmediateRenderDataSceneBrushContent(ITileBrush brush, RenderDataStream stream, Rect? rect, + bool useScalableRasterization) { Brush = brush; - _items = items; - _pool = pool; + _stream = stream; UseScalableRasterization = useScalableRasterization; - if (rect == null) - { - foreach (var i in _items) - rect = Rect.Union(rect, i.Bounds); - rect = ServerCompositionRenderData.ApplyRenderBoundsRounding(rect); - } - - Rect = rect ?? default; + Rect = rect ?? ServerCompositionRenderData.ApplyRenderBoundsRounding(stream.CalculateBounds()) ?? default; } public ITileBrush Brush { get; } @@ -38,31 +25,15 @@ internal class ImmediateRenderDataSceneBrushContent : ISceneBrushContent public void Dispose() { - if(_items == null) + if (_stream == null) return; - foreach (var i in _items) - (i as IDisposable)?.Dispose(); - _items.Clear(); - _pool.ReturnAndSetNull(ref _items); + _stream.DisposeResources(); + _stream.Dispose(); + _stream = null; } - void Render(IDrawingContextImpl context) - { - if (_items == null) - return; - - var ctx = new RenderDataNodeRenderContext(context); - try - { - foreach (var i in _items) - i.Invoke(ref ctx); - } - finally - { - ctx.Dispose(); - } - } - + private void Render(IDrawingContextImpl context) => _stream?.Replay(context); + public void Render(IDrawingContextImpl context, Matrix? transform) { if (transform.HasValue) @@ -77,5 +48,4 @@ internal class ImmediateRenderDataSceneBrushContent : ISceneBrushContent } public bool UseScalableRasterization { get; } - -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs deleted file mode 100644 index 11a1b87fc9..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using Avalonia.Platform; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -class RenderDataBitmapNode : IRenderDataItem, IDisposable -{ - public IRef? Bitmap { get; set; } - public double Opacity { get; set; } - public Rect SourceRect { get; set; } - public Rect DestRect { get; set; } - - public bool HitTest(Point p) => DestRect.Contains(p); - - public void Invoke(ref RenderDataNodeRenderContext context) - { - if (Bitmap != null) - context.Context.DrawBitmap(Bitmap.Item, Opacity, SourceRect, DestRect); - } - - public Rect? Bounds => DestRect; - public void Dispose() - { - Bitmap?.Dispose(); - Bitmap = null; - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs deleted file mode 100644 index 4e77281cc5..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -class RenderDataEllipseNode :RenderDataBrushAndPenNode -{ - public Rect Rect { get; set; } - - bool Contains(double dx, double dy, double radiusX, double radiusY) - { - var rx2 = radiusX * radiusX; - var ry2 = radiusY * radiusY; - - var distance = ry2 * dx * dx + rx2 * dy * dy; - - return distance <= rx2 * ry2; - } - - public override bool HitTest(Point p) - { - var center = Rect.Center; - - var strokeThickness = ClientPen?.Thickness ?? 0; - - var rx = Rect.Width / 2 + strokeThickness / 2; - var ry = Rect.Height / 2 + strokeThickness / 2; - - var dx = p.X - center.X; - var dy = p.Y - center.Y; - - if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) - { - return false; - } - - if (ServerBrush != null) - { - return Contains(dx, dy, rx, ry); - } - else if (strokeThickness > 0) - { - bool inStroke = Contains(dx, dy, rx, ry); - - rx = Rect.Width / 2 - strokeThickness / 2; - ry = Rect.Height / 2 - strokeThickness / 2; - - bool inInner = Contains(dx, dy, rx, ry); - - return inStroke && !inInner; - } - - return false; - } - - public override void Invoke(ref RenderDataNodeRenderContext context) => - context.Context.DrawEllipse(ServerBrush, ServerPen, Rect); - - public override Rect? Bounds => Rect.Inflate(ServerPen?.Thickness ?? 0); -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs deleted file mode 100644 index 5a40a6b279..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Diagnostics; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; - -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -class RenderDataGeometryNode : RenderDataBrushAndPenNode -{ - public IGeometryImpl? Geometry { get; set; } - - public override bool HitTest(Point p) - { - if (Geometry == null) - return false; - - return (ServerBrush != null // null check is safe - && Geometry.FillContains(p)) || - (ClientPen != null && Geometry.StrokeContains(ClientPen, p)); - } - - public override void Invoke(ref RenderDataNodeRenderContext context) - { - Debug.Assert(Geometry != null); - context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!); - } - - public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen) ?? default; -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs deleted file mode 100644 index ca2748cd55..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Diagnostics; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -class RenderDataGlyphRunNode : IRenderDataItemWithServerResources, IDisposable -{ - public IBrush? ServerBrush { get; set; } - // Dispose only happens once, so it's safe to have one reference - public IRef? GlyphRun { get; set; } - - public bool HitTest(Point p) => GlyphRun?.Item.Bounds.ContainsExclusive(p) ?? false; - - public void Invoke(ref RenderDataNodeRenderContext context) - { - Debug.Assert(GlyphRun!.Item != null); - context.Context.DrawGlyphRun(ServerBrush, GlyphRun.Item); - } - - public Rect? Bounds => GlyphRun?.Item?.Bounds ?? default; - - public void Collect(IRenderDataServerResourcesCollector collector) - { - collector.AddRenderDataServerResource(ServerBrush); - } - - public void Dispose() - { - GlyphRun?.Dispose(); - GlyphRun = null; - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs deleted file mode 100644 index 6b6b9bdb01..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; - -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -class RenderDataLineNode : IRenderDataItemWithServerResources -{ - public IPen? ServerPen { get; set; } - public IPen? ClientPen { get; set; } - public Point P1 { get; set; } - public Point P2 { get; set; } - - public bool HitTest(Point p) - { - if (ClientPen == null) - return false; - var halfThickness = ClientPen.Thickness / 2; - var minX = Math.Min(P1.X, P2.X) - halfThickness; - var maxX = Math.Max(P1.X, P2.X) + halfThickness; - var minY = Math.Min(P1.Y, P2.Y) - halfThickness; - var maxY = Math.Max(P1.Y, P2.Y) + halfThickness; - - if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) - return false; - - var a = P1; - var b = P2; - - //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse. - //The distance from a point to a straight line is defined as the - //length of the vector formed by the point and the closest point of the segment - - Vector ap = p - a; - var dot1 = Vector.Dot(b - a, ap); - - if (dot1 < 0) - return ap.Length <= ClientPen.Thickness / 2; - - Vector bp = p - b; - var dot2 = Vector.Dot(a - b, bp); - - if (dot2 < 0) - return bp.Length <= halfThickness; - - var bXaX = b.X - a.X; - var bYaY = b.Y - a.Y; - - var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / - (Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); - - return Math.Abs(distance) <= halfThickness; - } - - - public void Invoke(ref RenderDataNodeRenderContext context) - => context.Context.DrawLine(ServerPen, P1, P2); - - public Rect? Bounds => LineBoundsHelper.CalculateBounds(P1, P2, ServerPen!); - public void Collect(IRenderDataServerResourcesCollector collector) - { - collector.AddRenderDataServerResource(ServerPen); - } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs deleted file mode 100644 index 52098411cd..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs +++ /dev/null @@ -1,294 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Threading; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -enum RenderDataPopNodeType -{ - Transform, - Clip, - GeometryClip, - Opacity, - OpacityMask, - Effect -} - -interface IRenderDataServerResourcesCollector -{ - void AddRenderDataServerResource(object? obj); -} - -interface IRenderDataItemWithServerResources : IRenderDataItem -{ - void Collect(IRenderDataServerResourcesCollector collector); -} - -struct RenderDataNodeRenderContext : IDisposable -{ - private Stack? _stack; - private static readonly ThreadSafeObjectPool> s_matrixStackPool = new(); - - public RenderDataNodeRenderContext(IDrawingContextImpl context) - { - Context = context; - } - public IDrawingContextImpl Context { get; } - - public Stack MatrixStack - { - get => _stack ??= s_matrixStackPool.Get(); - } - - public void Dispose() - { - if (_stack != null) - { - _stack.Clear(); - s_matrixStackPool.ReturnAndSetNull(ref _stack); - } - } -} - -interface IRenderDataItem -{ - /// - /// Renders the node to a drawing context. - /// - /// The drawing context. - void Invoke(ref RenderDataNodeRenderContext context); - - /// - /// Gets the bounds of the visible content in the node in global coordinates. - /// - Rect? Bounds { get; } - - /// - /// Hit test the geometry in this node. - /// - /// The point in global coordinates. - /// True if the point hits the node's geometry; otherwise false. - /// - /// This method does not recurse to childs, if you want - /// to hit test children they must be hit tested manually. - /// - bool HitTest(Point p); -} - -class RenderDataCustomNode : IRenderDataItem, IDisposable -{ - public ICustomDrawOperation? Operation { get; set; } - public bool HitTest(Point p) => Operation?.HitTest(p) ?? false; - public void Invoke(ref RenderDataNodeRenderContext context) => Operation?.Render(new(context.Context, false)); - - public Rect? Bounds => Operation?.Bounds; - - public void Dispose() - { - Operation?.Dispose(); - Operation = null; - } -} - -abstract class RenderDataPushNode : IRenderDataItem, IDisposable -{ - public PooledInlineList Children; - public abstract void Push(ref RenderDataNodeRenderContext context); - public abstract void Pop(ref RenderDataNodeRenderContext context); - public void Invoke(ref RenderDataNodeRenderContext context) - { - if (Children.Count == 0) - return; - Push(ref context); - foreach (var ch in Children) - ch.Invoke(ref context); - Pop(ref context); - } - - public virtual Rect? Bounds - { - get - { - if (Children.Count == 0) - return null; - Rect? union = null; - foreach (var i in Children) - union = Rect.Union(union, i.Bounds); - return union; - } - } - - public virtual bool HitTest(Point p) - { - if (Children.Count == 0) - return false; - foreach(var ch in Children) - if (ch.HitTest(p)) - return true; - return false; - } - - public void Dispose() - { - if (Children.Count > 0) - { - foreach(var ch in Children) - if (ch is IDisposable disposable) - disposable.Dispose(); - Children.Dispose(); - } - } -} - -class RenderDataClipNode : RenderDataPushNode -{ - public RoundedRect Rect { get; set; } - public override void Push(ref RenderDataNodeRenderContext context) => - context.Context.PushClip(Rect); - - public override void Pop(ref RenderDataNodeRenderContext context) => - context.Context.PopClip(); - - public override bool HitTest(Point p) - { - if (!Rect.Rect.Contains(p)) - return false; - return base.HitTest(p); - } -} - -class RenderDataGeometryClipNode : RenderDataPushNode -{ - public IGeometryImpl? Geometry { get; set; } - public bool Contains(Point p) => Geometry?.FillContains(p) ?? false; - - public override void Push(ref RenderDataNodeRenderContext context) - { - if (Geometry != null) - context.Context.PushGeometryClip(Geometry); - } - - public override void Pop(ref RenderDataNodeRenderContext context) - { - if (Geometry != null) - context.Context.PopGeometryClip(); - } - - public override bool HitTest(Point p) - { - if (Geometry != null && !Geometry.FillContains(p)) - return false; - return base.HitTest(p); - } -} - -class RenderDataOpacityNode : RenderDataPushNode -{ - public double Opacity { get; set; } - public override void Push(ref RenderDataNodeRenderContext context) - { - if (Opacity != 1) - context.Context.PushOpacity(Opacity, null); - } - - public override void Pop(ref RenderDataNodeRenderContext context) - { - if (Opacity != 1) - context.Context.PopOpacity(); - } -} - -abstract class RenderDataBrushAndPenNode : IRenderDataItemWithServerResources -{ - public IBrush? ServerBrush { get; set; } - public IPen? ServerPen { get; set; } - public IPen? ClientPen { get; set; } - - public void Collect(IRenderDataServerResourcesCollector collector) - { - collector.AddRenderDataServerResource(ServerBrush); - collector.AddRenderDataServerResource(ServerPen); - } - - public abstract void Invoke(ref RenderDataNodeRenderContext context); - public abstract Rect? Bounds { get; } - public abstract bool HitTest(Point p); -} - -class RenderDataRenderOptionsNode : RenderDataPushNode -{ - public RenderOptions RenderOptions { get; set; } - - public override void Push(ref RenderDataNodeRenderContext context) - { - context.Context.PushRenderOptions(RenderOptions); - } - - public override void Pop(ref RenderDataNodeRenderContext context) - { - context.Context.PopRenderOptions(); - } -} - -class RenderDataTextOptionsNode : RenderDataPushNode -{ - public TextOptions TextOptions { get; set; } - - public override void Push(ref RenderDataNodeRenderContext context) - { - context.Context.PushTextOptions(TextOptions); - } - - public override void Pop(ref RenderDataNodeRenderContext context) - { - context.Context.PopTextOptions(); - } -} - -/// -/// A render data node that pushes an effect. -/// -class RenderDataEffectNode : RenderDataPushNode -{ - /// - /// Gets or sets the effect to push. - /// - public IEffect? Effect { get; set; } - - /// - /// Gets or sets the bounds of the effect. - /// - public Rect BoundsRect { get; set; } - - /// - public override Rect? Bounds - { - get - { - var bounds = base.Bounds; - if (bounds is null) - return null; - return Effect is { } effect - ? bounds.Value.Inflate(effect.GetEffectOutputPadding()) - : bounds; - } - } - - /// - public override void Push(ref RenderDataNodeRenderContext context) - { - if (Effect != null && context.Context is IDrawingContextImplWithEffects effectImpl) - effectImpl.PushEffect(BoundsRect, Effect); - } - - /// - public override void Pop(ref RenderDataNodeRenderContext context) - { - if (Effect != null && context.Context is IDrawingContextImplWithEffects effectImpl) - effectImpl.PopEffect(); - } -} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs deleted file mode 100644 index 2ccfa0e7a5..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -class RenderDataPushMatrixNode : RenderDataPushNode -{ - public Matrix Matrix { get; set; } - - public override void Push(ref RenderDataNodeRenderContext context) - { - var current = context.Context.Transform; - context.MatrixStack.Push(current); - context.Context.Transform = Matrix * current; - } - - public override void Pop(ref RenderDataNodeRenderContext context) - { - context.Context.Transform = context.MatrixStack.Pop(); - } - - public override bool HitTest(Point p) - { - if (Matrix.TryInvert(out var inverted)) - return base.HitTest(p.Transform(inverted)); - return false; - } - - public override Rect? Bounds => base.Bounds?.TransformToAABB(Matrix); -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs deleted file mode 100644 index 14607282da..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -class RenderDataOpacityMaskNode : RenderDataPushNode, IRenderDataItemWithServerResources -{ - public IBrush? ServerBrush { get; set; } - - public Rect BoundsRect { get; set; } - - public void Collect(IRenderDataServerResourcesCollector collector) - { - collector.AddRenderDataServerResource(ServerBrush); - } - - public override void Push(ref RenderDataNodeRenderContext context) - { - if (ServerBrush != null) - context.Context.PushOpacityMask(ServerBrush, BoundsRect); - } - - public override void Pop(ref RenderDataNodeRenderContext context) => - context.Context.PopOpacityMask(); -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs deleted file mode 100644 index c6a3859e3d..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Avalonia.Media; - -namespace Avalonia.Rendering.Composition.Drawing.Nodes; - -class RenderDataRectangleNode : RenderDataBrushAndPenNode -{ - public RoundedRect Rect { get; set; } - public BoxShadows BoxShadows { get; set; } - - public override bool HitTest(Point p) - { - var strokeThicknessAdjustment = (ClientPen?.Thickness / 2) ?? 0; - - if (Rect.IsRounded) - { - var outerRoundedRect = Rect.Inflate(strokeThicknessAdjustment, strokeThicknessAdjustment); - if (outerRoundedRect.ContainsExclusive(p)) - { - if (ServerBrush != null) // it's safe to check for null - return true; - - var innerRoundedRect = Rect.Deflate(strokeThicknessAdjustment, strokeThicknessAdjustment); - return !innerRoundedRect.ContainsExclusive(p); - } - } - else - { - var outerRect = Rect.Rect.Inflate(strokeThicknessAdjustment); - if (outerRect.ContainsExclusive(p)) - { - if (ServerBrush != null) // it's safe to check for null - return true; - - var innerRect = Rect.Rect.Deflate(strokeThicknessAdjustment); - return !innerRect.ContainsExclusive(p); - } - } - - return false; - } - - public override void Invoke(ref RenderDataNodeRenderContext context) => - context.Context.DrawRectangle(ServerBrush, ServerPen, Rect, BoxShadows); - - public override Rect? Bounds => BoxShadows.TransformBounds(Rect.Rect).Inflate((ServerPen?.Thickness ?? 0) / 2); -} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs index 28370d71e1..2472d69f08 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Platform; -using Avalonia.Rendering.Composition.Drawing.Nodes; using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.Utilities; @@ -16,101 +13,47 @@ namespace Avalonia.Rendering.Composition.Drawing; internal class RenderDataDrawingContext : DrawingContext { private readonly Compositor? _compositor; + private RenderDataStream? _stream; private CompositionRenderData? _renderData; private HashSet? _resourcesHashSet; + private Stack? _pushStack; private static readonly ThreadSafeObjectPool> s_hashSetPool = new(); - private CompositionRenderData RenderData - { - get - { - Debug.Assert(_compositor != null); - return _renderData ??= new(_compositor); - } - } - - struct ParentStackItem + private static readonly ThreadSafeObjectPool> s_pushStackPool = new(); + + private struct PushEntry { - public RenderDataPushNode? Node; - public List Items; + public bool Emitted; + public int PositionBefore; + public int PositionAfter; + public int DepthBefore; } - - private List? _currentItemList; - private static readonly ThreadSafeObjectPool> s_listPool = new(); - - private Stack? _parentNodeStack; - private static readonly ThreadSafeObjectPool> s_parentStackPool = new(); public RenderDataDrawingContext(Compositor? compositor) { _compositor = compositor; } - void Add(IRenderDataItem item) - { - _currentItemList ??= s_listPool.Get(); - _currentItemList.Add(item); - } - - void Push(RenderDataPushNode? node = null) - { - // Push a fake no-op node so something could be popped by the corresponding Pop call - // Since there is no nesting, we don't update the item list - if (node == null) - { - (_parentNodeStack ??= s_parentStackPool.Get()).Push(default); - return; - } - Add(node); - (_parentNodeStack ??= s_parentStackPool.Get()).Push(new ParentStackItem - { - Node = node, - Items = _currentItemList! - }); - _currentItemList = null; - } - - void Pop() where T : IRenderDataItem - { - var parent = _parentNodeStack!.Pop(); - - // No-op node - if (parent.Node == null) - return; + private RenderDataStream Stream => _stream ??= new RenderDataStream(); - if (!(parent.Node is T)) - throw new InvalidOperationException("Invalid Pop operation"); + private CompositionRenderData RenderData => _renderData ??= new CompositionRenderData(_compositor!, Stream); - var removeLastPush = true; - if (_currentItemList != null) - { - removeLastPush = _currentItemList.Count == 0; - foreach (var item in _currentItemList) - parent.Node.Children.Add(item); - _currentItemList.Clear(); - s_listPool.ReturnAndSetNull(ref _currentItemList); - } - _currentItemList = parent.Items; - if (removeLastPush) - _currentItemList.RemoveAt(_currentItemList.Count - 1); - } - - void AddResource(object? resource) + private void AddResource(object? resource) { if (_compositor == null) return; - + if (resource == null || resource is IImmutableBrush || resource is ImmutablePen || resource is ImmutableTransform) return; - + if (resource is ICompositionRenderResource renderResource) { _resourcesHashSet ??= s_hashSetPool.Get(); if (!_resourcesHashSet.Add(renderResource)) return; - + renderResource.AddRefOnCompositor(_compositor); RenderData.AddResource(renderResource); return; @@ -118,19 +61,37 @@ internal class RenderDataDrawingContext : DrawingContext throw new InvalidOperationException(resource.GetType().FullName + " can not be used with this DrawingContext"); } - + + private void PushedScope(int positionBefore) => + (_pushStack ??= s_pushStackPool.Get()).Push(new PushEntry + { + Emitted = true, + PositionBefore = positionBefore, + PositionAfter = Stream.OpcodeLength, + DepthBefore = Stream.Depth - 1 + }); + + private void PushedNoOpScope() => + (_pushStack ??= s_pushStackPool.Get()).Push(new PushEntry { Emitted = false }); + + private void PopCore() + { + var entry = _pushStack!.Pop(); + if (!entry.Emitted) + return; + + if (Stream.OpcodeLength == entry.PositionAfter) + Stream.Rewind(entry.PositionBefore, entry.DepthBefore); + else + Stream.Pop(); + } + protected override void DrawLineCore(IPen? pen, Point p1, Point p2) { - if(pen == null) + if (pen == null) return; AddResource(pen); - Add(new RenderDataLineNode - { - ClientPen = pen, - ServerPen = pen.GetServer(_compositor), - P1 = p1, - P2 = p2 - }); + Stream.DrawLine(pen.GetServer(_compositor), pen, p1, p2); } protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) @@ -139,244 +100,223 @@ internal class RenderDataDrawingContext : DrawingContext return; AddResource(brush); AddResource(pen); - Add(new RenderDataGeometryNode - { - ServerBrush = brush.GetServer(_compositor), - ServerPen = pen.GetServer(_compositor), - ClientPen = pen, - Geometry = geometry - }); + Stream.DrawGeometry(brush.GetServer(_compositor), pen.GetServer(_compositor), pen, geometry); } protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) { if (rrect.IsEmpty()) return; - if(brush == null && pen == null && boxShadows == default) + if (brush == null && pen == null && boxShadows == default) return; AddResource(brush); AddResource(pen); - Add(new RenderDataRectangleNode - { - ServerBrush = brush.GetServer(_compositor), - ServerPen = pen.GetServer(_compositor), - ClientPen = pen, - Rect = rrect, - BoxShadows = boxShadows - }); + Stream.DrawRectangle(brush.GetServer(_compositor), pen.GetServer(_compositor), pen, rrect, boxShadows); } protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) { if (rect.IsEmpty()) return; - - if(brush == null && pen == null) + if (brush == null && pen == null) return; AddResource(brush); AddResource(pen); - Add(new RenderDataEllipseNode - { - ServerBrush = brush.GetServer(_compositor), - ServerPen = pen.GetServer(_compositor), - ClientPen = pen, - Rect = rect, - }); + Stream.DrawEllipse(brush.GetServer(_compositor), pen.GetServer(_compositor), pen, rect); } - public override void Custom(ICustomDrawOperation custom) => Add(new RenderDataCustomNode - { - Operation = custom - }); + public override void Custom(ICustomDrawOperation custom) => Stream.DrawCustom(custom); public override void DrawGlyphRun(IBrush? foreground, GlyphRun? glyphRun) { if (foreground == null || glyphRun == null) return; AddResource(foreground); - Add(new RenderDataGlyphRunNode - { - ServerBrush = foreground.GetServer(_compositor), - GlyphRun = glyphRun.PlatformImpl.Clone() - }); + Stream.DrawGlyphRun(foreground.GetServer(_compositor), glyphRun.PlatformImpl.Clone()); } - protected override void PushClipCore(RoundedRect rect) => Push(new RenderDataClipNode + internal override void DrawBitmap(IRef? source, double opacity, Rect sourceRect, Rect destRect) { - Rect = rect - }); + if (source == null || sourceRect.IsEmpty() || destRect.IsEmpty()) + return; + Stream.DrawBitmap(source.Clone(), opacity, sourceRect, destRect); + } - protected override void PushClipCore(Rect rect) => Push(new RenderDataClipNode + protected override void PushClipCore(RoundedRect rect) { - Rect = rect - }); + var before = Stream.OpcodeLength; + Stream.PushClip(rect); + PushedScope(before); + } + + protected override void PushClipCore(Rect rect) + { + var before = Stream.OpcodeLength; + Stream.PushClip(new RoundedRect(rect)); + PushedScope(before); + } protected override void PushGeometryClipCore(Geometry? clip) { if (clip == null) - Push(); - else - Push(new RenderDataGeometryClipNode - { - Geometry = clip?.PlatformImpl - }); + { + PushedNoOpScope(); + return; + } + + var before = Stream.OpcodeLength; + Stream.PushGeometryClip(clip.PlatformImpl); + PushedScope(before); } protected override void PushOpacityCore(double opacity) { if (opacity == 1) - Push(); - else - Push(new RenderDataOpacityNode - { - Opacity = opacity - }); + { + PushedNoOpScope(); + return; + } + + var before = Stream.OpcodeLength; + Stream.PushOpacity(opacity); + PushedScope(before); } protected override void PushOpacityMaskCore(IBrush? mask, Rect bounds) { - if(mask == null) - Push(); - else + if (mask == null) { - AddResource(mask); - Push(new RenderDataOpacityMaskNode - { - ServerBrush = mask.GetServer(_compositor), - BoundsRect = bounds - }); + PushedNoOpScope(); + return; } + + AddResource(mask); + var before = Stream.OpcodeLength; + Stream.PushOpacityMask(mask.GetServer(_compositor), bounds); + PushedScope(before); } protected override void PushTransformCore(Matrix matrix) { if (matrix.IsIdentity) - Push(); - else - Push(new RenderDataPushMatrixNode() - { - Matrix = matrix - }); + { + PushedNoOpScope(); + return; + } + + var before = Stream.OpcodeLength; + Stream.PushTransform(matrix); + PushedScope(before); } - protected override void PushRenderOptionsCore(RenderOptions renderOptions) => Push(new RenderDataRenderOptionsNode() + protected override void PushRenderOptionsCore(RenderOptions renderOptions) { - RenderOptions = renderOptions - }); + var before = Stream.OpcodeLength; + Stream.PushRenderOptions(renderOptions); + PushedScope(before); + } - protected override void PushTextOptionsCore(TextOptions textOptions) => Push(new RenderDataTextOptionsNode() + protected override void PushTextOptionsCore(TextOptions textOptions) { - TextOptions = textOptions - }); + var before = Stream.OpcodeLength; + Stream.PushTextOptions(textOptions); + PushedScope(before); + } - /// - protected override void PushEffectCore(IEffect effect, Rect bounds) => Push(new RenderDataEffectNode() + protected override void PushEffectCore(IEffect effect, Rect bounds) { - Effect = effect.ToImmutable(), - BoundsRect = bounds.Inflate(effect.GetEffectOutputPadding()) - }); + var before = Stream.OpcodeLength; + Stream.PushEffect(effect.ToImmutable(), bounds.Inflate(effect.GetEffectOutputPadding())); + PushedScope(before); + } - protected override void PopClipCore() => Pop(); + protected override void PopClipCore() => PopCore(); - protected override void PopGeometryClipCore() => Pop(); + protected override void PopGeometryClipCore() => PopCore(); - protected override void PopOpacityCore() => Pop(); + protected override void PopOpacityCore() => PopCore(); - protected override void PopOpacityMaskCore() => Pop(); + protected override void PopOpacityMaskCore() => PopCore(); - protected override void PopTransformCore() => Pop(); + protected override void PopTransformCore() => PopCore(); - protected override void PopRenderOptionsCore() => Pop(); + protected override void PopRenderOptionsCore() => PopCore(); - protected override void PopTextOptionsCore() => Pop(); + protected override void PopTextOptionsCore() => PopCore(); - /// - protected override void PopEffectCore() => Pop(); + protected override void PopEffectCore() => PopCore(); - internal override void DrawBitmap(IRef? source, double opacity, Rect sourceRect, Rect destRect) + private void FlushStack() { - if (source == null || sourceRect.IsEmpty() || destRect.IsEmpty()) - return; - Add(new RenderDataBitmapNode - { - Bitmap = source.Clone(), - Opacity = opacity, - SourceRect = sourceRect, - DestRect = destRect - }); + while (_pushStack is { Count: > 0 }) + PopCore(); } - - void FlushStack() - { - // Flush stack - if (_parentNodeStack != null) - { - // TODO: throw error, unbalanced stack - while (_parentNodeStack.Count > 0) - Pop(); - } - - - } - public CompositionRenderData? GetRenderResults() { Debug.Assert(_compositor != null); - FlushStack(); - - // Transfer items to RenderData - if (_currentItemList is { Count: > 0 }) + + var rv = _renderData; + if (rv == null) { - foreach (var i in _currentItemList) - RenderData.Add(i); - _currentItemList.Clear(); + if (_stream is { OpcodeLength: > 0 }) + rv = new CompositionRenderData(_compositor!, _stream); + else + { + _stream?.Dispose(); + _stream = null; + return null; + } } - var rv = _renderData; _renderData = null; + _stream = null; _resourcesHashSet?.Clear(); - - if (rv != null) - _compositor.RegisterForSerialization(rv); + + _compositor!.RegisterForSerialization(rv); return rv; } public ImmediateRenderDataSceneBrushContent? GetImmediateSceneBrushContent(ITileBrush brush, Rect? rect, bool useScalableRasterization) { Debug.Assert(_compositor == null); - Debug.Assert(_resourcesHashSet == null); Debug.Assert(_renderData == null); - FlushStack(); - if (_currentItemList == null || _currentItemList.Count == 0) - return null; - var itemList = _currentItemList; - _currentItemList = null; + if (_stream is not { OpcodeLength: > 0 }) + { + _stream?.Dispose(); + _stream = null; + return null; + } - return new ImmediateRenderDataSceneBrushContent(brush, itemList, rect, useScalableRasterization, s_listPool); + var stream = _stream; + _stream = null; + return new ImmediateRenderDataSceneBrushContent(brush, stream, rect, useScalableRasterization); } public void Reset() { - // This means that render data should be discarded if (_renderData != null) { _renderData.Dispose(); _renderData = null; } + else + _stream?.Dispose(); - _currentItemList?.Clear(); - _parentNodeStack?.Clear(); + _stream = null; + _pushStack?.Clear(); _resourcesHashSet?.Clear(); } - + protected override void DisposeCore() { Reset(); - if (_resourcesHashSet != null) + if (_resourcesHashSet != null) s_hashSetPool.ReturnAndSetNull(ref _resourcesHashSet); + if (_pushStack != null) + s_pushStackPool.ReturnAndSetNull(ref _pushStack); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataOpcode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataOpcode.cs new file mode 100644 index 0000000000..888ee0cd12 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataOpcode.cs @@ -0,0 +1,22 @@ +namespace Avalonia.Rendering.Composition.Drawing; + +internal enum RenderDataOpcode : byte +{ + Invalid = 0, + DrawLine, + DrawRectangle, + DrawEllipse, + DrawGeometry, + DrawGlyphRun, + DrawBitmap, + DrawCustom, + PushClip, + PushGeometryClip, + PushOpacity, + PushOpacityMask, + PushTransform, + PushRenderOptions, + PushTextOptions, + PushEffect, + Pop +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataPayloads.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataPayloads.cs new file mode 100644 index 0000000000..6bb0c8b08c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataPayloads.cs @@ -0,0 +1,132 @@ +using Avalonia.Media; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal interface IRenderDataPayload where TSelf : unmanaged, IRenderDataPayload +{ + static abstract RenderDataOpcode Opcode { get; } +} + +internal struct DrawLinePayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.DrawLine; + + public int ServerPen; + public int ClientPen; + public Point P1; + public Point P2; +} + +internal struct DrawRectanglePayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.DrawRectangle; + + public int ServerBrush; + public int ServerPen; + public int ClientPen; + public RoundedRect Rect; + public int BoxShadowCount; +} + +internal struct DrawEllipsePayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.DrawEllipse; + + public int ServerBrush; + public int ServerPen; + public int ClientPen; + public Rect Rect; +} + +internal struct DrawGeometryPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.DrawGeometry; + + public int ServerBrush; + public int ServerPen; + public int ClientPen; + public int Geometry; +} + +internal struct DrawGlyphRunPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.DrawGlyphRun; + + public int ServerBrush; + public int GlyphRun; +} + +internal struct DrawBitmapPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.DrawBitmap; + + public int Bitmap; + public double Opacity; + public Rect SourceRect; + public Rect DestRect; +} + +internal struct DrawCustomPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.DrawCustom; + + public int Operation; +} + +internal struct PushClipPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.PushClip; + + public RoundedRect Clip; +} + +internal struct PushGeometryClipPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.PushGeometryClip; + + public int Geometry; +} + +internal struct PushOpacityPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.PushOpacity; + + public double Opacity; +} + +internal struct PushOpacityMaskPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.PushOpacityMask; + + public int Brush; + public Rect Bounds; +} + +internal struct PushTransformPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.PushTransform; + + public Matrix Matrix; +} + +internal struct PushRenderOptionsPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.PushRenderOptions; + + public RenderOptions Options; +} + +internal struct PushTextOptionsPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.PushTextOptions; + + public TextOptions Options; +} + +internal struct PushEffectPayload : IRenderDataPayload +{ + public static RenderDataOpcode Opcode => RenderDataOpcode.PushEffect; + + public int Effect; + public Rect Bounds; +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataReader.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataReader.cs new file mode 100644 index 0000000000..b1b7199100 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataReader.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal ref struct RenderDataReader +{ + private readonly ReadOnlySpan _buffer; + private int _position; + + public RenderDataReader(ReadOnlySpan buffer) + { + _buffer = buffer; + _position = 0; + } + + public int Position => _position; + + public bool IsAtEnd => _position >= _buffer.Length; + + public ReadOnlySpan Take(int count) + { + var span = _buffer.Slice(_position, count); + _position += count; + return span; + } + + public T Read() where T : unmanaged + => MemoryMarshal.Read(Take(Unsafe.SizeOf())); + + public T Peek() where T : unmanaged + => MemoryMarshal.Read(_buffer.Slice(_position, Unsafe.SizeOf())); + + public T ReadPayload() where T : unmanaged, IRenderDataPayload + { + var opcode = Read(); + Debug.Assert(opcode == T.Opcode); + return Read(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataResources.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataResources.cs new file mode 100644 index 0000000000..9d8a43c628 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataResources.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections.Pooled; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal struct RenderDataResources : IDisposable +{ + public const int NullHandle = -1; + + private PooledList? _resources; + private Dictionary? _internMap; + + public int Count => _resources?.Count ?? 0; + + // Recording path: dedupes by reference equality so a resource reused across many draws gets one slot. + public int Intern(object? resource) + { + if (resource is null) + return NullHandle; + + _resources ??= new PooledList(); + _internMap ??= new Dictionary(ReferenceEqualityComparer.Instance); + + if (_internMap.TryGetValue(resource, out var handle)) + return handle; + + handle = _resources.Count; + _resources.Add(resource); + _internMap.Add(resource, handle); + return handle; + } + + // Deserialize path: appends without deduping since the wire format is already deduped. + public int AppendDeserialized(object? resource) + { + if (resource is null) + return NullHandle; + + _resources ??= new PooledList(); + var handle = _resources.Count; + _resources.Add(resource); + return handle; + } + + public object? this[int handle] => handle == NullHandle ? null : _resources![handle]; + + public void Dispose() + { + _resources?.Dispose(); + _resources = null; + _internMap = null; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Bounds.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Bounds.cs new file mode 100644 index 0000000000..a1bc2d1a23 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Bounds.cs @@ -0,0 +1,93 @@ +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal partial class RenderDataStream +{ + internal struct BoundsScope + { + public Rect? SavedBounds; + public bool IsTransform; + public Matrix Matrix; + public Thickness EffectPadding; + } + + internal struct BoundsVisitor : IRenderDataVisitor + { + public Rect? Current; + + public bool StopVisiting => false; + + public void OnDrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2) + { + if (serverPen != null) + Current = Rect.Union(Current, LineBoundsHelper.CalculateBounds(p1, p2, serverPen)); + } + + public void OnDrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, + BoxShadows boxShadows) + { + var bounds = boxShadows.TransformBounds(rect.Rect) + .Inflate((serverPen?.Thickness ?? 0) / 2); + Current = Rect.Union(Current, bounds); + } + + public void OnDrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect) + => Current = Rect.Union(Current, rect.Inflate(serverPen?.Thickness ?? 0)); + + public void OnDrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry) + => Current = Rect.Union(Current, geometry?.GetRenderBounds(serverPen) ?? default); + + public void OnDrawGlyphRun(IBrush? serverBrush, IRef? glyphRun) + => Current = Rect.Union(Current, glyphRun?.Item?.Bounds ?? default); + + public void OnDrawBitmap(IRef? bitmap, double opacity, Rect sourceRect, Rect destRect) + => Current = Rect.Union(Current, destRect); + + public void OnDrawCustom(ICustomDrawOperation? operation) + => Current = Rect.Union(Current, operation?.Bounds); + + private BoundsScope EnterChildScope(bool isTransform = false, Matrix matrix = default, + Thickness effectPadding = default) + { + var scope = new BoundsScope + { + SavedBounds = Current, IsTransform = isTransform, Matrix = matrix, EffectPadding = effectPadding + }; + Current = null; + return scope; + } + + public BoundsScope OnPushClip(RoundedRect clip) => EnterChildScope(); + public BoundsScope OnPushGeometryClip(IGeometryImpl? geometry) => EnterChildScope(); + public BoundsScope OnPushOpacity(double opacity) => EnterChildScope(); + public BoundsScope OnPushOpacityMask(IBrush? brush, Rect bounds) => EnterChildScope(); + public BoundsScope OnPushTransform(Matrix matrix) => EnterChildScope(true, matrix); + public BoundsScope OnPushRenderOptions(RenderOptions options) => EnterChildScope(); + public BoundsScope OnPushTextOptions(TextOptions options) => EnterChildScope(); + + public BoundsScope OnPushEffect(IEffect? effect, Rect bounds) + => EnterChildScope(effectPadding: effect.GetEffectOutputPadding()); + + public void OnPop(in BoundsScope scope) + { + var childUnion = Current; + if (scope.IsTransform) + childUnion = childUnion?.TransformToAABB(scope.Matrix); + else if (childUnion.HasValue && !scope.EffectPadding.Equals(default)) + childUnion = childUnion.Value.Inflate(scope.EffectPadding); + Current = Rect.Union(scope.SavedBounds, childUnion); + } + } + + public Rect? CalculateBounds() + { + var visitor = new BoundsVisitor(); + Visit(ref visitor); + return visitor.Current; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.HitTest.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.HitTest.cs new file mode 100644 index 0000000000..38b39053f9 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.HitTest.cs @@ -0,0 +1,250 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal partial class RenderDataStream +{ + internal struct HitTestScope + { + public bool SavedLive; + public bool RestorePoint; + public Point SavedPoint; + } + + internal struct HitTestVisitor : IRenderDataVisitor + { + public bool StopVisiting { get; private set; } + public bool HitFound; + public Point Current; + public bool Live; + + public HitTestVisitor(Point point) + { + StopVisiting = false; + HitFound = false; + Current = point; + Live = true; + } + + private void Hit() + { + HitFound = true; + StopVisiting = true; + } + + public void OnDrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2) + { + if (Live && HitTestLine(clientPen, p1, p2, Current)) + Hit(); + } + + public void OnDrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, + BoxShadows boxShadows) + { + if (Live && HitTestRectangle(serverBrush, clientPen, rect, Current)) + Hit(); + } + + public void OnDrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect) + { + if (Live && HitTestEllipse(serverBrush, clientPen, rect, Current)) + Hit(); + } + + public void OnDrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry) + { + if (Live && geometry != null && + ((serverBrush != null && geometry.FillContains(Current)) || + (clientPen != null && geometry.StrokeContains(clientPen, Current)))) + Hit(); + } + + public void OnDrawGlyphRun(IBrush? serverBrush, IRef? glyphRun) + { + if (Live && glyphRun != null && glyphRun.Item.Bounds.ContainsExclusive(Current)) + Hit(); + } + + public void OnDrawBitmap(IRef? bitmap, double opacity, Rect sourceRect, Rect destRect) + { + if (Live && destRect.Contains(Current)) + Hit(); + } + + public void OnDrawCustom(ICustomDrawOperation? operation) + { + if (Live && operation != null && operation.HitTest(Current)) + Hit(); + } + + public HitTestScope OnPushClip(RoundedRect clip) + { + var scope = new HitTestScope { SavedLive = Live }; + if (Live && !clip.Rect.Contains(Current)) + Live = false; + return scope; + } + + public HitTestScope OnPushGeometryClip(IGeometryImpl? geometry) + { + var scope = new HitTestScope { SavedLive = Live }; + if (Live && geometry != null && !geometry.FillContains(Current)) + Live = false; + return scope; + } + + public HitTestScope OnPushOpacity(double opacity) + => new HitTestScope { SavedLive = Live }; + + public HitTestScope OnPushOpacityMask(IBrush? brush, Rect bounds) + => new HitTestScope { SavedLive = Live }; + + public HitTestScope OnPushTransform(Matrix matrix) + { + var scope = new HitTestScope { SavedLive = Live }; + if (Live) + { + if (matrix.TryInvert(out var inverted)) + { + scope.RestorePoint = true; + scope.SavedPoint = Current; + Current = Current.Transform(inverted); + } + else + Live = false; + } + return scope; + } + + public HitTestScope OnPushRenderOptions(RenderOptions options) + => new HitTestScope { SavedLive = Live }; + + public HitTestScope OnPushTextOptions(TextOptions options) + => new HitTestScope { SavedLive = Live }; + + public HitTestScope OnPushEffect(IEffect? effect, Rect bounds) + => new HitTestScope { SavedLive = Live }; + + public void OnPop(in HitTestScope scope) + { + Live = scope.SavedLive; + if (scope.RestorePoint) + Current = scope.SavedPoint; + } + } + + public bool HitTest(Point point) + { + var visitor = new HitTestVisitor(point); + Visit(ref visitor); + return visitor.HitFound; + } + + private static bool HitTestLine(IPen? clientPen, Point p1, Point p2, Point p) + { + if (clientPen == null) + return false; + + var halfThickness = clientPen.Thickness / 2; + var minX = Math.Min(p1.X, p2.X) - halfThickness; + var maxX = Math.Max(p1.X, p2.X) + halfThickness; + var minY = Math.Min(p1.Y, p2.Y) - halfThickness; + var maxY = Math.Max(p1.Y, p2.Y) + halfThickness; + + if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) + return false; + + Vector ap = p - p1; + var dot1 = Vector.Dot(p2 - p1, ap); + if (dot1 < 0) + return ap.Length <= halfThickness; + + Vector bp = p - p2; + var dot2 = Vector.Dot(p1 - p2, bp); + if (dot2 < 0) + return bp.Length <= halfThickness; + + var bXaX = p2.X - p1.X; + var bYaY = p2.Y - p1.Y; + var distance = (bXaX * (p.Y - p1.Y) - bYaY * (p.X - p1.X)) / + Math.Sqrt(bXaX * bXaX + bYaY * bYaY); + return Math.Abs(distance) <= halfThickness; + } + + private static bool HitTestRectangle(IBrush? serverBrush, IPen? clientPen, RoundedRect rect, Point p) + { + var strokeThicknessAdjustment = (clientPen?.Thickness / 2) ?? 0; + + if (rect.IsRounded) + { + var outer = rect.Inflate(strokeThicknessAdjustment, strokeThicknessAdjustment); + if (outer.ContainsExclusive(p)) + { + if (serverBrush != null) + return true; + + var inner = rect.Deflate(strokeThicknessAdjustment, strokeThicknessAdjustment); + return !inner.ContainsExclusive(p); + } + } + else + { + var outer = rect.Rect.Inflate(strokeThicknessAdjustment); + if (outer.ContainsExclusive(p)) + { + if (serverBrush != null) + return true; + + var inner = rect.Rect.Deflate(strokeThicknessAdjustment); + return !inner.ContainsExclusive(p); + } + } + + return false; + } + + private static bool HitTestEllipse(IBrush? serverBrush, IPen? clientPen, Rect rect, Point p) + { + var center = rect.Center; + var strokeThickness = clientPen?.Thickness ?? 0; + + var rx = rect.Width / 2 + strokeThickness / 2; + var ry = rect.Height / 2 + strokeThickness / 2; + + var dx = p.X - center.X; + var dy = p.Y - center.Y; + + if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) + return false; + + if (serverBrush != null) + return EllipseContains(dx, dy, rx, ry); + + if (strokeThickness > 0) + { + var inStroke = EllipseContains(dx, dy, rx, ry); + + rx = rect.Width / 2 - strokeThickness / 2; + ry = rect.Height / 2 - strokeThickness / 2; + + var inInner = EllipseContains(dx, dy, rx, ry); + + return inStroke && !inInner; + } + + return false; + } + + private static bool EllipseContains(double dx, double dy, double radiusX, double radiusY) + { + var rx2 = radiusX * radiusX; + var ry2 = radiusY * radiusY; + var distance = ry2 * dx * dx + rx2 * dy * dy; + return distance <= rx2 * ry2; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Replay.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Replay.cs new file mode 100644 index 0000000000..4ffb636d0d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Replay.cs @@ -0,0 +1,159 @@ +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal partial class RenderDataStream +{ + internal struct ReplayScope + { + public RenderDataOpcode Kind; + public bool Active; + public Matrix SavedTransform; + } + + internal struct ReplayVisitor : IRenderDataVisitor + { + private readonly IDrawingContextImpl _context; + + public ReplayVisitor(IDrawingContextImpl context) + { + _context = context; + } + + public bool StopVisiting => false; + + public void OnDrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2) + => _context.DrawLine(serverPen, p1, p2); + + public void OnDrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, + BoxShadows boxShadows) + => _context.DrawRectangle(serverBrush, serverPen, rect, boxShadows); + + public void OnDrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect) + => _context.DrawEllipse(serverBrush, serverPen, rect); + + public void OnDrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry) + { + if (geometry != null) + _context.DrawGeometry(serverBrush, serverPen, geometry); + } + + public void OnDrawGlyphRun(IBrush? serverBrush, IRef? glyphRun) + { + if (glyphRun != null) + _context.DrawGlyphRun(serverBrush, glyphRun.Item); + } + + public void OnDrawBitmap(IRef? bitmap, double opacity, Rect sourceRect, Rect destRect) + { + if (bitmap != null) + _context.DrawBitmap(bitmap.Item, opacity, sourceRect, destRect); + } + + public void OnDrawCustom(ICustomDrawOperation? operation) + => operation?.Render(new ImmediateDrawingContext(_context, false)); + + public ReplayScope OnPushClip(RoundedRect clip) + { + _context.PushClip(clip); + return new ReplayScope { Kind = RenderDataOpcode.PushClip, Active = true }; + } + + public ReplayScope OnPushGeometryClip(IGeometryImpl? geometry) + { + if (geometry != null) + _context.PushGeometryClip(geometry); + return new ReplayScope + { Kind = RenderDataOpcode.PushGeometryClip, Active = geometry != null }; + } + + public ReplayScope OnPushOpacity(double opacity) + { + if (opacity != 1) + _context.PushOpacity(opacity, null); + return new ReplayScope { Kind = RenderDataOpcode.PushOpacity, Active = opacity != 1 }; + } + + public ReplayScope OnPushOpacityMask(IBrush? brush, Rect bounds) + { + if (brush != null) + _context.PushOpacityMask(brush, bounds); + return new ReplayScope { Kind = RenderDataOpcode.PushOpacityMask, Active = brush != null }; + } + + public ReplayScope OnPushTransform(Matrix matrix) + { + var saved = _context.Transform; + _context.Transform = matrix * saved; + return new ReplayScope + { Kind = RenderDataOpcode.PushTransform, Active = true, SavedTransform = saved }; + } + + public ReplayScope OnPushRenderOptions(RenderOptions options) + { + _context.PushRenderOptions(options); + return new ReplayScope { Kind = RenderDataOpcode.PushRenderOptions, Active = true }; + } + + public ReplayScope OnPushTextOptions(TextOptions options) + { + _context.PushTextOptions(options); + return new ReplayScope { Kind = RenderDataOpcode.PushTextOptions, Active = true }; + } + + public ReplayScope OnPushEffect(IEffect? effect, Rect bounds) + { + var active = false; + if (effect != null && _context is IDrawingContextImplWithEffects effectImpl) + { + effectImpl.PushEffect(bounds, effect); + active = true; + } + return new ReplayScope { Kind = RenderDataOpcode.PushEffect, Active = active }; + } + + public void OnPop(in ReplayScope scope) + { + if (!scope.Active) + return; + + switch (scope.Kind) + { + case RenderDataOpcode.PushClip: + _context.PopClip(); + break; + case RenderDataOpcode.PushGeometryClip: + _context.PopGeometryClip(); + break; + case RenderDataOpcode.PushOpacity: + _context.PopOpacity(); + break; + case RenderDataOpcode.PushOpacityMask: + _context.PopOpacityMask(); + break; + case RenderDataOpcode.PushTransform: + _context.Transform = scope.SavedTransform; + break; + case RenderDataOpcode.PushRenderOptions: + _context.PopRenderOptions(); + break; + case RenderDataOpcode.PushTextOptions: + _context.PopTextOptions(); + break; + case RenderDataOpcode.PushEffect: + ((IDrawingContextImplWithEffects)_context).PopEffect(); + break; + } + } + } + + public void Replay(IDrawingContextImpl context) + { + var visitor = new ReplayVisitor(context); + Visit(ref visitor); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Visit.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Visit.cs new file mode 100644 index 0000000000..370a79403c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Visit.cs @@ -0,0 +1,177 @@ +using System; +using System.Buffers; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal partial class RenderDataStream +{ + private const int MaxStackScopeDepth = 64; + + public void Visit(ref TVisitor visitor) + where TVisitor : struct, IRenderDataVisitor + where TScope : unmanaged + { + var reader = new RenderDataReader(_writer.Written); + TScope[]? rented = null; + scoped Span scopes; + if (_maxDepth == 0) + scopes = default; + else if (_maxDepth <= MaxStackScopeDepth) + scopes = stackalloc TScope[_maxDepth]; + else + scopes = rented = ArrayPool.Shared.Rent(_maxDepth); + var depth = 0; + try + { + while (!visitor.StopVisiting && !reader.IsAtEnd) + { + switch (reader.Peek()) + { + case RenderDataOpcode.DrawLine: + { + var p = reader.ReadPayload(); + visitor.OnDrawLine( + (IPen?)_resources[p.ServerPen], + (IPen?)_resources[p.ClientPen], + p.P1, p.P2); + break; + } + case RenderDataOpcode.DrawRectangle: + { + var p = reader.ReadPayload(); + var shadows = ReadBoxShadows(ref reader, p.BoxShadowCount); + visitor.OnDrawRectangle( + (IBrush?)_resources[p.ServerBrush], + (IPen?)_resources[p.ServerPen], + (IPen?)_resources[p.ClientPen], + p.Rect, shadows); + break; + } + case RenderDataOpcode.DrawEllipse: + { + var p = reader.ReadPayload(); + visitor.OnDrawEllipse( + (IBrush?)_resources[p.ServerBrush], + (IPen?)_resources[p.ServerPen], + (IPen?)_resources[p.ClientPen], + p.Rect); + break; + } + case RenderDataOpcode.DrawGeometry: + { + var p = reader.ReadPayload(); + visitor.OnDrawGeometry( + (IBrush?)_resources[p.ServerBrush], + (IPen?)_resources[p.ServerPen], + (IPen?)_resources[p.ClientPen], + (IGeometryImpl?)_resources[p.Geometry]); + break; + } + case RenderDataOpcode.DrawGlyphRun: + { + var p = reader.ReadPayload(); + visitor.OnDrawGlyphRun( + (IBrush?)_resources[p.ServerBrush], + (IRef?)_resources[p.GlyphRun]); + break; + } + case RenderDataOpcode.DrawBitmap: + { + var p = reader.ReadPayload(); + visitor.OnDrawBitmap( + (IRef?)_resources[p.Bitmap], + p.Opacity, p.SourceRect, p.DestRect); + break; + } + case RenderDataOpcode.DrawCustom: + { + var p = reader.ReadPayload(); + visitor.OnDrawCustom((ICustomDrawOperation?)_resources[p.Operation]); + break; + } + case RenderDataOpcode.PushClip: + { + var p = reader.ReadPayload(); + scopes[depth++] = visitor.OnPushClip(p.Clip); + break; + } + case RenderDataOpcode.PushGeometryClip: + { + var p = reader.ReadPayload(); + scopes[depth++] = visitor.OnPushGeometryClip((IGeometryImpl?)_resources[p.Geometry]); + break; + } + case RenderDataOpcode.PushOpacity: + { + var p = reader.ReadPayload(); + scopes[depth++] = visitor.OnPushOpacity(p.Opacity); + break; + } + case RenderDataOpcode.PushOpacityMask: + { + var p = reader.ReadPayload(); + scopes[depth++] = visitor.OnPushOpacityMask( + (IBrush?)_resources[p.Brush], p.Bounds); + break; + } + case RenderDataOpcode.PushTransform: + { + var p = reader.ReadPayload(); + scopes[depth++] = visitor.OnPushTransform(p.Matrix); + break; + } + case RenderDataOpcode.PushRenderOptions: + { + var p = reader.ReadPayload(); + scopes[depth++] = visitor.OnPushRenderOptions(p.Options); + break; + } + case RenderDataOpcode.PushTextOptions: + { + var p = reader.ReadPayload(); + scopes[depth++] = visitor.OnPushTextOptions(p.Options); + break; + } + case RenderDataOpcode.PushEffect: + { + var p = reader.ReadPayload(); + scopes[depth++] = visitor.OnPushEffect( + (IEffect?)_resources[p.Effect], p.Bounds); + break; + } + case RenderDataOpcode.Pop: + { + reader.Read(); + visitor.OnPop(in scopes[--depth]); + break; + } + } + } + } + finally + { + if (rented != null) + ArrayPool.Shared.Return(rented); + } + } + + private static BoxShadows ReadBoxShadows(ref RenderDataReader reader, int count) + { + if (count == 0) + return default; + + var first = reader.Read(); + if (count == 1) + return new BoxShadows(first); + + var rest = new BoxShadow[count - 1]; + for (var i = 0; i < rest.Length; i++) + rest[i] = reader.Read(); + return new BoxShadows(first, rest); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.cs new file mode 100644 index 0000000000..910043ffea --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.cs @@ -0,0 +1,226 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal partial class RenderDataStream : IDisposable +{ + private RenderDataWriter _writer; + private RenderDataResources _resources; + private int _depth; + private int _maxDepth; + + public ReadOnlySpan Opcodes => _writer.Written; + + public int OpcodeLength => _writer.Length; + + public int Depth => _depth; + + public int ResourceCount => _resources.Count; + + public object? GetResource(int handle) => _resources[handle]; + + public void Rewind(int length, int depth) + { + _writer.Rewind(length); + _depth = depth; + } + + private void EnterScope() + { + _depth++; + if (_depth > _maxDepth) + _maxDepth = _depth; + } + + public void DisposeResources() + { + for (var i = 0; i < _resources.Count; i++) + { + switch (_resources[i]) + { + case IRef bitmap: + bitmap.Dispose(); + break; + case IRef glyphRun: + glyphRun.Dispose(); + break; + case ICustomDrawOperation operation: + operation.Dispose(); + break; + } + } + } + + public void DrawLine(IPen? serverPen, IPen? clientPen, Point p1, Point p2) + { + _writer.WritePayload(new DrawLinePayload + { + ServerPen = _resources.Intern(serverPen), + ClientPen = _resources.Intern(clientPen), + P1 = p1, + P2 = p2 + }); + } + + public void DrawRectangle(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, RoundedRect rect, + BoxShadows boxShadows) + { + _writer.WritePayload(new DrawRectanglePayload + { + ServerBrush = _resources.Intern(serverBrush), + ServerPen = _resources.Intern(serverPen), + ClientPen = _resources.Intern(clientPen), + Rect = rect, + BoxShadowCount = boxShadows.Count + }); + for (var i = 0; i < boxShadows.Count; i++) + _writer.Write(boxShadows[i]); + } + + public void DrawEllipse(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, Rect rect) + { + _writer.WritePayload(new DrawEllipsePayload + { + ServerBrush = _resources.Intern(serverBrush), + ServerPen = _resources.Intern(serverPen), + ClientPen = _resources.Intern(clientPen), + Rect = rect + }); + } + + public void DrawGeometry(IBrush? serverBrush, IPen? serverPen, IPen? clientPen, IGeometryImpl? geometry) + { + _writer.WritePayload(new DrawGeometryPayload + { + ServerBrush = _resources.Intern(serverBrush), + ServerPen = _resources.Intern(serverPen), + ClientPen = _resources.Intern(clientPen), + Geometry = _resources.Intern(geometry) + }); + } + + public void DrawGlyphRun(IBrush? serverBrush, IRef? glyphRun) + { + _writer.WritePayload(new DrawGlyphRunPayload + { + ServerBrush = _resources.Intern(serverBrush), + GlyphRun = _resources.Intern(glyphRun) + }); + } + + public void DrawBitmap(IRef? bitmap, double opacity, Rect sourceRect, Rect destRect) + { + _writer.WritePayload(new DrawBitmapPayload + { + Bitmap = _resources.Intern(bitmap), + Opacity = opacity, + SourceRect = sourceRect, + DestRect = destRect + }); + } + + public void DrawCustom(ICustomDrawOperation? operation) + { + _writer.WritePayload(new DrawCustomPayload + { + Operation = _resources.Intern(operation) + }); + } + + public void PushClip(RoundedRect clip) + { + _writer.WritePayload(new PushClipPayload { Clip = clip }); + EnterScope(); + } + + public void PushGeometryClip(IGeometryImpl? geometry) + { + _writer.WritePayload(new PushGeometryClipPayload { Geometry = _resources.Intern(geometry) }); + EnterScope(); + } + + public void PushOpacity(double opacity) + { + _writer.WritePayload(new PushOpacityPayload { Opacity = opacity }); + EnterScope(); + } + + public void PushOpacityMask(IBrush? serverBrush, Rect bounds) + { + _writer.WritePayload(new PushOpacityMaskPayload + { + Brush = _resources.Intern(serverBrush), + Bounds = bounds + }); + EnterScope(); + } + + public void PushTransform(Matrix matrix) + { + _writer.WritePayload(new PushTransformPayload { Matrix = matrix }); + EnterScope(); + } + + public void PushRenderOptions(RenderOptions renderOptions) + { + _writer.WritePayload(new PushRenderOptionsPayload { Options = renderOptions }); + EnterScope(); + } + + public void PushTextOptions(TextOptions textOptions) + { + _writer.WritePayload(new PushTextOptionsPayload { Options = textOptions }); + EnterScope(); + } + + public void PushEffect(IImmutableEffect? effect, Rect bounds) + { + _writer.WritePayload(new PushEffectPayload + { + Effect = _resources.Intern(effect), + Bounds = bounds + }); + EnterScope(); + } + + public void Pop() + { + _writer.WriteOpcode(RenderDataOpcode.Pop); + _depth--; + } + + public void SerializeTo(BatchStreamWriter writer) + { + var opcodes = _writer.Written; + writer.Write(_maxDepth); + writer.Write(_resources.Count); + for (var i = 0; i < _resources.Count; i++) + writer.WriteObject(_resources[i]); + writer.Write(opcodes.Length); + writer.Write(opcodes); + } + + public void DeserializeFrom(BatchStreamReader reader) + { + _maxDepth = reader.Read(); + + var resourceCount = reader.Read(); + for (var i = 0; i < resourceCount; i++) + _resources.AppendDeserialized(reader.ReadObject()); + + var byteCount = reader.Read(); + if (byteCount > 0) + reader.Read(_writer.Reserve(byteCount)); + } + + public void Dispose() + { + _writer.Dispose(); + _resources.Dispose(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataWriter.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataWriter.cs new file mode 100644 index 0000000000..7cecf86ead --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataWriter.cs @@ -0,0 +1,57 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal struct RenderDataWriter : IDisposable +{ + private byte[]? _buffer; + private int _length; + + public int Length => _length; + + public ReadOnlySpan Written => _buffer is null ? default : _buffer.AsSpan(0, _length); + + private Span Advance(int size) + { + var required = _length + size; + if (_buffer is null) + _buffer = ArrayPool.Shared.Rent(Math.Max(required, 256)); + else if (_buffer.Length < required) + { + var grown = ArrayPool.Shared.Rent(Math.Max(required, _buffer.Length * 2)); + Array.Copy(_buffer, grown, _length); + ArrayPool.Shared.Return(_buffer); + _buffer = grown; + } + + var span = _buffer.AsSpan(_length, size); + _length += size; + return span; + } + + public Span Reserve(int count) => Advance(count); + + public void Rewind(int length) => _length = length; + + public void Write(T value) where T : unmanaged + => MemoryMarshal.Write(Advance(Unsafe.SizeOf()), in value); + + public void WriteOpcode(RenderDataOpcode opcode) => Write(opcode); + + public void WritePayload(T payload) where T : unmanaged, IRenderDataPayload + { + Write(T.Opcode); + Write(payload); + } + + public void Dispose() + { + if (_buffer != null) + ArrayPool.Shared.Return(_buffer); + _buffer = null; + _length = 0; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs index 3745fb54d8..8c1b5a2585 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs @@ -1,70 +1,39 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using Avalonia.Platform; -using Avalonia.Rendering.Composition.Drawing.Nodes; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; -using Avalonia.Threading; using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Drawing; class ServerCompositionRenderData : SimpleServerRenderResource { - private PooledInlineList _items; + private RenderDataStream? _stream; private PooledInlineList _referencedResources; private LtrbRect? _bounds; private bool _boundsValid; - private static readonly ThreadSafeObjectPool s_resourceHashSetPool = new(); public ServerCompositionRenderData(ServerCompositor compositor) : base(compositor) { } - class Collector : IRenderDataServerResourcesCollector - { - public readonly HashSet Resources = new(); - public void AddRenderDataServerResource(object? obj) - { - if (obj is IServerRenderResource res) - Resources.Add(res); - } - } - protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { Reset(); - - var count = reader.Read(); - _items.EnsureCapacity(count); - for (var c = 0; c < count; c++) - _items.Add(reader.ReadObject()); - - var collector = s_resourceHashSetPool.Get(); - CollectResources(_items, collector); - - foreach (var r in collector.Resources) - { - _referencedResources.Add(r); - r.AddObserver(this); - } - collector.Resources.Clear(); - s_resourceHashSetPool.ReturnAndSetNull(ref collector); - - base.DeserializeChangesCore(reader, committedAt); - } + _stream = new RenderDataStream(); + _stream.DeserializeFrom(reader); - private static void CollectResources(PooledInlineList items, IRenderDataServerResourcesCollector collector) - { - foreach (var item in items) + for (var i = 0; i < _stream.ResourceCount; i++) { - if (item is IRenderDataItemWithServerResources resourceItem) - resourceItem.Collect(collector); - else if (item is RenderDataPushNode pushNode) - CollectResources(pushNode.Children, collector); + if (_stream.GetResource(i) is IServerRenderResource resource) + { + _referencedResources.Add(resource); + resource.AddObserver(this); + } } + + base.DeserializeChangesCore(reader, committedAt); } public LtrbRect? Bounds @@ -82,11 +51,8 @@ class ServerCompositionRenderData : SimpleServerRenderResource private LtrbRect? CalculateRenderBounds() { - LtrbRect? totalBounds = null; - foreach (var item in _items) - totalBounds = LtrbRect.FullUnion(totalBounds, item.Bounds); - - return ApplyRenderBoundsRounding(totalBounds); + var bounds = _stream?.CalculateBounds(); + return bounds.HasValue ? ApplyRenderBoundsRounding(new LtrbRect(bounds.Value)) : null; } public static Rect? ApplyRenderBoundsRounding(Rect? rect) @@ -95,7 +61,7 @@ class ServerCompositionRenderData : SimpleServerRenderResource return null; return ApplyRenderBoundsRounding(new LtrbRect(rect.Value))?.ToRect(); } - + public static LtrbRect? ApplyRenderBoundsRounding(LtrbRect? rect) { if (rect != null) @@ -115,34 +81,26 @@ class ServerCompositionRenderData : SimpleServerRenderResource _boundsValid = false; base.DependencyQueuedInvalidate(sender); } - - public void Render(IDrawingContextImpl context) - { - var ctx = new RenderDataNodeRenderContext(context); - try - { - foreach (var item in _items) - item.Invoke(ref ctx); - } - finally - { - ctx.Dispose(); - } - } - void Reset() + public void Render(IDrawingContextImpl context) => _stream?.Replay(context); + + private void Reset() { _bounds = null; _boundsValid = false; + foreach (var r in _referencedResources) r.RemoveObserver(this); _referencedResources.Dispose(); - foreach(var i in _items) - if (i is IDisposable disp) - disp.Dispose(); - _items.Dispose(); + + if (_stream != null) + { + _stream.DisposeResources(); + _stream.Dispose(); + _stream = null; + } } - + public override void Dispose() { Reset(); diff --git a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs index 1fcadae1c2..7bd3fe59dc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs +++ b/src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs @@ -113,6 +113,24 @@ internal class BatchStreamWriter : IDisposable _currentDataSegment.ElementCount += size; } + public unsafe void Write(ReadOnlySpan data) + { + while (data.Length > 0) + { + if (_currentDataSegment.Data == IntPtr.Zero || + _currentDataSegment.ElementCount == _memoryPool.BufferSize) + NextDataSegment(); + + var chunk = Math.Min(_memoryPool.BufferSize - _currentDataSegment.ElementCount, data.Length); + var destination = new Span( + (byte*)_currentDataSegment.Data + _currentDataSegment.ElementCount, chunk); + data.Slice(0, chunk).CopyTo(destination); + + _currentDataSegment.ElementCount += chunk; + data = data.Slice(chunk); + } + } + public void WriteObject(object? item) { if (_currentObjectSegment.Data == null || @@ -180,6 +198,34 @@ internal class BatchStreamReader : IDisposable return rv; } + public unsafe void Read(Span destination) + { + while (destination.Length > 0) + { + if (_currentDataSegment.Data == IntPtr.Zero) + { + if (_input.Structs.Count == 0) + throw new EndOfStreamException(); + _currentDataSegment = _input.Structs.Dequeue(); + _memoryOffset = 0; + } + + var chunk = Math.Min(_currentDataSegment.ElementCount - _memoryOffset, destination.Length); + var source = new ReadOnlySpan( + (byte*)_currentDataSegment.Data + _memoryOffset, chunk); + source.CopyTo(destination); + + _memoryOffset += chunk; + destination = destination.Slice(chunk); + + if (_memoryOffset == _currentDataSegment.ElementCount) + { + _memoryPool.Return(_currentDataSegment.Data); + _currentDataSegment = new(); + } + } + } + public T ReadObject() where T : class? => (T)ReadObject()!; public object? ReadObject() diff --git a/tests/Avalonia.Base.UnitTests/Rendering/RenderDataEffectNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/RenderDataEffectNodeTests.cs deleted file mode 100644 index e812e388ca..0000000000 --- a/tests/Avalonia.Base.UnitTests/Rendering/RenderDataEffectNodeTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Avalonia.Media; -using Avalonia.Rendering.Composition.Drawing.Nodes; -using Xunit; - -namespace Avalonia.Base.UnitTests.Rendering; - -public class RenderDataEffectNodeTests -{ - /// - /// Regression test: RenderDataEffectNode.Bounds was returning BoundsRect even when - /// there were no children, causing incorrect dirty rect tracking. - /// - [Fact] - public void Bounds_Should_Return_Null_When_No_Children() - { - var effect = new BlurEffect { Radius = 10 }.ToImmutable(); - var node = new RenderDataEffectNode - { - Effect = effect, - BoundsRect = new Rect(0, 0, 200, 200) - }; - - Assert.Null(node.Bounds); - } - - /// - /// RenderDataEffectNode.Bounds should expand child bounds by the effect output padding - /// so that dirty rects include effect output (blur/shadow extending beyond geometry bounds). - /// - [Fact] - public void Bounds_Should_Inflate_Child_Bounds_By_Effect_Output_Padding() - { - var effect = new BlurEffect { Radius = 10 }.ToImmutable(); - var childBounds = new Rect(10, 10, 100, 100); - var node = new RenderDataEffectNode { Effect = effect }; - node.Children.Add(new MockRenderDataItem { MockBounds = childBounds }); - - var expectedBounds = childBounds.Inflate(effect.GetEffectOutputPadding()); - - Assert.Equal(expectedBounds, node.Bounds); - } - - private class MockRenderDataItem : IRenderDataItem - { - public Rect? MockBounds { get; set; } - public Rect? Bounds => MockBounds; - public bool HitTest(Point p) => false; - public void Invoke(ref RenderDataNodeRenderContext context) { } - } -} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index cd3ede7726..7ee04c32ad 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -7,7 +7,6 @@ using Avalonia.Media.Immutable; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Drawing; -using Avalonia.Rendering.Composition.Drawing.Nodes; using Avalonia.Threading; using Avalonia.UnitTests; using Moq; @@ -356,33 +355,6 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph Assert.Equal(1, op.DisposeCount); } - [Fact] - public void GlyphRun_Node_Releases_Reference_On_Direct_Dispose() - { - var glyphRunRef = RefCountable.Create(Mock.Of()); - Assert.Equal(1, glyphRunRef.RefCount); - - var node = new RenderDataGlyphRunNode { GlyphRun = glyphRunRef.Clone() }; - Assert.Equal(2, glyphRunRef.RefCount); - - node.Dispose(); - Assert.Equal(1, glyphRunRef.RefCount); - } - - [Fact] - public void GlyphRun_Node_Disposed_When_Containing_Push_Node_Disposed() - { - var glyphRunRef = RefCountable.Create(Mock.Of()); - var glyphNode = new RenderDataGlyphRunNode { GlyphRun = glyphRunRef.Clone() }; - Assert.Equal(2, glyphRunRef.RefCount); - - var pushNode = new RenderDataOpacityNode { Opacity = 0.5 }; - pushNode.Children.Add(glyphNode); - - pushNode.Dispose(); - Assert.Equal(1, glyphRunRef.RefCount); - } - [Fact] public void PushOpacityMask_Brush_Is_AddRefed_Once_And_Released_On_Dispose() { @@ -412,24 +384,6 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph Assert.False(rd.HitTest(new Point(50, 50))); } - [Fact] - public void PushGeometryClip_HitTest_Restricts_By_FillContains() - { - var geomMock = new Mock(); - geomMock.Setup(g => g.FillContains(new Point(5, 5))).Returns(true); - geomMock.Setup(g => g.FillContains(new Point(50, 50))).Returns(false); - - var node = new RenderDataGeometryClipNode { Geometry = geomMock.Object }; - node.Children.Add(new RenderDataRectangleNode - { - ServerBrush = Brushes.Black, - Rect = new RoundedRect(new Rect(0, 0, 100, 100)) - }); - - Assert.True(node.HitTest(new Point(5, 5))); - Assert.False(node.HitTest(new Point(50, 50))); - } - [Fact] public void Geometry_Node_AddRefs_Brush_And_Pen() { @@ -478,36 +432,6 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph Assert.False(rd.HitTest(new Point(50, 50))); } - [Fact] - public void PushRenderOptions_Forwards_Push_And_Pop_To_Drawing_Context_Impl() - { - var mockImpl = new Mock(); - var ctx = new RenderDataNodeRenderContext(mockImpl.Object); - var opts = new RenderOptions { EdgeMode = EdgeMode.Aliased }; - var node = new RenderDataRenderOptionsNode { RenderOptions = opts }; - - node.Push(ref ctx); - node.Pop(ref ctx); - - mockImpl.Verify(x => x.PushRenderOptions(opts), Times.Once); - mockImpl.Verify(x => x.PopRenderOptions(), Times.Once); - } - - [Fact] - public void PushTextOptions_Forwards_Push_And_Pop_To_Drawing_Context_Impl() - { - var mockImpl = new Mock(); - var ctx = new RenderDataNodeRenderContext(mockImpl.Object); - var opts = new TextOptions { TextRenderingMode = TextRenderingMode.Antialias }; - var node = new RenderDataTextOptionsNode { TextOptions = opts }; - - node.Push(ref ctx); - node.Pop(ref ctx); - - mockImpl.Verify(x => x.PushTextOptions(opts), Times.Once); - mockImpl.Verify(x => x.PopTextOptions(), Times.Once); - } - [Fact] public void Two_Distinct_Brushes_Are_AddRefed_Separately() { diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs deleted file mode 100644 index 2e36bd6392..0000000000 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Rendering.Composition.Drawing.Nodes; -using Avalonia.Rendering.SceneGraph; -using Xunit; - -namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph -{ - public class LineNodeTests - { - static RenderDataLineNode LineNode(IPen pen, Point p1, Point p2) => new RenderDataLineNode - { - P1 = p1, - P2 = p2, - ServerPen = pen, - ClientPen = pen - }; - - [Fact] - public void HitTest_Should_Be_True() - { - var lineNode = LineNode( - new Pen(Brushes.Black, 3), - new Point(15, 10), - new Point(150, 73)); - - var pointsInside = new List() - { - new Point(14, 8.9), - new Point(15, 10), - new Point(30, 15.5), - new Point(30, 18.5), - new Point(150, 73), - new Point(151, 71.9), - }; - - foreach (var point in pointsInside) - { - Assert.True(lineNode.HitTest(point)); - } - } - - [Fact] - public void HitTest_Should_Be_False() - { - var lineNode = LineNode( - new Pen(Brushes.Black, 3), - new Point(15, 10), - new Point(150, 73)); - - var pointsOutside = new List() - { - new Point(14, 8), - new Point(14, 8.8), - new Point(30, 15.3), - new Point(30, 18.7), - new Point(151, 71.8), - new Point(155, 75), - }; - - foreach (var point in pointsOutside) - { - Assert.False(lineNode.HitTest(point)); - } - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataResourcesTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataResourcesTests.cs new file mode 100644 index 0000000000..7396f9a950 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataResourcesTests.cs @@ -0,0 +1,103 @@ +using Avalonia.Rendering.Composition.Drawing; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph +{ + public class RenderDataResourcesTests + { + [Fact] + public void Intern_Null_Returns_Null_Handle() + { + var resources = new RenderDataResources(); + Assert.Equal(RenderDataResources.NullHandle, resources.Intern(null)); + Assert.Equal(0, resources.Count); + } + + [Fact] + public void Intern_Same_Reference_Returns_Same_Handle() + { + var resources = new RenderDataResources(); + var obj = new object(); + + var first = resources.Intern(obj); + var second = resources.Intern(obj); + + Assert.Equal(first, second); + Assert.Equal(1, resources.Count); + } + + [Fact] + public void Intern_Distinct_References_Return_Distinct_Handles() + { + var resources = new RenderDataResources(); + var a = resources.Intern(new object()); + var b = resources.Intern(new object()); + + Assert.NotEqual(a, b); + Assert.Equal(2, resources.Count); + } + + [Fact] + public void Intern_Equal_But_Distinct_References_Return_Distinct_Handles() + { + var resources = new RenderDataResources(); + var a = resources.Intern(new EqualByValue()); + var b = resources.Intern(new EqualByValue()); + + Assert.NotEqual(a, b); + Assert.Equal(2, resources.Count); + } + + [Fact] + public void Indexer_Returns_Interned_Resource() + { + var resources = new RenderDataResources(); + var obj = new object(); + + var handle = resources.Intern(obj); + + Assert.Same(obj, resources[handle]); + } + + [Fact] + public void Indexer_Null_Handle_Returns_Null() + { + var resources = new RenderDataResources(); + resources.Intern(new object()); + + Assert.Null(resources[RenderDataResources.NullHandle]); + } + + [Fact] + public void AppendDeserialized_Appends_Without_Deduplication() + { + var resources = new RenderDataResources(); + var obj = new object(); + + var first = resources.AppendDeserialized(obj); + var second = resources.AppendDeserialized(obj); + + Assert.NotEqual(first, second); + Assert.Equal(2, resources.Count); + Assert.Same(obj, resources[first]); + Assert.Same(obj, resources[second]); + } + + [Fact] + public void Dispose_Resets_The_Table() + { + var resources = new RenderDataResources(); + resources.Intern(new object()); + + resources.Dispose(); + + Assert.Equal(0, resources.Count); + } + + private sealed class EqualByValue + { + public override bool Equals(object? obj) => obj is EqualByValue; + public override int GetHashCode() => 1; + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamBoundsTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamBoundsTests.cs new file mode 100644 index 0000000000..1fa1f71894 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamBoundsTests.cs @@ -0,0 +1,177 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Moq; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph +{ + public class RenderDataStreamBoundsTests + { + [Fact] + public void Empty_Stream_Has_Null_Bounds() + { + using var stream = new RenderDataStream(); + Assert.Null(stream.CalculateBounds()); + } + + [Fact] + public void Filled_Rectangle_Bounds_Are_The_Rectangle() + { + using var stream = new RenderDataStream(); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + + Assert.Equal(new Rect(0, 0, 10, 10), stream.CalculateBounds()); + } + + [Fact] + public void Stroked_Rectangle_Bounds_Are_Inflated_By_Half_Thickness() + { + var pen = new ImmutablePen(Brushes.Black, 4); + + using var stream = new RenderDataStream(); + stream.DrawRectangle(null, pen, pen, + new RoundedRect(new Rect(10, 10, 20, 20)), default); + + Assert.Equal(new Rect(8, 8, 24, 24), stream.CalculateBounds()); + } + + [Fact] + public void Bounds_Are_The_Union_Of_All_Draws() + { + using var stream = new RenderDataStream(); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(20, 20, 10, 10)), default); + + Assert.Equal(new Rect(0, 0, 30, 30), stream.CalculateBounds()); + } + + [Fact] + public void Stroked_Ellipse_Bounds_Are_Inflated_By_Thickness() + { + var pen = new ImmutablePen(Brushes.Black, 4); + + using var stream = new RenderDataStream(); + stream.DrawEllipse(null, pen, pen, new Rect(0, 0, 10, 10)); + + Assert.Equal(new Rect(-4, -4, 18, 18), stream.CalculateBounds()); + } + + [Fact] + public void Bitmap_Bounds_Are_The_Destination_Rect() + { + using var bitmap = RefCountable.Create(Mock.Of()); + + using var stream = new RenderDataStream(); + stream.DrawBitmap(bitmap, 1, new Rect(0, 0, 10, 10), new Rect(5, 5, 20, 20)); + + Assert.Equal(new Rect(5, 5, 20, 20), stream.CalculateBounds()); + } + + [Fact] + public void Custom_Operation_Bounds_Are_Used() + { + var operation = new Mock(); + operation.Setup(x => x.Bounds).Returns(new Rect(1, 2, 3, 4)); + + using var stream = new RenderDataStream(); + stream.DrawCustom(operation.Object); + + Assert.Equal(new Rect(1, 2, 3, 4), stream.CalculateBounds()); + } + + [Fact] + public void Line_Bounds_Cover_The_Segment() + { + var pen = new ImmutablePen(Brushes.Black, 2); + + using var stream = new RenderDataStream(); + stream.DrawLine(pen, pen, new Point(0, 0), new Point(100, 0)); + + var bounds = Assert.NotNull(stream.CalculateBounds()); + Assert.True(bounds.Contains(new Point(50, 0))); + } + + [Fact] + public void Transform_Is_Applied_To_Bounds() + { + using var stream = new RenderDataStream(); + stream.PushTransform(Matrix.CreateTranslation(50, 50)); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + stream.Pop(); + + Assert.Equal(new Rect(50, 50, 10, 10), stream.CalculateBounds()); + } + + [Fact] + public void Nested_Transforms_Compose_For_Bounds() + { + using var stream = new RenderDataStream(); + stream.PushTransform(Matrix.CreateTranslation(20, 20)); + stream.PushTransform(Matrix.CreateScale(2, 2)); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + stream.Pop(); + stream.Pop(); + + Assert.Equal(new Rect(20, 20, 20, 20), stream.CalculateBounds()); + } + + [Fact] + public void Clip_Does_Not_Restrict_Bounds() + { + using var stream = new RenderDataStream(); + stream.PushClip(new RoundedRect(new Rect(0, 0, 5, 5))); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 100, 100)), default); + stream.Pop(); + + Assert.Equal(new Rect(0, 0, 100, 100), stream.CalculateBounds()); + } + + [Fact] + public void Opacity_Push_Is_Transparent_To_Bounds() + { + using var stream = new RenderDataStream(); + stream.PushOpacity(0.5); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + stream.Pop(); + + Assert.Equal(new Rect(0, 0, 10, 10), stream.CalculateBounds()); + } + + [Fact] + public void Empty_Push_Scope_Contributes_Nothing() + { + using var stream = new RenderDataStream(); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + stream.PushTransform(Matrix.CreateTranslation(1000, 1000)); + stream.Pop(); + + Assert.Equal(new Rect(0, 0, 10, 10), stream.CalculateBounds()); + } + + [Fact] + public void Bounds_Handles_Deeply_Nested_Scopes() + { + using var stream = new RenderDataStream(); + for (var i = 0; i < 100; i++) + stream.PushOpacity(0.5); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + for (var i = 0; i < 100; i++) + stream.Pop(); + + Assert.Equal(new Rect(0, 0, 10, 10), stream.CalculateBounds()); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEffectTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEffectTests.cs new file mode 100644 index 0000000000..bed5d7825b --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEffectTests.cs @@ -0,0 +1,29 @@ +using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph; + +public class RenderDataStreamEffectTests +{ + [Fact] + public void Effect_Inflates_Child_Bounds_By_Padding() + { + using var stream = new RenderDataStream(); + stream.PushEffect(new ImmutableBlurEffect(5), new Rect(0, 0, 100, 100)); + stream.DrawRectangle(null, null, null, new RoundedRect(new Rect(0, 0, 100, 100)), default); + stream.Pop(); + + var padding = ((IEffect)new ImmutableBlurEffect(5)).GetEffectOutputPadding(); + Assert.Equal(new Rect(0, 0, 100, 100).Inflate(padding), stream.CalculateBounds()); + } + + [Fact] + public void Empty_Effect_Scope_Has_Null_Bounds() + { + using var stream = new RenderDataStream(); + stream.PushEffect(new ImmutableBlurEffect(5), new Rect(0, 0, 100, 100)); + stream.Pop(); + Assert.Null(stream.CalculateBounds()); + } +} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEllipseHitTestTests.cs similarity index 53% rename from tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs rename to tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEllipseHitTestTests.cs index 89cc63edec..bd81f5391d 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEllipseHitTestTests.cs @@ -1,12 +1,11 @@ -using Avalonia.Media; +using Avalonia.Media; using Avalonia.Media.Immutable; -using Avalonia.Rendering.Composition.Drawing.Nodes; -using Avalonia.Rendering.SceneGraph; +using Avalonia.Rendering.Composition.Drawing; using Xunit; -namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph { - public class EllipseNodeTests + public class RenderDataStreamEllipseHitTestTests { [Theory] [InlineData(50, 50, true)] @@ -19,15 +18,10 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void FillOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new RenderDataEllipseNode() - { - Rect = new Rect(0, 0, 100, 100), - ServerBrush = Brushes.Black - }; - - var point = new Point(x, y); + using var stream = new RenderDataStream(); + stream.DrawEllipse(Brushes.Black, null, null, new Rect(0, 0, 100, 100)); - Assert.True(ellipseNode.HitTest(point) == inside); + Assert.Equal(inside, stream.HitTest(new Point(x, y))); } [Theory] @@ -43,16 +37,11 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph public void StrokeOnly_HitTest(double x, double y, bool inside) { var pen = new ImmutablePen(Brushes.Black, 2); - var ellipseNode = new RenderDataEllipseNode() - { - Rect = new Rect(0, 0, 100, 100), - ServerPen = pen, - ClientPen = pen - }; - var point = new Point(x, y); + using var stream = new RenderDataStream(); + stream.DrawEllipse(null, pen, pen, new Rect(0, 0, 100, 100)); - Assert.Equal(inside, ellipseNode.HitTest(point)); + Assert.Equal(inside, stream.HitTest(new Point(x, y))); } } } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamHitTestTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamHitTestTests.cs new file mode 100644 index 0000000000..65dc9470f3 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamHitTestTests.cs @@ -0,0 +1,208 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Moq; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph +{ + public class RenderDataStreamHitTestTests + { + [Fact] + public void Filled_Rectangle_Is_Hit_Inside_And_Missed_Outside() + { + using var stream = new RenderDataStream(); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 100, 100)), default); + + Assert.True(stream.HitTest(new Point(50, 50))); + Assert.False(stream.HitTest(new Point(150, 150))); + } + + [Fact] + public void Stroked_Rectangle_Is_Hit_On_Border_And_Missed_In_Hollow_Center() + { + var pen = new ImmutablePen(Brushes.Black, 4); + + using var stream = new RenderDataStream(); + stream.DrawRectangle(null, pen, pen, + new RoundedRect(new Rect(0, 0, 100, 100)), default); + + Assert.True(stream.HitTest(new Point(0, 50))); + Assert.False(stream.HitTest(new Point(50, 50))); + } + + [Fact] + public void Line_Is_Hit_Along_Its_Length() + { + var pen = new ImmutablePen(Brushes.Black, 4); + + using var stream = new RenderDataStream(); + stream.DrawLine(pen, pen, new Point(0, 0), new Point(100, 0)); + + Assert.True(stream.HitTest(new Point(50, 1))); + Assert.False(stream.HitTest(new Point(50, 50))); + } + + [Fact] + public void Filled_Ellipse_Is_Hit_At_Center_And_Missed_At_Corner() + { + using var stream = new RenderDataStream(); + stream.DrawEllipse(Brushes.Black, null, null, new Rect(0, 0, 100, 100)); + + Assert.True(stream.HitTest(new Point(50, 50))); + Assert.False(stream.HitTest(new Point(2, 2))); + } + + [Fact] + public void Geometry_Is_Hit_Via_FillContains() + { + var geometry = new Mock(); + geometry.Setup(x => x.FillContains(new Point(5, 5))).Returns(true); + geometry.Setup(x => x.FillContains(new Point(50, 50))).Returns(false); + + using var stream = new RenderDataStream(); + stream.DrawGeometry(Brushes.Black, null, null, geometry.Object); + + Assert.True(stream.HitTest(new Point(5, 5))); + Assert.False(stream.HitTest(new Point(50, 50))); + } + + [Fact] + public void Bitmap_Is_Hit_Within_Its_Destination_Rect() + { + using var bitmap = RefCountable.Create(Mock.Of()); + + using var stream = new RenderDataStream(); + stream.DrawBitmap(bitmap, 1, new Rect(0, 0, 10, 10), new Rect(20, 20, 30, 30)); + + Assert.True(stream.HitTest(new Point(25, 25))); + Assert.False(stream.HitTest(new Point(5, 5))); + } + + [Fact] + public void Custom_Operation_Hit_Test_Is_Delegated() + { + var operation = new Mock(); + operation.Setup(x => x.HitTest(new Point(5, 5))).Returns(true); + + using var stream = new RenderDataStream(); + stream.DrawCustom(operation.Object); + + Assert.True(stream.HitTest(new Point(5, 5))); + Assert.False(stream.HitTest(new Point(99, 99))); + } + + [Fact] + public void Clip_Restricts_The_Hit_Region() + { + using var stream = new RenderDataStream(); + stream.PushClip(new RoundedRect(new Rect(0, 0, 10, 10))); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 100, 100)), default); + stream.Pop(); + + Assert.True(stream.HitTest(new Point(5, 5))); + Assert.False(stream.HitTest(new Point(50, 50))); + } + + [Fact] + public void Geometry_Clip_Restricts_The_Hit_Region() + { + var geometry = new Mock(); + geometry.Setup(x => x.FillContains(new Point(5, 5))).Returns(true); + geometry.Setup(x => x.FillContains(new Point(50, 50))).Returns(false); + + using var stream = new RenderDataStream(); + stream.PushGeometryClip(geometry.Object); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 100, 100)), default); + stream.Pop(); + + Assert.True(stream.HitTest(new Point(5, 5))); + Assert.False(stream.HitTest(new Point(50, 50))); + } + + [Fact] + public void Transform_Maps_Hit_Test_Coordinates() + { + using var stream = new RenderDataStream(); + stream.PushTransform(Matrix.CreateTranslation(50, 50)); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + stream.Pop(); + + Assert.True(stream.HitTest(new Point(55, 55))); + Assert.False(stream.HitTest(new Point(5, 5))); + } + + [Fact] + public void Nested_Transforms_Compose() + { + using var stream = new RenderDataStream(); + stream.PushTransform(Matrix.CreateTranslation(20, 20)); + stream.PushTransform(Matrix.CreateScale(2, 2)); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + stream.Pop(); + stream.Pop(); + + Assert.True(stream.HitTest(new Point(30, 30))); + Assert.False(stream.HitTest(new Point(5, 5))); + } + + [Fact] + public void Singular_Transform_Excludes_Its_Scope() + { + using var stream = new RenderDataStream(); + stream.PushTransform(new Matrix()); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 100, 100)), default); + stream.Pop(); + + Assert.False(stream.HitTest(new Point(50, 50))); + } + + [Fact] + public void Opacity_Push_Is_Transparent_To_Hit_Testing() + { + using var stream = new RenderDataStream(); + stream.PushOpacity(0.5); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 100, 100)), default); + stream.Pop(); + + Assert.True(stream.HitTest(new Point(50, 50))); + } + + [Fact] + public void Scope_State_Is_Restored_After_Pop() + { + using var stream = new RenderDataStream(); + stream.PushTransform(Matrix.CreateTranslation(1000, 1000)); + stream.Pop(); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + + Assert.True(stream.HitTest(new Point(5, 5))); + } + + [Fact] + public void Hit_Test_Handles_Deeply_Nested_Scopes() + { + using var stream = new RenderDataStream(); + for (var i = 0; i < 100; i++) + stream.PushOpacity(0.5); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + for (var i = 0; i < 100; i++) + stream.Pop(); + + Assert.True(stream.HitTest(new Point(5, 5))); + Assert.False(stream.HitTest(new Point(50, 50))); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamLineHitTestTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamLineHitTestTests.cs new file mode 100644 index 0000000000..10ce87a7b9 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamLineHitTestTests.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph +{ + public class RenderDataStreamLineHitTestTests + { + private static RenderDataStream LineStream(IPen pen, Point p1, Point p2) + { + var stream = new RenderDataStream(); + stream.DrawLine(pen, pen, p1, p2); + return stream; + } + + [Fact] + public void HitTest_Should_Be_True() + { + using var stream = LineStream(new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); + + var pointsInside = new List + { + new Point(14, 8.9), + new Point(15, 10), + new Point(30, 15.5), + new Point(30, 18.5), + new Point(150, 73), + new Point(151, 71.9), + }; + + foreach (var point in pointsInside) + Assert.True(stream.HitTest(point)); + } + + [Fact] + public void HitTest_Should_Be_False() + { + using var stream = LineStream(new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); + + var pointsOutside = new List + { + new Point(14, 8), + new Point(14, 8.8), + new Point(30, 15.3), + new Point(30, 18.7), + new Point(151, 71.8), + new Point(155, 75), + }; + + foreach (var point in pointsOutside) + Assert.False(stream.HitTest(point)); + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamSerializationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamSerializationTests.cs new file mode 100644 index 0000000000..4abae13451 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamSerializationTests.cs @@ -0,0 +1,98 @@ +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Transport; +using Moq; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph +{ + public class RenderDataStreamSerializationTests + { + [Fact] + public void Round_Trip_Preserves_Bounds() + { + using var source = new RenderDataStream(); + source.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + source.PushTransform(Matrix.CreateTranslation(40, 40)); + source.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + source.Pop(); + + using var result = RoundTrip(source); + + Assert.Equal(source.CalculateBounds(), result.CalculateBounds()); + Assert.Equal(new Rect(0, 0, 50, 50), result.CalculateBounds()); + } + + [Fact] + public void Round_Trip_Preserves_Hit_Testing() + { + using var source = new RenderDataStream(); + source.PushClip(new RoundedRect(new Rect(0, 0, 10, 10))); + source.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 100, 100)), default); + source.Pop(); + + using var result = RoundTrip(source); + + Assert.True(result.HitTest(new Point(5, 5))); + Assert.False(result.HitTest(new Point(50, 50))); + } + + [Fact] + public void Round_Trip_Preserves_Resource_References() + { + var brush = Mock.Of(); + using var source = new RenderDataStream(); + source.DrawRectangle(brush, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + + using var result = RoundTrip(source); + + var context = new Mock(); + result.Replay(context.Object); + context.Verify(x => x.DrawRectangle(brush, null, + It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void Round_Trip_Of_Empty_Stream_Produces_Empty_Stream() + { + using var source = new RenderDataStream(); + using var result = RoundTrip(source); + + Assert.Null(result.CalculateBounds()); + } + + [Fact] + public void Round_Trip_Spanning_Multiple_Stream_Segments() + { + using var source = new RenderDataStream(); + for (var i = 0; i < 50; i++) + source.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(i, i, 1, 1)), default); + + using var result = RoundTrip(source); + + Assert.Equal(source.CalculateBounds(), result.CalculateBounds()); + } + + private static RenderDataStream RoundTrip(RenderDataStream source) + { + var data = new BatchStreamData(); + var memoryPool = new BatchStreamMemoryPool(false, 64, _ => { }); + var objectPool = new BatchStreamObjectPool(false, 8, _ => { }); + + using (var writer = new BatchStreamWriter(data, memoryPool, objectPool)) + source.SerializeTo(writer); + + var result = new RenderDataStream(); + using (var reader = new BatchStreamReader(data, memoryPool, objectPool)) + result.DeserializeFrom(reader); + + return result; + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamTests.cs new file mode 100644 index 0000000000..943ae19994 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamTests.cs @@ -0,0 +1,228 @@ +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Moq; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph +{ + public class RenderDataStreamTests + { + [Fact] + public void Replay_Forwards_Line() + { + var pen = Mock.Of(); + var context = new Mock(); + + using var stream = new RenderDataStream(); + stream.DrawLine(pen, pen, new Point(1, 2), new Point(3, 4)); + stream.Replay(context.Object); + + context.Verify(x => x.DrawLine(pen, new Point(1, 2), new Point(3, 4)), Times.Once); + } + + [Fact] + public void Replay_Forwards_Rectangle_With_Box_Shadows() + { + var brush = Mock.Of(); + var pen = Mock.Of(); + var rect = new RoundedRect(new Rect(0, 0, 10, 20)); + var shadows = new BoxShadows( + new BoxShadow { Blur = 1 }, + new[] { new BoxShadow { Blur = 2 } }); + var context = new Mock(); + + using var stream = new RenderDataStream(); + stream.DrawRectangle(brush, pen, pen, rect, shadows); + stream.Replay(context.Object); + + context.Verify(x => x.DrawRectangle(brush, pen, rect, + It.Is(s => s.Count == 2)), Times.Once); + } + + [Fact] + public void Replay_Forwards_Custom_Operation() + { + var operation = new Mock(); + var context = new Mock(); + + using var stream = new RenderDataStream(); + stream.DrawCustom(operation.Object); + stream.Replay(context.Object); + + operation.Verify(x => x.Render(It.IsAny()), Times.Once); + } + + [Fact] + public void Replay_Pop_Dispatches_To_Matching_Pop_In_Lifo_Order() + { + var calls = new List(); + var context = RecordingContext(calls); + + using var stream = new RenderDataStream(); + stream.PushClip(new RoundedRect(new Rect(0, 0, 10, 10))); + stream.PushOpacity(0.5); + stream.Pop(); + stream.Pop(); + stream.Replay(context.Object); + + Assert.Equal(new[] { "PushClip", "PushOpacity", "PopOpacity", "PopClip" }, calls); + } + + [Fact] + public void Replay_Applies_And_Restores_Transform() + { + var context = new Mock(); + context.SetupProperty(x => x.Transform); + context.Object.Transform = Matrix.Identity; + var matrix = Matrix.CreateTranslation(5, 7); + + using var stream = new RenderDataStream(); + stream.PushTransform(matrix); + stream.Pop(); + + stream.Replay(context.Object); + + Assert.Equal(Matrix.Identity, context.Object.Transform); + } + + [Fact] + public void Replay_Skips_Opacity_One_Push() + { + var calls = new List(); + var context = RecordingContext(calls); + + using var stream = new RenderDataStream(); + stream.PushOpacity(1); + stream.Pop(); + stream.Replay(context.Object); + + Assert.Empty(calls); + } + + [Fact] + public void Replay_Skips_Null_Geometry_Clip() + { + var calls = new List(); + var context = RecordingContext(calls); + + using var stream = new RenderDataStream(); + stream.PushGeometryClip(null); + stream.Pop(); + stream.Replay(context.Object); + + Assert.Empty(calls); + } + + [Fact] + public void Replay_Walks_Nested_Pushes_In_Order() + { + var calls = new List(); + var context = RecordingContext(calls); + + using var stream = new RenderDataStream(); + stream.PushClip(new RoundedRect(new Rect(0, 0, 10, 10))); + stream.PushGeometryClip(Mock.Of()); + stream.Pop(); + stream.Pop(); + stream.Replay(context.Object); + + Assert.Equal( + new[] { "PushClip", "PushGeometryClip", "PopGeometryClip", "PopClip" }, + calls); + } + + [Fact] + public void Replay_Forwards_Render_Options() + { + var options = new RenderOptions { EdgeMode = EdgeMode.Aliased }; + var context = new Mock(); + + using var stream = new RenderDataStream(); + stream.PushRenderOptions(options); + stream.Pop(); + stream.Replay(context.Object); + + context.Verify(x => x.PushRenderOptions(options), Times.Once); + context.Verify(x => x.PopRenderOptions(), Times.Once); + } + + [Fact] + public void Replay_Forwards_Text_Options() + { + var options = new TextOptions { TextRenderingMode = TextRenderingMode.Antialias }; + var context = new Mock(); + + using var stream = new RenderDataStream(); + stream.PushTextOptions(options); + stream.Pop(); + stream.Replay(context.Object); + + context.Verify(x => x.PushTextOptions(options), Times.Once); + context.Verify(x => x.PopTextOptions(), Times.Once); + } + + [Fact] + public void Dispose_Resources_Disposes_Owned_Resources() + { + var bitmap = RefCountable.Create(Mock.Of()); + var glyphRun = RefCountable.Create(Mock.Of()); + var operation = new Mock(); + + using (var stream = new RenderDataStream()) + { + stream.DrawBitmap(bitmap.Clone(), 1, new Rect(0, 0, 1, 1), new Rect(0, 0, 1, 1)); + stream.DrawGlyphRun(null, glyphRun.Clone()); + stream.DrawCustom(operation.Object); + + Assert.Equal(2, bitmap.RefCount); + Assert.Equal(2, glyphRun.RefCount); + + stream.DisposeResources(); + } + + Assert.Equal(1, bitmap.RefCount); + Assert.Equal(1, glyphRun.RefCount); + operation.Verify(x => x.Dispose(), Times.Once); + + bitmap.Dispose(); + glyphRun.Dispose(); + } + + [Fact] + public void Replay_Handles_Deeply_Nested_Scopes() + { + var context = new Mock(); + + using var stream = new RenderDataStream(); + for (var i = 0; i < 100; i++) + stream.PushOpacity(0.5); + stream.DrawRectangle(Brushes.Black, null, null, + new RoundedRect(new Rect(0, 0, 10, 10)), default); + for (var i = 0; i < 100; i++) + stream.Pop(); + + stream.Replay(context.Object); + + context.Verify(x => x.DrawRectangle(Brushes.Black, null, + It.IsAny(), It.IsAny()), Times.Once); + } + + private static Mock RecordingContext(List calls) + { + var context = new Mock(); + context.Setup(x => x.PushClip(It.IsAny())).Callback(() => calls.Add("PushClip")); + context.Setup(x => x.PopClip()).Callback(() => calls.Add("PopClip")); + context.Setup(x => x.PushGeometryClip(It.IsAny())) + .Callback(() => calls.Add("PushGeometryClip")); + context.Setup(x => x.PopGeometryClip()).Callback(() => calls.Add("PopGeometryClip")); + context.Setup(x => x.PushOpacity(It.IsAny(), It.IsAny())) + .Callback(() => calls.Add("PushOpacity")); + context.Setup(x => x.PopOpacity()).Callback(() => calls.Add("PopOpacity")); + return context; + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataWriterReaderTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataWriterReaderTests.cs new file mode 100644 index 0000000000..97fa1d8740 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataWriterReaderTests.cs @@ -0,0 +1,237 @@ +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Rendering.Composition.Drawing; +using Xunit; + +namespace Avalonia.Base.UnitTests.Rendering.SceneGraph +{ + public class RenderDataWriterReaderTests + { + [Fact] + public void Primitives_Round_Trip() + { + var writer = new RenderDataWriter(); + try + { + writer.Write(200); + writer.WriteOpcode(RenderDataOpcode.DrawGeometry); + writer.Write(-123456); + writer.Write(4000000000u); + writer.Write(3.14159); + writer.Write(true); + writer.Write(false); + + var reader = new RenderDataReader(writer.Written); + Assert.Equal(200, reader.Read()); + Assert.Equal(RenderDataOpcode.DrawGeometry, reader.Read()); + Assert.Equal(-123456, reader.Read()); + Assert.Equal(4000000000u, reader.Read()); + Assert.Equal(3.14159, reader.Read()); + Assert.True(reader.Read()); + Assert.False(reader.Read()); + Assert.True(reader.IsAtEnd); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public void Geometric_Structs_Round_Trip() + { + var point = new Point(1, 2); + var vector = new Vector(3, 4); + var rect = new Rect(5, 6, 7, 8); + var roundedRect = new RoundedRect(new Rect(1, 2, 30, 40), + new Vector(1, 1), new Vector(2, 2), new Vector(3, 3), new Vector(4, 4)); + var matrix = new Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9); + + var writer = new RenderDataWriter(); + try + { + writer.Write(point); + writer.Write(vector); + writer.Write(rect); + writer.Write(roundedRect); + writer.Write(matrix); + + var reader = new RenderDataReader(writer.Written); + Assert.Equal(point, reader.Read()); + Assert.Equal(vector, reader.Read()); + Assert.Equal(rect, reader.Read()); + + var readRounded = reader.Read(); + Assert.Equal(roundedRect.Rect, readRounded.Rect); + Assert.Equal(roundedRect.RadiiTopLeft, readRounded.RadiiTopLeft); + Assert.Equal(roundedRect.RadiiTopRight, readRounded.RadiiTopRight); + Assert.Equal(roundedRect.RadiiBottomRight, readRounded.RadiiBottomRight); + Assert.Equal(roundedRect.RadiiBottomLeft, readRounded.RadiiBottomLeft); + + Assert.Equal(matrix, reader.Read()); + Assert.True(reader.IsAtEnd); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public void Color_And_BoxShadow_Round_Trip() + { + var color = Color.FromArgb(10, 20, 30, 40); + var shadow = new BoxShadow + { + OffsetX = 1.5, + OffsetY = -2.5, + Blur = 3, + Spread = 4, + Color = Color.FromArgb(255, 1, 2, 3), + IsInset = true + }; + + var writer = new RenderDataWriter(); + try + { + writer.Write(color); + writer.Write(shadow); + + var reader = new RenderDataReader(writer.Written); + Assert.Equal(color, reader.Read()); + + var readShadow = reader.Read(); + Assert.Equal(shadow.OffsetX, readShadow.OffsetX); + Assert.Equal(shadow.OffsetY, readShadow.OffsetY); + Assert.Equal(shadow.Blur, readShadow.Blur); + Assert.Equal(shadow.Spread, readShadow.Spread); + Assert.Equal(shadow.Color, readShadow.Color); + Assert.Equal(shadow.IsInset, readShadow.IsInset); + Assert.True(reader.IsAtEnd); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public void RenderOptions_And_TextOptions_Round_Trip() + { + var renderOptions = new RenderOptions + { +#pragma warning disable CS0618 + TextRenderingMode = TextRenderingMode.Antialias, +#pragma warning restore CS0618 + BitmapInterpolationMode = BitmapInterpolationMode.HighQuality, + EdgeMode = EdgeMode.Aliased, + BitmapBlendingMode = BitmapBlendingMode.Plus, + RequiresFullOpacityHandling = true + }; + var textOptions = new TextOptions + { + TextRenderingMode = TextRenderingMode.SubpixelAntialias, + TextHintingMode = TextHintingMode.Light, + BaselinePixelAlignment = BaselinePixelAlignment.Aligned + }; + + var writer = new RenderDataWriter(); + try + { + writer.Write(renderOptions); + writer.Write(textOptions); + + var reader = new RenderDataReader(writer.Written); + Assert.Equal(renderOptions, reader.Read()); + Assert.Equal(textOptions, reader.Read()); + Assert.True(reader.IsAtEnd); + } + finally + { + writer.Dispose(); + } + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void Nullable_Boolean_Round_Trips_All_Three_States(bool? value) + { + var writer = new RenderDataWriter(); + try + { + writer.Write(new RenderOptions { RequiresFullOpacityHandling = value }); + + var reader = new RenderDataReader(writer.Written); + Assert.Equal(value, reader.Read().RequiresFullOpacityHandling); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public void Length_Tracks_Written_Bytes() + { + var writer = new RenderDataWriter(); + try + { + Assert.Equal(0, writer.Length); + writer.Write(1); + Assert.Equal(1, writer.Length); + writer.Write(2); + Assert.Equal(5, writer.Length); + writer.Write(3d); + Assert.Equal(13, writer.Length); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public void Writer_Grows_Buffer_To_Fit_Large_Payloads() + { + var writer = new RenderDataWriter(); + try + { + for (var i = 0; i < 1000; i++) + writer.Write(i); + + Assert.Equal(4000, writer.Length); + + var reader = new RenderDataReader(writer.Written); + for (var i = 0; i < 1000; i++) + Assert.Equal(i, reader.Read()); + Assert.True(reader.IsAtEnd); + } + finally + { + writer.Dispose(); + } + } + + [Fact] + public void Payload_Auto_Prepends_Opcode() + { + var writer = new RenderDataWriter(); + try + { + writer.WritePayload(new PushOpacityPayload { Opacity = 0.5 }); + + var reader = new RenderDataReader(writer.Written); + Assert.Equal(RenderDataOpcode.PushOpacity, reader.Read()); + var payload = reader.Read(); + Assert.Equal(0.5, payload.Opacity); + Assert.True(reader.IsAtEnd); + } + finally + { + writer.Dispose(); + } + } + } +}