diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 7eb9b91f9a..884740fe65 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/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) { } diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs index 312cae2c52..ee0e0357d0 100644 --- a/src/Avalonia.Base/Media/PlatformDrawingContext.cs +++ b/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; } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 1a813f7bbe..c5b57e5601 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/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 /// /// Gets or sets the current transform of the drawing context. /// - Matrix Transform { get; set; } + CompositionMatrix Transform { get; set; } /// /// Clears the render target to the specified color. diff --git a/src/Avalonia.Base/Platform/LtrbRect.cs b/src/Avalonia.Base/Platform/LtrbRect.cs index 188f75c010..213c3fe58b 100644 --- a/src/Avalonia.Base/Platform/LtrbRect.cs +++ b/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 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); + } + /// /// Perform _WPF-like_ union operation /// @@ -165,6 +192,11 @@ public struct LtrbRect return hash; } } + + public override string ToString() + { + return $"{Left}:{Top}-{Right}:{Bottom}"; + } } /// @@ -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}"; + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs b/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs index 8cf4cca0a5..51e9274228 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs +++ b/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)); } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs index bd7165398a..bb1c2eb2b1 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs +++ b/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(); } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs index ac0a5c4cb7..f40b128c28 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs +++ b/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? _stack; - private static readonly ThreadSafeObjectPool> s_matrixStackPool = new(); + private Stack? _stack; + private static readonly ThreadSafeObjectPool> s_matrixStackPool = new(); public RenderDataNodeRenderContext(IDrawingContextImpl context) { @@ -38,7 +39,7 @@ struct RenderDataNodeRenderContext : IDisposable } public IDrawingContextImpl Context { get; } - public Stack MatrixStack + public Stack MatrixStack { get => _stack ??= s_matrixStackPool.Get(); } diff --git a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs b/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs index 4ec1326103..6faf6d1e36 100644 --- a/src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs +++ b/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; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/CompositionMatrix.cs b/src/Avalonia.Base/Rendering/Composition/Server/CompositionMatrix.cs new file mode 100644 index 0000000000..da1cd57d30 --- /dev/null +++ b/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}";*/ + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs index ee0447629a..5c375e6426 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs +++ b/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) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 6b4982c490..8ab8a0eb99 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/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> s_transformStackPool = new(); - private Stack? _transformStack = s_transformStackPool.Get(); + private static readonly ThreadSafeObjectPool> s_transformStackPool = new(); + private Stack? _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) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index 396009841b..0bfe3e6f81 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/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)); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 90b973c3a8..0555c2aac8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/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; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index c2d43f5667..86a4de4814 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/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); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs index cb4da6723f..19158c2d39 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs +++ b/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 diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs index 8ba5470e7f..74e68a023f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs +++ b/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()); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs index 4ee22d3bff..a8fef45491 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs +++ b/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? _transformStack; + private readonly Stack? _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; diff --git a/src/Avalonia.Base/Vector3D.cs b/src/Avalonia.Base/Vector3D.cs index 9502898677..add27705f7 100644 --- a/src/Avalonia.Base/Vector3D.cs +++ b/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); + /// + /// Multiplies the vector by the second vector. + /// + public static Vector3D operator *(Vector3D left, Vector3D right) => Multiply(left, right); + /// /// Multiplies the vector by the given scalar. /// @@ -123,6 +128,8 @@ public readonly record struct Vector3D(double X, double Y, double Z) /// Length of the vector. /// public double Length => Math.Sqrt(Dot(this, this)); + + public static Vector3D One { get; } = new Vector3D(1, 1, 1); /// /// Returns a normalized version of this vector. diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index ce989296b1..ab2bfb2496 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -13,6 +13,10 @@ + @@ -28,7 +32,7 @@ - + diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 2774bf63fe..c640c49bcc 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/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) { diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index e122c21242..a141ac354a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/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 _opacityStack = new(); private readonly Stack _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 } /// - 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. diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index fd4e49d012..007fae8877 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/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, diff --git a/src/tools/DevGenerators/CompositionGenerator/Config.cs b/src/tools/DevGenerators/CompositionGenerator/Config.cs index d18947f0d2..2bc23b7b46 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Config.cs +++ b/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; } diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs index faad2458fb..e25965b3fa 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs +++ b/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 _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 }; } } diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 2f27a752e2..279e1b8362 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/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)); } diff --git a/tests/Avalonia.Benchmarks/Compositor/CompositionMatrix.cs b/tests/Avalonia.Benchmarks/Compositor/CompositionMatrix.cs new file mode 100644 index 0000000000..4173c206fb --- /dev/null +++ b/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(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); + } + + +} \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs b/tests/Avalonia.Benchmarks/Compositor/CompositionTargetUpdate.cs new file mode 100644 index 0000000000..eca15fa212 --- /dev/null +++ b/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 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) + { + } +} \ No newline at end of file