Browse Source

wip (matrix stuff)

feature/optimized-matrix
Nikita Tsukanov 3 months ago
parent
commit
295ef57b17
  1. 2
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  2. 2
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  3. 3
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  4. 37
      src/Avalonia.Base/Platform/LtrbRect.cs
  5. 4
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs
  6. 2
      src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs
  7. 7
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs
  8. 16
      src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs
  9. 274
      src/Avalonia.Base/Rendering/Composition/Server/CompositionMatrix.cs
  10. 4
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs
  11. 14
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  12. 42
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  13. 46
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  14. 13
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  15. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs
  16. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  17. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs
  18. 7
      src/Avalonia.Base/Vector3D.cs
  19. 6
      src/Avalonia.Base/composition-schema.xml
  20. 3
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  21. 7
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  22. 29
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  23. 11
      src/tools/DevGenerators/CompositionGenerator/Config.cs
  24. 11
      src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs
  25. 9
      src/tools/DevGenerators/CompositionGenerator/Generator.cs
  26. 54
      tests/Avalonia.Benchmarks/Compositor/CompositionMatrix.cs
  27. 121
      tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs

2
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@ -35,7 +35,7 @@ namespace Avalonia.Media
}
}
internal ImmediateDrawingContext(IDrawingContextImpl impl, bool ownsImpl) : this(impl, impl.Transform, ownsImpl)
internal ImmediateDrawingContext(IDrawingContextImpl impl, bool ownsImpl) : this(impl, impl.Transform.ToMatrix(), ownsImpl)
{
}

2
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@ -80,7 +80,7 @@ internal sealed class PlatformDrawingContext : DrawingContext
{
_transforms ??= TransformStackPool.Get();
var current = _impl.Transform;
_transforms.Push(current);
_transforms.Push(current.ToMatrix());
_impl.Transform = matrix * current;
}

3
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -4,6 +4,7 @@ using Avalonia.Utilities;
using Avalonia.Metadata;
using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Platform
{
@ -16,7 +17,7 @@ namespace Avalonia.Platform
/// <summary>
/// Gets or sets the current transform of the drawing context.
/// </summary>
Matrix Transform { get; set; }
CompositionMatrix Transform { get; set; }
/// <summary>
/// Clears the render target to the specified color.

37
src/Avalonia.Base/Platform/LtrbRect.cs

@ -2,6 +2,7 @@
using System;
using Avalonia.Metadata;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Platform;
@ -115,6 +116,32 @@ public struct LtrbRect
return new LtrbRect(left, top, right, bottom);
}
internal LtrbRect TransformToAABB(CompositionMatrix matrix)
{
ReadOnlySpan<Point> points = stackalloc Point[4]
{
matrix.Transform(TopLeft),
matrix.Transform(TopRight),
matrix.Transform(BottomRight),
matrix.Transform(BottomLeft)
};
var left = double.MaxValue;
var right = double.MinValue;
var top = double.MaxValue;
var bottom = double.MinValue;
foreach (var p in points)
{
if (p.X < left) left = p.X;
if (p.X > right) right = p.X;
if (p.Y < top) top = p.Y;
if (p.Y > bottom) bottom = p.Y;
}
return new LtrbRect(left, top, right, bottom);
}
/// <summary>
/// Perform _WPF-like_ union operation
/// </summary>
@ -165,6 +192,11 @@ public struct LtrbRect
return hash;
}
}
public override string ToString()
{
return $"{Left}:{Top}-{Right}:{Bottom}";
}
}
/// <summary>
@ -265,4 +297,9 @@ public struct LtrbPixelRect
return new LtrbPixelRect((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right),
(int)Math.Ceiling(rect.Bottom));
}
public override string ToString()
{
return $"{Left}:{Top}-{Right}:{Bottom}";
}
}

4
src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs

@ -97,14 +97,14 @@ public abstract class CompositionCustomVisualHandler
protected bool RenderClipContains(Point pt)
{
VerifyInRender();
pt *= _host!.GlobalTransformMatrix;
pt = _host!.GlobalTransformMatrix.Transform(pt);
return _currentTransformedClip.Contains(pt) && _host.Root!.DirtyRects.Contains(pt);
}
protected bool RenderClipIntersectes(Rect rc)
{
VerifyInRender();
rc = rc.TransformToAABB(_host!.GlobalTransformMatrix);
rc = rc.TransformToAABB(_host!.GlobalTransformMatrix.ToMatrix());
return _currentTransformedClip.Intersects(rc) && _host.Root!.DirtyRects.Intersects(new (rc));
}
}

2
src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs

@ -5,5 +5,5 @@ namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionSimpleTransform : ITransform
{
Matrix ITransform.Value => this.Value.ToMatrix();
}

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

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
@ -29,8 +30,8 @@ interface IRenderDataItemWithServerResources : IRenderDataItem
struct RenderDataNodeRenderContext : IDisposable
{
private Stack<Matrix>? _stack;
private static readonly ThreadSafeObjectPool<Stack<Matrix>> s_matrixStackPool = new();
private Stack<CompositionMatrix>? _stack;
private static readonly ThreadSafeObjectPool<Stack<CompositionMatrix>> s_matrixStackPool = new();
public RenderDataNodeRenderContext(IDrawingContextImpl context)
{
@ -38,7 +39,7 @@ struct RenderDataNodeRenderContext : IDisposable
}
public IDrawingContextImpl Context { get; }
public Stack<Matrix> MatrixStack
public Stack<CompositionMatrix> MatrixStack
{
get => _stack ??= s_matrixStackPool.Get();
}

16
src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs

@ -1,25 +1,26 @@
using System.Numerics;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Rendering.Composition
{
static class MatrixUtils
{
public static Matrix ComputeTransform(Vector size, Vector anchorPoint, Vector3D centerPoint,
Matrix transformMatrix, Vector3D scale, float rotationAngle, Quaternion orientation, Vector3D offset)
public static CompositionMatrix ComputeTransform(Vector size, Vector anchorPoint, Vector3D centerPoint,
CompositionMatrix transformMatrix, Vector3D scale, float rotationAngle, Quaternion orientation, Vector3D offset)
{
// The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work
var anchor = Vector.Multiply(size, anchorPoint);
var mat = Matrix.CreateTranslation(-anchor.X, -anchor.Y);
var mat = CompositionMatrix.CreateTranslation(-anchor.X, -anchor.Y);
var center = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);
if (!transformMatrix.IsIdentity)
if (!transformMatrix.GuaranteedIdentity)
mat = transformMatrix * mat;
if (scale != new Vector3D(1, 1, 1))
mat *= ToMatrix(Matrix4x4.CreateScale(scale.ToVector3(), center.ToVector3()));
mat = CompositionMatrix.CreateScale(scale, center);
//TODO: RotationAxis support
if (rotationAngle != 0)
@ -39,10 +40,7 @@ namespace Avalonia.Rendering.Composition
if (offset != default)
{
if (offset.Z == 0)
mat *= Matrix.CreateTranslation(offset.X, offset.Y);
else
mat *= ToMatrix(Matrix4x4.CreateTranslation(offset.ToVector3()));
mat *= CompositionMatrix.CreateTranslation(offset.X, offset.Y);
}
return mat;

274
src/Avalonia.Base/Rendering/Composition/Server/CompositionMatrix.cs

@ -0,0 +1,274 @@
//#define DEBUG_COMPOSITION_MATRIX
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Metadata;
// ReSharper disable CompareOfFloatsByEqualityOperator
namespace Avalonia.Rendering.Composition.Server;
[PrivateApi]
public struct CompositionMatrix(Matrix matrix)
{
enum Type
{
Identity = 0,
TranslateAndScale = 1,
Full = 2
}
private double _scaleX_11;
private double _skewY_12;
private double _perspX_13;
private double _skewX_21;
private double _scaleY_22;
private double _perspY_23;
private double _offsetX_31;
private double _offsetY_32;
private double _perspZ_33;
private Type _type;
public bool GuaranteedIdentity => _type == Type.Identity;
public bool GuaranteedTranslateAndScaleOnly => _type == Type.TranslateAndScale;
public bool GuaranteedTranslateAndScaleOnlyOrIdentity => _type is Type.TranslateAndScale or Type.Identity;
public double ScaleX => _scaleX_11;
public double SkewY => _skewY_12;
public double PerspX => _perspX_13;
public double SkewX => _skewX_21;
public double ScaleY => _scaleY_22;
public double PerspY => _perspY_23;
public double OffsetX => _offsetX_31;
public double OffsetY => _offsetY_32;
public double PerspZ => _perspZ_33;
public static CompositionMatrix Identity { get; } = default;
public static CompositionMatrix CreateTranslation(double offsetX, double offsetY)
{
if(offsetX == 0 && offsetY == 0)
return default;
return new CompositionMatrix
{
_type = Type.TranslateAndScale,
_offsetX_31 = offsetX,
_offsetY_32 = offsetY,
_scaleX_11 = 1,
_scaleY_22 = 1,
};
}
public static CompositionMatrix CreateScale(in Vector3D scales, in Vector3D centerPoint)
{
var cp = centerPoint * (Vector3D.One - scales);
return new CompositionMatrix()
{
_type = Type.TranslateAndScale,
_scaleX_11 = scales.X,
_scaleY_22 = scales.Y,
_offsetX_31 = cp.X,
_offsetY_32 = cp.Y,
_perspZ_33 = 1
};
}
public static CompositionMatrix CreateScale(double scaleX, double scaleY)
{
if (scaleX == 1 && scaleY == 1)
return default;
return new CompositionMatrix()
{
_type = Type.TranslateAndScale,
_scaleX_11 = scaleX,
_scaleY_22 = scaleY,
_perspZ_33 = 1
};
}
[SkipLocalsInit]
public static unsafe CompositionMatrix FromMatrix(in Matrix m)
{
CompositionMatrix rv;
if(m.M13 != 0 || m.M23 != 0 || m.M33 != 1)
rv._type = Type.Full;
else if (m.M11 != 1 || m.M22 != 1 || m.M31 != 0 || m.M32 != 0 || m.M12 != 0 || m.M21 != 0)
rv._type = Type.TranslateAndScale;
else
return default;
*(Matrix*)&rv = m;
return rv;
}
public Matrix ToMatrix()
{
if (_type == Type.Identity)
return Matrix.Identity;
if (_type == Type.TranslateAndScale)
return new Matrix(_scaleX_11, 0, 0, _scaleY_22, _offsetX_31, _offsetY_32);
return new Matrix(_scaleX_11, _skewY_12, _skewX_21, _scaleY_22, _offsetX_31, _offsetY_32, _perspX_13, _perspY_23, _perspZ_33);
}
//public static implicit operator Matrix(CompositionMatrix m) => m.ToMatrix();
public static implicit operator CompositionMatrix(Matrix m) => FromMatrix(m);
public bool Equals(CompositionMatrix m)
{
if(m._type == Type.Identity && _type == Type.Identity)
return true;
if(m._type == Type.TranslateAndScale && _type == Type.TranslateAndScale)
{
return m._scaleX_11 == _scaleX_11 &&
m._scaleY_22 == _scaleY_22 &&
m._offsetX_31 == _offsetX_31 &&
m._offsetY_32 == _offsetY_32;
}
return m._scaleX_11 == _scaleX_11 &&
m._skewY_12 == _skewY_12 &&
m._perspX_13 == _perspX_13 &&
m._skewX_21 == _skewX_21 &&
m._scaleY_22 == _scaleY_22 &&
m._perspY_23 == _perspY_23 &&
m._offsetX_31 == _offsetX_31 &&
m._offsetY_32 == _offsetY_32 &&
m._perspZ_33 == _perspZ_33;
}
public static bool operator ==(CompositionMatrix left, CompositionMatrix right) => left.Equals(right);
public static bool operator !=(CompositionMatrix left, CompositionMatrix right) => !left.Equals(right);
[SkipLocalsInit]
private static CompositionMatrix Concat(ref CompositionMatrix left, ref CompositionMatrix right)
#if DEBUG_COMPOSITION_MATRIX
{
var res = ConcatCore(ref left, ref right);
var expected = left.ToMatrix() * right.ToMatrix();
var actual = res.ToMatrix();
if (!expected.Equals(actual))
{
Console.WriteLine("BUG!");
}
return res;
}
static CompositionMatrix ConcatCore(ref CompositionMatrix left, ref CompositionMatrix right)
#endif
{
if (left._type == Type.Identity)
return right;
if (right._type == Type.Identity)
return left;
if(left._type == Type.TranslateAndScale && right._type == Type.TranslateAndScale)
{
return new CompositionMatrix
{
_type = Type.TranslateAndScale,
_scaleX_11 = left._scaleX_11 * right._scaleX_11,
_scaleY_22 = left._scaleY_22 * right._scaleY_22,
_offsetX_31 = (left._offsetX_31 * right._scaleX_11) + right._offsetX_31,
_offsetY_32 = (left._offsetY_32 * right._scaleY_22) + right._offsetY_32,
_perspZ_33 = 1
};
}
return new CompositionMatrix
{
_type = Type.Full,
_scaleX_11 =
(left._scaleX_11 * right._scaleX_11) + (left._skewY_12 * right._skewX_21) +
(left._perspX_13 * right._offsetX_31),
_skewY_12 =
(left._scaleX_11 * right._skewY_12) + (left._skewY_12 * right._scaleY_22) +
(left._perspX_13 * right._offsetY_32),
_perspX_13 =
(left._scaleX_11 * right._perspX_13) + (left._skewY_12 * right._perspY_23) +
(left._perspX_13 * right._perspZ_33),
_skewX_21 =
(left._skewX_21 * right._scaleX_11) + (left._scaleY_22 * right._skewX_21) +
(left._perspY_23 * right._offsetX_31),
_scaleY_22 =
(left._skewX_21 * right._skewY_12) + (left._scaleY_22 * right._scaleY_22) +
(left._perspY_23 * right._offsetY_32),
_perspY_23 =
(left._skewX_21 * right._perspX_13) + (left._scaleY_22 * right._perspY_23) +
(left._perspY_23 * right._perspZ_33),
_offsetX_31 =
(left._offsetX_31 * right._scaleX_11) + (left._offsetY_32 * right._skewX_21) +
(left._perspZ_33 * right._offsetX_31),
_offsetY_32 =
(left._offsetX_31 * right._skewY_12) + (left._offsetY_32 * right._scaleY_22) +
(left._perspZ_33 * right._offsetY_32),
_perspZ_33 =
(left._offsetX_31 * right._perspX_13) + (left._offsetY_32 * right._perspY_23) +
(left._perspZ_33 * right._perspZ_33)
};
}
public static CompositionMatrix operator*(CompositionMatrix left, CompositionMatrix right)
{
return Concat(ref left, ref right);
}
public Point Transform(Point p)
#if DEBUG_COMPOSITION_MATRIX
{
var res = TransformCore(p);
var expected = p.Transform(ToMatrix());
if (res != expected)
{
Console.WriteLine("Bug!");
}
return res;
}
public Point TransformCore(Point p)
#endif
{
if (_type == Type.Identity)
return p;
if (_type == Type.TranslateAndScale)
{
return new Point(
(p.X * _scaleX_11) + _offsetX_31,
(p.Y * _scaleY_22) + _offsetY_32);
}
var m44 = new Matrix4x4(
(float)_scaleX_11, (float)_skewY_12, (float)_perspX_13, 0,
(float)_skewX_21, (float)_scaleY_22, (float)_perspY_23, 0,
(float)_offsetX_31, (float)_offsetY_32, (float)_perspZ_33, 0,
0, 0, 0, 1
);
var vector = new Vector3((float)p.X, (float)p.Y, 1);
var transformedVector = Vector3.Transform(vector, m44);
var z = 1 / transformedVector.Z;
return new Point(transformedVector.X * z, transformedVector.Y * z);
}
public override string ToString()
{
if (_type == Type.Identity)
return "Identity";
var m = ToMatrix();
if(m.IsIdentity)
return "Identity";
return m.ToString();
/*
if (_type == Type.Identity)
return "Identity";
if (_type == Type.TranslateAndScale)
return $"{_scaleX_11} 0 0 / 0 {_scaleY_22} 0 / {_offsetX_31} {_offsetY_32} / 1";
return $"{_scaleX_11} {_skewY_12} {_perspX_13} / {_skewX_21} {_scaleY_22} {_perspY_23} / {_offsetX_31} {_offsetY_32} {_perspZ_33}";*/
}
}

4
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs

@ -40,7 +40,7 @@ internal partial class CompositorDrawingContextProxy
[FieldOffset(0)] public double Opacity;
[FieldOffset(8)] public Rect? NullableOpacityRect;
[FieldOffset(0)] public Matrix Transform;
[FieldOffset(0)] public CompositionMatrix Transform;
[FieldOffset(0)] public RenderOptions RenderOptions;
@ -73,7 +73,7 @@ internal partial class CompositorDrawingContextProxy
}
}
public void SetTransform(Matrix m)
public void SetTransform(CompositionMatrix m)
{
if (_autoFlush)
{

14
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -17,8 +17,8 @@ internal partial class CompositorDrawingContextProxy : IDrawingContextImpl,
IDrawingContextWithAcrylicLikeSupport, IDrawingContextImplWithEffects
{
private readonly IDrawingContextImpl _impl;
private static readonly ThreadSafeObjectPool<Stack<Matrix>> s_transformStackPool = new();
private Stack<Matrix>? _transformStack = s_transformStackPool.Get();
private static readonly ThreadSafeObjectPool<Stack<CompositionMatrix>> s_transformStackPool = new();
private Stack<CompositionMatrix>? _transformStack = s_transformStackPool.Get();
public CompositorDrawingContextProxy(IDrawingContextImpl impl)
{
@ -38,17 +38,17 @@ internal partial class CompositorDrawingContextProxy : IDrawingContextImpl,
s_transformStackPool.ReturnAndSetNull(ref _transformStack);
}
public Matrix? PostTransform { get; set; }
public CompositionMatrix? PostTransform { get; set; }
// Transform that was most recently passed to set_Transform or restored by a PopXXX operation
// We use it to report the transform that would correspond to the current state if all commands were executed
Matrix _reportedTransform = Matrix.Identity;
CompositionMatrix _reportedTransform = CompositionMatrix.Identity;
// Transform that was most recently passed to SetImplTransform or restored by a PopXXX operation
// We use it to save the effective transform before executing a Push operation
Matrix _effectiveTransform = Matrix.Identity;
CompositionMatrix _effectiveTransform = CompositionMatrix.Identity;
public Matrix Transform
public CompositionMatrix Transform
{
get => _reportedTransform;
set
@ -58,7 +58,7 @@ internal partial class CompositorDrawingContextProxy : IDrawingContextImpl,
}
}
void SetImplTransform(Matrix m)
void SetImplTransform(CompositionMatrix m)
{
_effectiveTransform = m;
if (PostTransform.HasValue)

42
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -29,16 +29,16 @@ namespace Avalonia.Rendering.Composition.Server
}
}
public override UpdateResult Update(ServerCompositionTarget root, Matrix parentCombinedTransform)
public override UpdateResult Update(ServerCompositionTarget root, ref CompositionMatrix parentCombinedTransform)
{
var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root, parentCombinedTransform);
var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root, ref parentCombinedTransform);
foreach (var child in Children)
{
if (child.AdornedVisual != null)
root.EnqueueAdornerUpdate(child);
else
{
var res = child.Update(root, GlobalTransformMatrix);
var res = child.Update(root, ref GlobalTransformMatrix);
oldInvalidated |= res.InvalidatedOld;
newInvalidated |= res.InvalidatedNew;
combinedBounds = LtrbRect.FullUnion(combinedBounds, res.Bounds);
@ -79,24 +79,26 @@ namespace Avalonia.Rendering.Composition.Server
// We are in a weird position here: bounds are in global coordinates while padding gets applied in local ones
// Since we have optimizations to AVOID recomputing transformed bounds and since visuals with effects are relatively rare
// we instead apply the transformation matrix to rescale the bounds
// If we only have translation and scale, just scale the padding
if (CombinedTransformMatrix is
{
M12: 0, M13: 0,
M21: 0, M23: 0,
M31: 0, M32: 0
})
padding = new Thickness(padding.Left * CombinedTransformMatrix.M11,
padding.Top * CombinedTransformMatrix.M22,
padding.Right * CombinedTransformMatrix.M11,
padding.Bottom * CombinedTransformMatrix.M22);
else
if (!CombinedTransformMatrix.GuaranteedIdentity)
{
// Conservatively use the transformed rect size
var transformedPaddingRect = new Rect().Inflate(padding).TransformToAABB(CombinedTransformMatrix);
padding = new(Math.Max(transformedPaddingRect.Width, transformedPaddingRect.Height));
// If we only have translation and scale, just scale the padding
if (CombinedTransformMatrix.GuaranteedTranslateAndScaleOnly)
{
padding = new Thickness(padding.Left * CombinedTransformMatrix.ScaleX,
padding.Top * CombinedTransformMatrix.ScaleY,
padding.Right * CombinedTransformMatrix.ScaleX,
padding.Bottom * CombinedTransformMatrix.ScaleY);
}
else
{
// Conservatively use the transformed rect size
var transformedPaddingRect =
new Rect().Inflate(padding).TransformToAABB(CombinedTransformMatrix.ToMatrix());
padding = new(Math.Max(transformedPaddingRect.Width, transformedPaddingRect.Height));
}
}
AddDirtyRect(transformedBounds.Inflate(padding));

46
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -84,6 +84,31 @@ namespace Avalonia.Rendering.Composition.Server
_fullRedrawRequested = true;
}
// Public for usage in benchmarks
public void Update()
{
Revision++;
_overlays.MarkUpdateCallStart();
using (Diagnostic.BeginCompositorUpdatePass())
{
var transform = CompositionMatrix.CreateScale(Scaling, Scaling);
// Update happens in a separate phase to extend dirty rect if needed
Root!.Update(this, ref transform);
while (_adornerUpdateQueue.Count > 0)
{
var adorner = _adornerUpdateQueue.Dequeue();
adorner.Update(this, ref adorner.AdornedVisual!.GlobalTransformMatrix);
}
_updateRequested = false;
Readback.CompleteWrite(Revision);
_overlays.MarkUpdateCallEnd();
}
}
public void Render()
{
if (_disposed)
@ -120,26 +145,7 @@ namespace Avalonia.Rendering.Composition.Server
if (DirtyRects.IsEmpty && !_redrawRequested && !_updateRequested)
return;
Revision++;
_overlays.MarkUpdateCallStart();
using (Diagnostic.BeginCompositorUpdatePass())
{
var transform = Matrix.CreateScale(Scaling, Scaling);
// Update happens in a separate phase to extend dirty rect if needed
Root.Update(this, transform);
while (_adornerUpdateQueue.Count > 0)
{
var adorner = _adornerUpdateQueue.Dequeue();
adorner.Update(this, transform);
}
_updateRequested = false;
Readback.CompleteWrite(Revision);
_overlays.MarkUpdateCallEnd();
}
Update();
if (!_redrawRequested)
return;

13
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -18,7 +18,6 @@ namespace Avalonia.Rendering.Composition.Server
{
private bool _isDirtyForUpdate;
private LtrbRect _oldOwnContentBounds;
private bool _isBackface;
private LtrbRect? _transformedClipBounds;
private LtrbRect _combinedTransformedClipBounds;
@ -125,8 +124,8 @@ namespace Avalonia.Rendering.Composition.Server
return ref _readback2;
}
public Matrix CombinedTransformMatrix { get; private set; } = Matrix.Identity;
public Matrix GlobalTransformMatrix { get; private set; }
public CompositionMatrix CombinedTransformMatrix = Matrix.Identity;
public CompositionMatrix GlobalTransformMatrix;
public record struct UpdateResult(LtrbRect? Bounds, bool InvalidatedOld, bool InvalidatedNew)
{
@ -136,7 +135,7 @@ namespace Avalonia.Rendering.Composition.Server
}
}
public virtual UpdateResult Update(ServerCompositionTarget root, Matrix parentVisualTransform)
public virtual UpdateResult Update(ServerCompositionTarget root, ref CompositionMatrix parentTransform)
{
if (Parent == null && Root == null)
return default;
@ -153,7 +152,6 @@ namespace Avalonia.Rendering.Composition.Server
_combinedTransformDirty = false;
}
var parentTransform = AdornedVisual?.GlobalTransformMatrix ?? parentVisualTransform;
var newTransform = CombinedTransformMatrix * parentTransform;
@ -161,8 +159,6 @@ namespace Avalonia.Rendering.Composition.Server
var positionChanged = false;
if (GlobalTransformMatrix != newTransform)
{
_isBackface = Vector3.Transform(
new Vector3(0, 0, float.PositiveInfinity), MatrixUtils.ToMatrix4x4(GlobalTransformMatrix)).Z <= 0;
positionChanged = true;
}
@ -232,7 +228,6 @@ namespace Avalonia.Rendering.Composition.Server
IsHitTestVisibleInFrame = _parent?.IsHitTestVisibleInFrame != false
&& Visible
&& !_isBackface
&& !(_combinedTransformedClipBounds.IsZeroSize);
IsVisibleInFrame = IsHitTestVisibleInFrame
@ -259,7 +254,7 @@ namespace Avalonia.Rendering.Composition.Server
var i = Root!.Readback;
ref var readback = ref GetReadback(i.WriteIndex);
readback.Revision = root.Revision;
readback.Matrix = GlobalTransformMatrix;
readback.Matrix = GlobalTransformMatrix.ToMatrix();
readback.TargetId = Root.Id;
readback.Visible = IsHitTestVisibleInFrame;
return new(TransformedOwnContentBounds, invalidateNewBounds, invalidateOldBounds);

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs

@ -56,7 +56,7 @@ internal partial class ServerCompositor
var pixelSize = PixelSize.FromSize(new Size(visual.Size.X, visual.Size.Y), scaling);
var scaleTransform = Matrix.CreateScale(scaling, scaling);
var invertRootTransform = visual.CombinedTransformMatrix.Invert();
var invertRootTransform = visual.CombinedTransformMatrix.ToMatrix().Invert();
IDrawingContextLayerImpl? target = null;
try

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs

@ -74,7 +74,7 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip)
{
ctx.Canvas.AutoFlush = true;
using var context = new ImmediateDrawingContext(ctx.Canvas, GlobalTransformMatrix, false);
using var context = new ImmediateDrawingContext(ctx.Canvas, GlobalTransformMatrix.ToMatrix(), false);
try
{
_handler.Render(context, currentTransformedClip.ToRect());

4
src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs

@ -10,7 +10,7 @@ internal class ServerVisualRenderContext
public bool DetachedRendering { get; }
public bool RenderChildren { get; }
public CompositorDrawingContextProxy Canvas { get; }
private readonly Stack<Matrix>? _transformStack;
private readonly Stack<CompositionMatrix>? _transformStack;
public ServerVisualRenderContext(CompositorDrawingContextProxy canvas, IDirtyRectTracker? dirtyRects,
bool detachedRendering, bool renderChildren)
@ -38,7 +38,7 @@ internal class ServerVisualRenderContext
return true;
}
public bool ShouldRenderOwnContent(ServerCompositionVisual visual, LtrbRect currentTransformedClip)
public bool ShouldRenderOwnContent(ServerCompositionVisual visual, ref LtrbRect currentTransformedClip)
{
if (DetachedRendering)
return true;

7
src/Avalonia.Base/Vector3D.cs

@ -75,6 +75,11 @@ public readonly record struct Vector3D(double X, double Y, double Z)
public static Vector3D Multiply(Vector3D left, Vector3D right) =>
new(left.X * right.X, left.Y * right.Y, left.Z * right.Z);
/// <summary>
/// Multiplies the vector by the second vector.
/// </summary>
public static Vector3D operator *(Vector3D left, Vector3D right) => Multiply(left, right);
/// <summary>
/// Multiplies the vector by the given scalar.
/// </summary>
@ -123,6 +128,8 @@ public readonly record struct Vector3D(double X, double Y, double Z)
/// Length of the vector.
/// </summary>
public double Length => Math.Sqrt(Dot(this, this));
public static Vector3D One { get; } = new Vector3D(1, 1, 1);
/// <summary>
/// Returns a normalized version of this vector.

6
src/Avalonia.Base/composition-schema.xml

@ -13,6 +13,10 @@
<Manual Name="Avalonia.Media.Immutable.ImmutableDashStyle" Passthrough="true"/>
<Manual Name="CompositionSurface" />
<Manual Name="CompositionDrawingSurface" />
<Manual Name="Avalonia.Matrix"
ServerName="Avalonia.Rendering.Composition.Server.CompositionMatrix"
DefaultValue="Avalonia.Matrix.Identity" ServerDefaultValue="default"
IsValueType="true" Passthrough="true"/>
<Object Name="CompositionVisual" Abstract="true">
<Property Name="Root" Type="CompositionTarget?" Internal="true" />
<Property Name="Parent" Type="CompositionVisual?" Internal="true" />
@ -28,7 +32,7 @@
<Property Name="RotationAngle" Type="float" Animated="true"/>
<Property Name="Orientation" Type="Quaternion" DefaultValue="Quaternion.Identity" Animated="true"/>
<Property Name="Scale" Type="Vector3D" DefaultValue="new Avalonia.Vector3D(1, 1, 1)" Animated="true"/>
<Property Name="TransformMatrix" Type="Avalonia.Matrix" DefaultValue="Avalonia.Matrix.Identity" Animated="true" Internal="true"/>
<Property Name="TransformMatrix" Type="Avalonia.Matrix" Animated="true" Internal="true"/>
<Property Name="AdornedVisual" Type="CompositionVisual?" Internal="true" />
<Property Name="AdornerIsClipped" Type="bool" Internal="true" />
<Property Name="OpacityMaskBrush" ClientName="OpacityMaskBrushTransportField" Type="Avalonia.Media.IBrush?" Private="true" />

3
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -10,6 +10,7 @@ using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using Avalonia.Media.TextFormatting;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Headless
{
@ -448,7 +449,7 @@ namespace Avalonia.Headless
}
public Matrix Transform { get; set; }
public CompositionMatrix Transform { get; set; }
public void Clear(Color color)
{

7
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Utilities;
using Avalonia.Skia.Helpers;
using Avalonia.Utilities;
@ -27,10 +28,10 @@ namespace Avalonia.Skia
private readonly Stack<(SKMatrix matrix, PaintWrapper paint)> _maskStack = new();
private readonly Stack<double> _opacityStack = new();
private readonly Stack<RenderOptions> _renderOptionsStack = new();
private readonly Matrix? _postTransform;
private readonly CompositionMatrix? _postTransform;
private double _currentOpacity = 1.0f;
private readonly bool _disableSubpixelTextRendering;
private Matrix? _currentTransform;
private CompositionMatrix? _currentTransform;
private bool _disposed;
private GRContext? _grContext;
public GRContext? GrContext => _grContext;
@ -844,7 +845,7 @@ namespace Avalonia.Skia
}
/// <inheritdoc />
public Matrix Transform
public CompositionMatrix Transform
{
// There is a Canvas.TotalMatrix (non 4x4 overload), but internally it still uses 4x4 matrix.
// We want to avoid SKMatrix4x4 -> SKMatrix -> Matrix conversion by directly going SKMatrix4x4 -> Matrix.

29
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Server;
using SkiaSharp;
namespace Avalonia.Skia
@ -173,6 +174,34 @@ namespace Avalonia.Skia
return sm;
}
public static SKMatrix44 ToSKMatrix44(this CompositionMatrix m)
{
if (m.GuaranteedIdentity)
return SKMatrix44.Identity;
return new SKMatrix44
{
M00 = (float)m.ScaleX,
M01 = (float)m.SkewY,
M02 = 0,
M03 = (float)m.PerspX,
M10 = (float)m.SkewX,
M11 = (float)m.ScaleY,
M12 = 0,
M13 = (float)m.PerspY,
M20 = 0,
M21 = 0,
M22 = 1,
M23 = 0,
M30 = (float)m.OffsetX,
M31 = (float)m.OffsetY,
M32 = 0,
M33 = (float)m.PerspZ
};
}
internal static Matrix ToAvaloniaMatrix(this SKMatrix m) => new(
m.ScaleX, m.SkewY, m.Persp0,

11
src/tools/DevGenerators/CompositionGenerator/Config.cs

@ -39,6 +39,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
[XmlAttribute]
public string ServerName { get; set; }
[XmlAttribute]
public string? DefaultValue { get; set; }
[XmlAttribute]
public string? ServerDefaultValue { get; set; }
[XmlAttribute]
public bool IsValueType { get; set; }
}
public class GImplements
@ -111,6 +120,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
[XmlAttribute]
public string Type { get; set; }
[XmlAttribute]
public string ServerType { get; set; }
[XmlAttribute]
public string DefaultValue { get; set; }
[XmlAttribute]
public bool Animated { get; set; }

11
src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs

@ -14,6 +14,8 @@ public partial class Generator
public bool IsPassthrough { get; set; }
public string ServerType { get; set; } = null!;
public bool IsNullable { get; set; }
public string? DefaultValue { get; set; }
public string? ServerDefaultValue { get; set; }
}
private readonly Dictionary<string, GeneratorTypeInfo> _typeInfoCache = new();
@ -28,6 +30,7 @@ public partial class Generator
var isObject = _objects.Contains(filteredType);
var isNullable = type.EndsWith("?");
bool isPassthrough = false;
string? defaultValue = null, serverDefaultValue = null;
var serverType = ((isObject ? "Server" : "") + type);
if (_manuals.TryGetValue(filteredType, out var manual))
@ -40,6 +43,10 @@ public partial class Generator
if (manual.ServerName != null)
serverType = manual.ServerName + (isNullable ? "?" : "");
defaultValue = manual.ServerDefaultValue;
serverDefaultValue = manual.ServerDefaultValue;
isObject = !manual.IsValueType;
}
return _typeInfoCache[type] = new GeneratorTypeInfo
@ -49,7 +56,9 @@ public partial class Generator
IsObject = isObject,
IsPassthrough = isPassthrough,
ServerType = serverType,
IsNullable = isNullable
IsNullable = isNullable,
ServerDefaultValue = serverDefaultValue,
DefaultValue = defaultValue
};
}
}

9
src/tools/DevGenerators/CompositionGenerator/Generator.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -209,7 +210,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
resetBody = resetBody.AddStatements(
ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset"))));
serializeMethodBody = ApplySerializeField(serializeMethodBody, cl, prop, isObject, isPassthrough);
serializeMethodBody = ApplySerializeField(serializeMethodBody, cl, prop, isObject, isPassthrough, typeInfo.ServerType);
deserializeMethodBody = ApplyDeserializeField(deserializeMethodBody,cl, prop, serverPropertyType, isObject);
if (animatedServer)
@ -230,6 +231,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
$"CompositionProperty.Register<{serverName}, {serverPropertyType}>(\"{prop.Name}\", obj => (({serverName})obj).{fieldName}, (obj, v) => (({serverName})obj).{fieldName} = v, {compositionPropertyVariantGetter})")),
SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword));
var defaultValue = prop.DefaultValue ?? typeInfo.DefaultValue;
if (prop.DefaultValue != null)
{
defaultsMethodBody = defaultsMethodBody.AddStatements(
@ -475,7 +477,8 @@ return;
private static BlockSyntax SerializeChangesEpilogue(GClass cl) =>
Block(ParseStatement(ChangedFieldsFieldName(cl) + " = default;"));
static BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough)
static BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough,
string serverType)
{
var changedFields = ChangedFieldsFieldName(cl);
var changedFieldsType = ChangedFieldsTypeName(cl);
@ -491,7 +494,7 @@ return;
code += $@"
if(({changedFields} & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name})
writer.Write{(isObject ? "Object" : "")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")});
writer.Write{(isObject ? "Object" : $"<{serverType}>")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")});
";
return body.AddStatements(ParseStatement(code));
}

54
tests/Avalonia.Benchmarks/Compositor/CompositionMatrix.cs

@ -0,0 +1,54 @@
using System;
using System.Runtime.CompilerServices;
using Avalonia.Rendering.Composition.Server;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks;
public class CompositionMatrixOperations
{
private Matrix _matrix0 = Matrix.CreateScale(2, 2); // Simulate DPI scaling of the root
private Matrix _matrix1 = Matrix.Identity;
private Matrix _matrix2 = Matrix.CreateTranslation(10, 10) * Matrix.CreateScale(1.5, 1.5);
private Matrix _matrix3 = Matrix.CreateTranslation(10, 10) * Matrix.CreateScale(1.5, 1.5);
private Matrix _matrix4 = Matrix.Identity;
[MethodImpl(MethodImplOptions.NoInlining)]
private void Consume<T>(ref T t)
{
}
[Benchmark]
public void MultiplyMatrix()
{
var m = Matrix.Identity;
for (var c = 0; c < 1000; c++)
{
m = m * _matrix0 * _matrix1 * _matrix2 * _matrix3 * _matrix4;
}
Consume(ref m);
}
private CompositionMatrix _cmatrix0 = CompositionMatrix.CreateScale(2, 2); // Simulate DPI scaling of the root
private CompositionMatrix _cmatrix1 = CompositionMatrix.Identity;
private CompositionMatrix _cmatrix2 = CompositionMatrix.CreateTranslation(10, 10) * CompositionMatrix.CreateScale(1.5, 1.5);
private CompositionMatrix _cmatrix3 = CompositionMatrix.CreateTranslation(10, 10) * CompositionMatrix.CreateScale(1.5, 1.5);
private CompositionMatrix _cmatrix4 = CompositionMatrix.Identity;
[Benchmark]
public void MultiplyCompositionMatrix()
{
var m = CompositionMatrix.Identity;
for (var c = 0; c < 1000; c++)
{
m = m * _cmatrix0 * _cmatrix1 * _cmatrix2 * _cmatrix3 * _cmatrix4;
}
Consume(ref m);
}
}

121
tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs

@ -0,0 +1,121 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Threading;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks;
public class CompositionTargetUpdateOnly : IDisposable
{
private readonly bool _includeRender;
private readonly IDisposable _app;
private readonly Compositor _compositor;
private readonly CompositionTarget _target;
class Timer : IRenderTimer
{
public event Action<TimeSpan> Tick;
public bool RunsInBackground { get; }
}
class ManualScheduler : ICompositorScheduler
{
public void CommitRequested(Compositor compositor)
{
}
}
class NullFramebuffer : IFramebufferPlatformSurface
{
private static readonly IntPtr Buffer = Marshal.AllocHGlobal(4);
public IFramebufferRenderTarget CreateFramebufferRenderTarget() =>
new FuncFramebufferRenderTarget(() => new LockedFramebuffer(Buffer, new PixelSize(1, 1), 4, new Vector(96, 96), PixelFormat.Rgba8888,
null));
}
public CompositionTargetUpdateOnly() : this(false)
{
}
protected CompositionTargetUpdateOnly(bool includeRender)
{
_includeRender = includeRender;
_app = UnitTestApplication.Start(TestServices.StyledWindow);
_compositor = new Compositor(new RenderLoop(new Timer()), null, true, new ManualScheduler(), true,
Dispatcher.UIThread, null);
_target = _compositor.CreateCompositionTarget(() => [new NullFramebuffer()]);
_target.PixelSize = new PixelSize(1000, 1000);
_target.Scaling = 1;
var root = _compositor.CreateContainerVisual();
root.Size = new Vector(1000, 1000);
_target.Root = root;
if (includeRender)
_target.IsEnabled = true;
CreatePyramid(root, 7);
_compositor.Commit();
}
void CreatePyramid(CompositionContainerVisual visual, int depth)
{
for (var c = 0; c < 4; c++)
{
var child = new CompositionDrawListVisual(visual.Compositor,
new ServerCompositionDrawListVisual(visual.Compositor.Server, null!), null!);
double right = c == 1 || c == 3 ? 1 : 0;
double bottom = c > 1 ? 1 : 0;
var rect = new Rect(
visual.Size.X / 2 * right,
visual.Size.Y / 2 * bottom,
visual.Size.X / 2,
visual.Size.Y / 2
);
child.Offset = new(rect.X, rect.Y, 0);
child.Size = new Vector(rect.Width, rect.Height);
var ctx = new RenderDataDrawingContext(child.Compositor);
ctx.DrawRectangle(Brushes.Aqua, null, new Rect(rect.Size));
child.DrawList = ctx.GetRenderResults();
child.Visible = true;
visual.Children.Add(child);
if (depth > 0)
CreatePyramid(child, depth - 1);
}
}
[Benchmark]
public void TargetUpdate()
{
_target.Root.Offset = new Vector3D(_target.Root.Offset.X == 0 ? 1 : 0, 0, 0);
_compositor.Commit();
_compositor.Server.Render();
if (!_includeRender)
_target.Server.Update();
}
public void Dispose()
{
_app.Dispose();
}
}
public class CompositionTargetUpdateWithRender : CompositionTargetUpdateOnly
{
public CompositionTargetUpdateWithRender() : base(true)
{
}
}
Loading…
Cancel
Save