Browse Source

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
pull/21591/head
Matt 2 months ago
committed by GitHub
parent
commit
aa1fcb4871
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 50
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs
  2. 30
      src/Avalonia.Base/Rendering/Composition/Drawing/IRenderDataVisitor.cs
  3. 54
      src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs
  4. 28
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs
  5. 61
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs
  6. 29
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
  7. 35
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs
  8. 65
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs
  9. 294
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs
  10. 27
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs
  11. 25
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs
  12. 46
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
  13. 370
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  14. 22
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataOpcode.cs
  15. 132
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataPayloads.cs
  16. 42
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataReader.cs
  17. 54
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataResources.cs
  18. 93
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Bounds.cs
  19. 250
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.HitTest.cs
  20. 159
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Replay.cs
  21. 177
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.Visit.cs
  22. 226
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataStream.cs
  23. 57
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataWriter.cs
  24. 94
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs
  25. 46
      src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs
  26. 50
      tests/Avalonia.Base.UnitTests/Rendering/RenderDataEffectNodeTests.cs
  27. 76
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
  28. 67
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs
  29. 103
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataResourcesTests.cs
  30. 177
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamBoundsTests.cs
  31. 29
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEffectTests.cs
  32. 31
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamEllipseHitTestTests.cs
  33. 208
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamHitTestTests.cs
  34. 55
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamLineHitTestTests.cs
  35. 98
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamSerializationTests.cs
  36. 228
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataStreamTests.cs
  37. 237
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/RenderDataWriterReaderTests.cs

50
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<ICompositionRenderResource> _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<ICompositionRenderResource> _resources;
private PooledInlineList<IRenderDataItem> _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;
}
}
public bool HitTest(Point pt) => _stream.HitTest(pt);
}

30
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<TScope> 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<IGlyphRunImpl>? glyphRun);
void OnDrawBitmap(IRef<IBitmapImpl>? 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);
}

54
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<IRenderDataItem>? _items;
private readonly ThreadSafeObjectPool<List<IRenderDataItem>> _pool;
private RenderDataStream? _stream;
public ImmediateRenderDataSceneBrushContent(ITileBrush brush, List<IRenderDataItem> items, Rect? rect,
bool useScalableRasterization, ThreadSafeObjectPool<List<IRenderDataItem>> 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; }
}
}

28
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs

@ -1,28 +0,0 @@
using System;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Drawing.Nodes;
class RenderDataBitmapNode : IRenderDataItem, IDisposable
{
public IRef<IBitmapImpl>? 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;
}
}

61
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs

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

29
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs

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

35
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs

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

65
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs

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

294
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs

@ -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<Matrix>? _stack;
private static readonly ThreadSafeObjectPool<Stack<Matrix>> s_matrixStackPool = new();
public RenderDataNodeRenderContext(IDrawingContextImpl context)
{
Context = context;
}
public IDrawingContextImpl Context { get; }
public Stack<Matrix> MatrixStack
{
get => _stack ??= s_matrixStackPool.Get();
}
public void Dispose()
{
if (_stack != null)
{
_stack.Clear();
s_matrixStackPool.ReturnAndSetNull(ref _stack);
}
}
}
interface IRenderDataItem
{
/// <summary>
/// Renders the node to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
void Invoke(ref RenderDataNodeRenderContext context);
/// <summary>
/// Gets the bounds of the visible content in the node in global coordinates.
/// </summary>
Rect? Bounds { get; }
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to childs, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
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<IRenderDataItem> 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();
}
}
/// <summary>
/// A render data node that pushes an effect.
/// </summary>
class RenderDataEffectNode : RenderDataPushNode
{
/// <summary>
/// Gets or sets the effect to push.
/// </summary>
public IEffect? Effect { get; set; }
/// <summary>
/// Gets or sets the bounds of the effect.
/// </summary>
public Rect BoundsRect { get; set; }
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
public override void Push(ref RenderDataNodeRenderContext context)
{
if (Effect != null && context.Context is IDrawingContextImplWithEffects effectImpl)
effectImpl.PushEffect(BoundsRect, Effect);
}
/// <inheritdoc />
public override void Pop(ref RenderDataNodeRenderContext context)
{
if (Effect != null && context.Context is IDrawingContextImplWithEffects effectImpl)
effectImpl.PopEffect();
}
}

27
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs

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

25
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs

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

46
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs

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

370
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<object>? _resourcesHashSet;
private Stack<PushEntry>? _pushStack;
private static readonly ThreadSafeObjectPool<HashSet<object>> s_hashSetPool = new();
private CompositionRenderData RenderData
{
get
{
Debug.Assert(_compositor != null);
return _renderData ??= new(_compositor);
}
}
struct ParentStackItem
private static readonly ThreadSafeObjectPool<Stack<PushEntry>> s_pushStackPool = new();
private struct PushEntry
{
public RenderDataPushNode? Node;
public List<IRenderDataItem> Items;
public bool Emitted;
public int PositionBefore;
public int PositionAfter;
public int DepthBefore;
}
private List<IRenderDataItem>? _currentItemList;
private static readonly ThreadSafeObjectPool<List<IRenderDataItem>> s_listPool = new();
private Stack<ParentStackItem>? _parentNodeStack;
private static readonly ThreadSafeObjectPool<Stack<ParentStackItem>> 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<T>() 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<IBitmapImpl>? 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);
}
/// <inheritdoc />
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<RenderDataClipNode>();
protected override void PopClipCore() => PopCore();
protected override void PopGeometryClipCore() => Pop<RenderDataGeometryClipNode>();
protected override void PopGeometryClipCore() => PopCore();
protected override void PopOpacityCore() => Pop<RenderDataOpacityNode>();
protected override void PopOpacityCore() => PopCore();
protected override void PopOpacityMaskCore() => Pop<RenderDataOpacityMaskNode>();
protected override void PopOpacityMaskCore() => PopCore();
protected override void PopTransformCore() => Pop<RenderDataPushMatrixNode>();
protected override void PopTransformCore() => PopCore();
protected override void PopRenderOptionsCore() => Pop<RenderDataRenderOptionsNode>();
protected override void PopRenderOptionsCore() => PopCore();
protected override void PopTextOptionsCore() => Pop<RenderDataTextOptionsNode>();
protected override void PopTextOptionsCore() => PopCore();
/// <inheritdoc />
protected override void PopEffectCore() => Pop<RenderDataEffectNode>();
protected override void PopEffectCore() => PopCore();
internal override void DrawBitmap(IRef<IBitmapImpl>? 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<IRenderDataItem>();
}
}
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);
}
}

22
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
}

132
src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataPayloads.cs

@ -0,0 +1,132 @@
using Avalonia.Media;
namespace Avalonia.Rendering.Composition.Drawing;
internal interface IRenderDataPayload<TSelf> where TSelf : unmanaged, IRenderDataPayload<TSelf>
{
static abstract RenderDataOpcode Opcode { get; }
}
internal struct DrawLinePayload : IRenderDataPayload<DrawLinePayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawLine;
public int ServerPen;
public int ClientPen;
public Point P1;
public Point P2;
}
internal struct DrawRectanglePayload : IRenderDataPayload<DrawRectanglePayload>
{
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<DrawEllipsePayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawEllipse;
public int ServerBrush;
public int ServerPen;
public int ClientPen;
public Rect Rect;
}
internal struct DrawGeometryPayload : IRenderDataPayload<DrawGeometryPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawGeometry;
public int ServerBrush;
public int ServerPen;
public int ClientPen;
public int Geometry;
}
internal struct DrawGlyphRunPayload : IRenderDataPayload<DrawGlyphRunPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawGlyphRun;
public int ServerBrush;
public int GlyphRun;
}
internal struct DrawBitmapPayload : IRenderDataPayload<DrawBitmapPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawBitmap;
public int Bitmap;
public double Opacity;
public Rect SourceRect;
public Rect DestRect;
}
internal struct DrawCustomPayload : IRenderDataPayload<DrawCustomPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.DrawCustom;
public int Operation;
}
internal struct PushClipPayload : IRenderDataPayload<PushClipPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.PushClip;
public RoundedRect Clip;
}
internal struct PushGeometryClipPayload : IRenderDataPayload<PushGeometryClipPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.PushGeometryClip;
public int Geometry;
}
internal struct PushOpacityPayload : IRenderDataPayload<PushOpacityPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.PushOpacity;
public double Opacity;
}
internal struct PushOpacityMaskPayload : IRenderDataPayload<PushOpacityMaskPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.PushOpacityMask;
public int Brush;
public Rect Bounds;
}
internal struct PushTransformPayload : IRenderDataPayload<PushTransformPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.PushTransform;
public Matrix Matrix;
}
internal struct PushRenderOptionsPayload : IRenderDataPayload<PushRenderOptionsPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.PushRenderOptions;
public RenderOptions Options;
}
internal struct PushTextOptionsPayload : IRenderDataPayload<PushTextOptionsPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.PushTextOptions;
public TextOptions Options;
}
internal struct PushEffectPayload : IRenderDataPayload<PushEffectPayload>
{
public static RenderDataOpcode Opcode => RenderDataOpcode.PushEffect;
public int Effect;
public Rect Bounds;
}

42
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<byte> _buffer;
private int _position;
public RenderDataReader(ReadOnlySpan<byte> buffer)
{
_buffer = buffer;
_position = 0;
}
public int Position => _position;
public bool IsAtEnd => _position >= _buffer.Length;
public ReadOnlySpan<byte> Take(int count)
{
var span = _buffer.Slice(_position, count);
_position += count;
return span;
}
public T Read<T>() where T : unmanaged
=> MemoryMarshal.Read<T>(Take(Unsafe.SizeOf<T>()));
public T Peek<T>() where T : unmanaged
=> MemoryMarshal.Read<T>(_buffer.Slice(_position, Unsafe.SizeOf<T>()));
public T ReadPayload<T>() where T : unmanaged, IRenderDataPayload<T>
{
var opcode = Read<RenderDataOpcode>();
Debug.Assert(opcode == T.Opcode);
return Read<T>();
}
}

54
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<object?>? _resources;
private Dictionary<object, int>? _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<object?>();
_internMap ??= new Dictionary<object, int>(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<object?>();
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;
}
}

93
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<BoundsScope>
{
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<IGlyphRunImpl>? glyphRun)
=> Current = Rect.Union(Current, glyphRun?.Item?.Bounds ?? default);
public void OnDrawBitmap(IRef<IBitmapImpl>? 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<BoundsVisitor, BoundsScope>(ref visitor);
return visitor.Current;
}
}

250
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<HitTestScope>
{
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<IGlyphRunImpl>? glyphRun)
{
if (Live && glyphRun != null && glyphRun.Item.Bounds.ContainsExclusive(Current))
Hit();
}
public void OnDrawBitmap(IRef<IBitmapImpl>? 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<HitTestVisitor, HitTestScope>(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;
}
}

159
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<ReplayScope>
{
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<IGlyphRunImpl>? glyphRun)
{
if (glyphRun != null)
_context.DrawGlyphRun(serverBrush, glyphRun.Item);
}
public void OnDrawBitmap(IRef<IBitmapImpl>? 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<ReplayVisitor, ReplayScope>(ref visitor);
}
}

177
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<TVisitor, TScope>(ref TVisitor visitor)
where TVisitor : struct, IRenderDataVisitor<TScope>
where TScope : unmanaged
{
var reader = new RenderDataReader(_writer.Written);
TScope[]? rented = null;
scoped Span<TScope> scopes;
if (_maxDepth == 0)
scopes = default;
else if (_maxDepth <= MaxStackScopeDepth)
scopes = stackalloc TScope[_maxDepth];
else
scopes = rented = ArrayPool<TScope>.Shared.Rent(_maxDepth);
var depth = 0;
try
{
while (!visitor.StopVisiting && !reader.IsAtEnd)
{
switch (reader.Peek<RenderDataOpcode>())
{
case RenderDataOpcode.DrawLine:
{
var p = reader.ReadPayload<DrawLinePayload>();
visitor.OnDrawLine(
(IPen?)_resources[p.ServerPen],
(IPen?)_resources[p.ClientPen],
p.P1, p.P2);
break;
}
case RenderDataOpcode.DrawRectangle:
{
var p = reader.ReadPayload<DrawRectanglePayload>();
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<DrawEllipsePayload>();
visitor.OnDrawEllipse(
(IBrush?)_resources[p.ServerBrush],
(IPen?)_resources[p.ServerPen],
(IPen?)_resources[p.ClientPen],
p.Rect);
break;
}
case RenderDataOpcode.DrawGeometry:
{
var p = reader.ReadPayload<DrawGeometryPayload>();
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<DrawGlyphRunPayload>();
visitor.OnDrawGlyphRun(
(IBrush?)_resources[p.ServerBrush],
(IRef<IGlyphRunImpl>?)_resources[p.GlyphRun]);
break;
}
case RenderDataOpcode.DrawBitmap:
{
var p = reader.ReadPayload<DrawBitmapPayload>();
visitor.OnDrawBitmap(
(IRef<IBitmapImpl>?)_resources[p.Bitmap],
p.Opacity, p.SourceRect, p.DestRect);
break;
}
case RenderDataOpcode.DrawCustom:
{
var p = reader.ReadPayload<DrawCustomPayload>();
visitor.OnDrawCustom((ICustomDrawOperation?)_resources[p.Operation]);
break;
}
case RenderDataOpcode.PushClip:
{
var p = reader.ReadPayload<PushClipPayload>();
scopes[depth++] = visitor.OnPushClip(p.Clip);
break;
}
case RenderDataOpcode.PushGeometryClip:
{
var p = reader.ReadPayload<PushGeometryClipPayload>();
scopes[depth++] = visitor.OnPushGeometryClip((IGeometryImpl?)_resources[p.Geometry]);
break;
}
case RenderDataOpcode.PushOpacity:
{
var p = reader.ReadPayload<PushOpacityPayload>();
scopes[depth++] = visitor.OnPushOpacity(p.Opacity);
break;
}
case RenderDataOpcode.PushOpacityMask:
{
var p = reader.ReadPayload<PushOpacityMaskPayload>();
scopes[depth++] = visitor.OnPushOpacityMask(
(IBrush?)_resources[p.Brush], p.Bounds);
break;
}
case RenderDataOpcode.PushTransform:
{
var p = reader.ReadPayload<PushTransformPayload>();
scopes[depth++] = visitor.OnPushTransform(p.Matrix);
break;
}
case RenderDataOpcode.PushRenderOptions:
{
var p = reader.ReadPayload<PushRenderOptionsPayload>();
scopes[depth++] = visitor.OnPushRenderOptions(p.Options);
break;
}
case RenderDataOpcode.PushTextOptions:
{
var p = reader.ReadPayload<PushTextOptionsPayload>();
scopes[depth++] = visitor.OnPushTextOptions(p.Options);
break;
}
case RenderDataOpcode.PushEffect:
{
var p = reader.ReadPayload<PushEffectPayload>();
scopes[depth++] = visitor.OnPushEffect(
(IEffect?)_resources[p.Effect], p.Bounds);
break;
}
case RenderDataOpcode.Pop:
{
reader.Read<RenderDataOpcode>();
visitor.OnPop(in scopes[--depth]);
break;
}
}
}
}
finally
{
if (rented != null)
ArrayPool<TScope>.Shared.Return(rented);
}
}
private static BoxShadows ReadBoxShadows(ref RenderDataReader reader, int count)
{
if (count == 0)
return default;
var first = reader.Read<BoxShadow>();
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<BoxShadow>();
return new BoxShadows(first, rest);
}
}

226
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<byte> 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<IBitmapImpl> bitmap:
bitmap.Dispose();
break;
case IRef<IGlyphRunImpl> 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<IGlyphRunImpl>? glyphRun)
{
_writer.WritePayload(new DrawGlyphRunPayload
{
ServerBrush = _resources.Intern(serverBrush),
GlyphRun = _resources.Intern(glyphRun)
});
}
public void DrawBitmap(IRef<IBitmapImpl>? 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<int>();
var resourceCount = reader.Read<int>();
for (var i = 0; i < resourceCount; i++)
_resources.AppendDeserialized(reader.ReadObject());
var byteCount = reader.Read<int>();
if (byteCount > 0)
reader.Read(_writer.Reserve(byteCount));
}
public void Dispose()
{
_writer.Dispose();
_resources.Dispose();
}
}

57
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<byte> Written => _buffer is null ? default : _buffer.AsSpan(0, _length);
private Span<byte> Advance(int size)
{
var required = _length + size;
if (_buffer is null)
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(required, 256));
else if (_buffer.Length < required)
{
var grown = ArrayPool<byte>.Shared.Rent(Math.Max(required, _buffer.Length * 2));
Array.Copy(_buffer, grown, _length);
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = grown;
}
var span = _buffer.AsSpan(_length, size);
_length += size;
return span;
}
public Span<byte> Reserve(int count) => Advance(count);
public void Rewind(int length) => _length = length;
public void Write<T>(T value) where T : unmanaged
=> MemoryMarshal.Write(Advance(Unsafe.SizeOf<T>()), in value);
public void WriteOpcode(RenderDataOpcode opcode) => Write(opcode);
public void WritePayload<T>(T payload) where T : unmanaged, IRenderDataPayload<T>
{
Write(T.Opcode);
Write(payload);
}
public void Dispose()
{
if (_buffer != null)
ArrayPool<byte>.Shared.Return(_buffer);
_buffer = null;
_length = 0;
}
}

94
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<IRenderDataItem> _items;
private RenderDataStream? _stream;
private PooledInlineList<IServerRenderResource> _referencedResources;
private LtrbRect? _bounds;
private bool _boundsValid;
private static readonly ThreadSafeObjectPool<Collector> s_resourceHashSetPool = new();
public ServerCompositionRenderData(ServerCompositor compositor) : base(compositor)
{
}
class Collector : IRenderDataServerResourcesCollector
{
public readonly HashSet<IServerRenderResource> 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<int>();
_items.EnsureCapacity(count);
for (var c = 0; c < count; c++)
_items.Add(reader.ReadObject<IRenderDataItem>());
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<IRenderDataItem> 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();

46
src/Avalonia.Base/Rendering/Composition/Transport/BatchStream.cs

@ -113,6 +113,24 @@ internal class BatchStreamWriter : IDisposable
_currentDataSegment.ElementCount += size;
}
public unsafe void Write(ReadOnlySpan<byte> 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>(
(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<byte> 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>(
(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<T>() where T : class? => (T)ReadObject()!;
public object? ReadObject()

50
tests/Avalonia.Base.UnitTests/Rendering/RenderDataEffectNodeTests.cs

@ -1,50 +0,0 @@
using Avalonia.Media;
using Avalonia.Rendering.Composition.Drawing.Nodes;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering;
public class RenderDataEffectNodeTests
{
/// <summary>
/// Regression test: RenderDataEffectNode.Bounds was returning BoundsRect even when
/// there were no children, causing incorrect dirty rect tracking.
/// </summary>
[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);
}
/// <summary>
/// RenderDataEffectNode.Bounds should expand child bounds by the effect output padding
/// so that dirty rects include effect output (blur/shadow extending beyond geometry bounds).
/// </summary>
[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) { }
}
}

76
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<IGlyphRunImpl>());
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<IGlyphRunImpl>());
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<IGeometryImpl>();
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<IDrawingContextImpl>();
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<IDrawingContextImpl>();
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()
{

67
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs

@ -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<Point>()
{
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<Point>()
{
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));
}
}
}
}

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

177
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<IBitmapImpl>());
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<ICustomDrawOperation>();
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());
}
}
}

29
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());
}
}

31
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs → 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)));
}
}
}

208
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<IGeometryImpl>();
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<IBitmapImpl>());
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<ICustomDrawOperation>();
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<IGeometryImpl>();
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)));
}
}
}

55
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<Point>
{
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<Point>
{
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));
}
}
}

98
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<IBrush>();
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<IDrawingContextImpl>();
result.Replay(context.Object);
context.Verify(x => x.DrawRectangle(brush, null,
It.IsAny<RoundedRect>(), It.IsAny<BoxShadows>()), 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<object?>(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;
}
}
}

228
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<IPen>();
var context = new Mock<IDrawingContextImpl>();
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<IBrush>();
var pen = Mock.Of<IPen>();
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<IDrawingContextImpl>();
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<BoxShadows>(s => s.Count == 2)), Times.Once);
}
[Fact]
public void Replay_Forwards_Custom_Operation()
{
var operation = new Mock<ICustomDrawOperation>();
var context = new Mock<IDrawingContextImpl>();
using var stream = new RenderDataStream();
stream.DrawCustom(operation.Object);
stream.Replay(context.Object);
operation.Verify(x => x.Render(It.IsAny<ImmediateDrawingContext>()), Times.Once);
}
[Fact]
public void Replay_Pop_Dispatches_To_Matching_Pop_In_Lifo_Order()
{
var calls = new List<string>();
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<IDrawingContextImpl>();
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<string>();
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<string>();
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<string>();
var context = RecordingContext(calls);
using var stream = new RenderDataStream();
stream.PushClip(new RoundedRect(new Rect(0, 0, 10, 10)));
stream.PushGeometryClip(Mock.Of<IGeometryImpl>());
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<IDrawingContextImpl>();
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<IDrawingContextImpl>();
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<IBitmapImpl>());
var glyphRun = RefCountable.Create(Mock.Of<IGlyphRunImpl>());
var operation = new Mock<ICustomDrawOperation>();
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<IDrawingContextImpl>();
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<RoundedRect>(), It.IsAny<BoxShadows>()), Times.Once);
}
private static Mock<IDrawingContextImpl> RecordingContext(List<string> calls)
{
var context = new Mock<IDrawingContextImpl>();
context.Setup(x => x.PushClip(It.IsAny<RoundedRect>())).Callback(() => calls.Add("PushClip"));
context.Setup(x => x.PopClip()).Callback(() => calls.Add("PopClip"));
context.Setup(x => x.PushGeometryClip(It.IsAny<IGeometryImpl>()))
.Callback(() => calls.Add("PushGeometryClip"));
context.Setup(x => x.PopGeometryClip()).Callback(() => calls.Add("PopGeometryClip"));
context.Setup(x => x.PushOpacity(It.IsAny<double>(), It.IsAny<Rect?>()))
.Callback(() => calls.Add("PushOpacity"));
context.Setup(x => x.PopOpacity()).Callback(() => calls.Add("PopOpacity"));
return context;
}
}
}

237
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<byte>(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<byte>());
Assert.Equal(RenderDataOpcode.DrawGeometry, reader.Read<RenderDataOpcode>());
Assert.Equal(-123456, reader.Read<int>());
Assert.Equal(4000000000u, reader.Read<uint>());
Assert.Equal(3.14159, reader.Read<double>());
Assert.True(reader.Read<bool>());
Assert.False(reader.Read<bool>());
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<Point>());
Assert.Equal(vector, reader.Read<Vector>());
Assert.Equal(rect, reader.Read<Rect>());
var readRounded = reader.Read<RoundedRect>();
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<Matrix>());
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<Color>());
var readShadow = reader.Read<BoxShadow>();
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<RenderOptions>());
Assert.Equal(textOptions, reader.Read<TextOptions>());
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<RenderOptions>().RequiresFullOpacityHandling);
}
finally
{
writer.Dispose();
}
}
[Fact]
public void Length_Tracks_Written_Bytes()
{
var writer = new RenderDataWriter();
try
{
Assert.Equal(0, writer.Length);
writer.Write<byte>(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<int>());
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<RenderDataOpcode>());
var payload = reader.Read<PushOpacityPayload>();
Assert.Equal(0.5, payload.Opacity);
Assert.True(reader.IsAtEnd);
}
finally
{
writer.Dispose();
}
}
}
}
Loading…
Cancel
Save