diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index df3c78b1bc..93a5226f83 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -57,13 +57,13 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua base.DeserializeChangesCore(reader, commitedAt); } - protected override void RenderCore(CompositorDrawingContextProxy canvas) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { if (_renderCommands != null) { _renderCommands.Render(canvas); } - base.RenderCore(canvas); + base.RenderCore(canvas, currentTransformedClip); } #if DEBUG diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 5ec5df8416..691cdf57c5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -35,6 +35,7 @@ namespace Avalonia.Rendering.Composition.Server public ReadbackIndices Readback { get; } = new(); + public int RenderedVisuals { get; set; } public ServerCompositionTarget(ServerCompositor compositor, Func renderTargetFactory) : base(compositor) @@ -85,12 +86,12 @@ namespace Avalonia.Rendering.Composition.Server Revision++; // Update happens in a separate phase to extend dirty rect if needed - Root.Update(this, Matrix4x4.Identity); + Root.Update(this); while (_adornerUpdateQueue.Count > 0) { var adorner = _adornerUpdateQueue.Dequeue(); - adorner.Update(this, adorner.AdornedVisual?.GlobalTransformMatrix ?? Matrix4x4.Identity); + adorner.Update(this); } Readback.CompleteWrite(Revision); @@ -114,7 +115,7 @@ namespace Avalonia.Rendering.Composition.Server { context.PushClip(_dirtyRect); context.Clear(Colors.Transparent); - Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper)); + Root.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), _dirtyRect); context.PopClip(); } } @@ -143,8 +144,9 @@ namespace Avalonia.Rendering.Composition.Server (Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) * Compositor.BatchObjectPool.ArraySize * IntPtr.Size), false); - _fpsCounter.RenderFps(targetContext, $"M:{managedMem} / N:{nativeMem}"); + _fpsCounter.RenderFps(targetContext, $"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}"); } + RenderedVisuals = 0; _dirtyRect = Rect.Empty; } @@ -163,6 +165,8 @@ namespace Avalonia.Rendering.Composition.Server public void AddDirtyRect(Rect rect) { + if(rect.IsEmpty) + return; var snapped = SnapToDevicePixels(rect, Scaling); _dirtyRect = _dirtyRect.Union(snapped); _redrawRequested = true; diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs index 136ebc1d63..f7152293cc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerContainerVisual.cs @@ -12,26 +12,28 @@ namespace Avalonia.Rendering.Composition.Server { public ServerCompositionVisualCollection Children { get; private set; } = null!; - protected override void RenderCore(CompositorDrawingContextProxy canvas) + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { - base.RenderCore(canvas); + base.RenderCore(canvas, currentTransformedClip); foreach (var ch in Children) { - ch.Render(canvas); + ch.Render(canvas, currentTransformedClip); } } - public override void Update(ServerCompositionTarget root, Matrix4x4 transform) + public override void Update(ServerCompositionTarget root) { - base.Update(root, transform); + base.Update(root); foreach (var child in Children) { if (child.AdornedVisual != null) root.EnqueueAdornerUpdate(child); else - child.Update(root, GlobalTransformMatrix); + child.Update(root); } + + IsDirtyComposition = false; } partial void Initialize() diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index b53e31c9cf..a2af34c7f0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -81,7 +81,7 @@ namespace Avalonia.Rendering.Composition.Server new IntPtr(offset))); } - public void NotifyAnimatedValueChanged(int offset) + public virtual void NotifyAnimatedValueChanged(int offset) { ref var store = ref GetStoreFromOffset(offset); store.Invalidate(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs new file mode 100644 index 0000000000..e434b97b2f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.DirtyProperties.cs @@ -0,0 +1,76 @@ +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositionVisual +{ + protected bool IsDirtyComposition; + private bool _combinedTransformDirty; + private bool _clipSizeDirty; + + private const CompositionVisualChangedFields CompositionFieldsMask + = CompositionVisualChangedFields.Opacity + | CompositionVisualChangedFields.OpacityAnimated + | CompositionVisualChangedFields.OpacityMaskBrush + | CompositionVisualChangedFields.Clip + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.ClipToBoundsAnimated + | CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated; + + private const CompositionVisualChangedFields CombinedTransformFieldsMask = + CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.AnchorPoint + | CompositionVisualChangedFields.AnchorPointAnimated + | CompositionVisualChangedFields.CenterPoint + | CompositionVisualChangedFields.CenterPointAnimated + | CompositionVisualChangedFields.AdornedVisual + | CompositionVisualChangedFields.TransformMatrix + | CompositionVisualChangedFields.Scale + | CompositionVisualChangedFields.ScaleAnimated + | CompositionVisualChangedFields.RotationAngle + | CompositionVisualChangedFields.RotationAngleAnimated + | CompositionVisualChangedFields.Orientation + | CompositionVisualChangedFields.OrientationAnimated + | CompositionVisualChangedFields.Offset + | CompositionVisualChangedFields.OffsetAnimated; + + private const CompositionVisualChangedFields ClipSizeDirtyMask = + CompositionVisualChangedFields.Size + | CompositionVisualChangedFields.SizeAnimated + | CompositionVisualChangedFields.ClipToBounds + | CompositionVisualChangedFields.ClipToBoundsAnimated; + + partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) + { + if ((changed & CompositionFieldsMask) != 0) + IsDirtyComposition = true; + if ((changed & CombinedTransformFieldsMask) != 0) + _combinedTransformDirty = true; + if ((changed & ClipSizeDirtyMask) != 0) + _clipSizeDirty = true; + } + + public override void NotifyAnimatedValueChanged(int offset) + { + base.NotifyAnimatedValueChanged(offset); + if (offset == s_OffsetOf_clipToBounds + || offset == s_OffsetOf_opacity + || offset == s_OffsetOf_size) + IsDirtyComposition = true; + + if (offset == s_OffsetOf_size + || offset == s_OffsetOf_anchorPoint + || offset == s_OffsetOf_centerPoint + || offset == s_OffsetOf_adornedVisual + || offset == s_OffsetOf_transformMatrix + || offset == s_OffsetOf_scale + || offset == s_OffsetOf_rotationAngle + || offset == s_OffsetOf_orientation + || offset == s_OffsetOf_offset) + _combinedTransformDirty = true; + + if (offset == s_OffsetOf_clipToBounds + || offset == s_OffsetOf_size) + _clipSizeDirty = true; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs index 04eba72594..7d98b3b246 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisual.cs @@ -16,22 +16,30 @@ namespace Avalonia.Rendering.Composition.Server /// partial class ServerCompositionVisual : ServerObject { - private bool _isDirty; - private bool _isDirtyComposition; - private CompositionProperties _oldCompositionProperties; + private bool _isDirtyForUpdate; + private Rect _oldOwnContentBounds; private bool _isBackface; - protected virtual void RenderCore(CompositorDrawingContextProxy canvas) + private Rect? _transformedClipBounds; + private Rect _combinedTransformedClipBounds; + + protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { } - - public void Render(CompositorDrawingContextProxy canvas) + + public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) { - _isDirtyComposition = false; if(Visible == false || IsVisibleInFrame == false) return; if(Opacity == 0) return; + + currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); + if(currentTransformedClip.IsEmpty) + return; + + Root!.RenderedVisuals++; + var transform = GlobalTransformMatrix; canvas.PreTransform = MatrixUtils.ToMatrix(transform); canvas.Transform = Matrix.Identity; @@ -45,7 +53,7 @@ namespace Avalonia.Rendering.Composition.Server if(OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - RenderCore(canvas); + RenderCore(canvas, currentTransformedClip); // Hack to force invalidation of SKMatrix canvas.PreTransform = MatrixUtils.ToMatrix(transform); @@ -76,18 +84,29 @@ namespace Avalonia.Rendering.Composition.Server return ref _readback2; } - public Matrix4x4 CombinedTransformMatrix { get; private set; } + public Matrix4x4 CombinedTransformMatrix { get; private set; } = Matrix4x4.Identity; public Matrix4x4 GlobalTransformMatrix { get; private set; } - public virtual void Update(ServerCompositionTarget root, Matrix4x4 transform) + public virtual void Update(ServerCompositionTarget root) { - // Calculate new parent-relative transform - CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, - // HACK: Ignore RenderTransform set by the adorner layer - AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, - Scale, RotationAngle, Orientation, Offset); + if(Parent == null && Root == null) + return; - var newTransform = CombinedTransformMatrix * transform; + var wasVisible = IsVisibleInFrame; + + // Calculate new parent-relative transform + if (_combinedTransformDirty) + { + CombinedTransformMatrix = MatrixUtils.ComputeTransform(Size, AnchorPoint, CenterPoint, + // HACK: Ignore RenderTransform set by the adorner layer + AdornedVisual != null ? Matrix4x4.Identity : TransformMatrix, + Scale, RotationAngle, Orientation, Offset); + _combinedTransformDirty = false; + } + + var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix4x4.Identity; + + var newTransform = CombinedTransformMatrix * parentTransform; // Check if visual was moved and recalculate face orientation var positionChanged = false; @@ -97,36 +116,69 @@ namespace Avalonia.Rendering.Composition.Server new Vector3(0, 0, float.PositiveInfinity), GlobalTransformMatrix).Z <= 0; positionChanged = true; } + + var oldTransformedContentBounds = TransformedOwnContentBounds; + var oldCombinedTransformedClipBounds = _combinedTransformedClipBounds; + + + if (_parent.Value?.IsDirtyComposition == true) + { + IsDirtyComposition = true; + _isDirtyForUpdate = true; + } + + GlobalTransformMatrix = newTransform; - var wasVisible = IsVisibleInFrame; + var ownBounds = OwnContentBounds; + if (ownBounds != _oldOwnContentBounds || positionChanged) + { + _oldOwnContentBounds = ownBounds; + if (ownBounds.IsEmpty) + TransformedOwnContentBounds = default; + else + TransformedOwnContentBounds = + ownBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); + } + if (_clipSizeDirty || positionChanged) + { + _transformedClipBounds = ClipToBounds + ? new Rect(new Size(Size.X, Size.Y)) + .TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)) + : null; + + _clipSizeDirty = false; + } + + _combinedTransformedClipBounds = Parent?._combinedTransformedClipBounds ?? new Rect(Root!.Size); + if (_transformedClipBounds != null) + _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); + EffectiveOpacity = Opacity * (Parent?.EffectiveOpacity ?? 1); - IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface; + + IsVisibleInFrame = Visible && EffectiveOpacity > 0.04 && !_isBackface && + !_combinedTransformedClipBounds.IsEmpty; + + if (wasVisible != IsVisibleInFrame) + _isDirtyForUpdate = true; // Invalidate previous rect and queue new rect based on visibility if (positionChanged) { if(wasVisible) - Root!.AddDirtyRect(TransformedOwnContentBounds); + AddDirtyRect(oldTransformedContentBounds.Intersect(oldCombinedTransformedClipBounds)); if (IsVisibleInFrame) - _isDirty = true; + _isDirtyForUpdate = true; } + + // Invalidate new bounds + if (IsVisibleInFrame && _isDirtyForUpdate) + AddDirtyRect(TransformedOwnContentBounds.Intersect(_combinedTransformedClipBounds)); - if (wasVisible != IsVisibleInFrame) - _isDirty = true; - - if (_parent.Value?._isDirtyComposition == true) - _isDirty = true; - GlobalTransformMatrix = newTransform; - //TODO: Cache - TransformedOwnContentBounds = OwnContentBounds.TransformToAABB(MatrixUtils.ToMatrix(GlobalTransformMatrix)); - if (IsVisibleInFrame && _isDirty) - Root!.AddDirtyRect(TransformedOwnContentBounds); - - _isDirty = false; + _isDirtyForUpdate = false; // Update readback indices var i = Root!.Readback; @@ -135,15 +187,13 @@ namespace Avalonia.Rendering.Composition.Server readback.Matrix = CombinedTransformMatrix; readback.TargetId = Root.Id; readback.Visible = IsVisibleInFrame; + } - // Forcefully mark any children visuals are dirty if any of the composition - // properties were changed since the last update - var newProps = GetCompositionProperties(); - if (!newProps.Equals(_oldCompositionProperties)) - { - _isDirtyComposition = true; - _oldCompositionProperties = newProps; - } + void AddDirtyRect(Rect rc) + { + if(rc == Rect.Empty) + return; + Root?.AddDirtyRect(rc); } /// @@ -176,36 +226,10 @@ namespace Avalonia.Rendering.Composition.Server protected override void ValuesInvalidated() { - _isDirty = true; - if (IsVisibleInFrame) - Root?.AddDirtyRect(TransformedOwnContentBounds); - else - Root?.Invalidate(); + _isDirtyForUpdate = true; + Root?.Invalidate(); } - struct CompositionProperties - { - public double EffectiveOpacity { get; set; } - public Vector2? ClipSize { get; set; } - public IGeometryImpl? Clip { get; set; } - public IBrush? OpacityMaskBrush { get; set; } - - public bool Equals(CompositionProperties other) => - EffectiveOpacity == other.EffectiveOpacity - && ClipSize == other.ClipSize - && Clip == other.Clip - && OpacityMaskBrush == other.OpacityMaskBrush; - } - - private CompositionProperties GetCompositionProperties() => new CompositionProperties - { - EffectiveOpacity = EffectiveOpacity, - Clip = Clip, - ClipSize = ClipToBounds ? Size : null, - OpacityMaskBrush = OpacityMaskBrush - }; - - public bool IsVisibleInFrame { get; set; } public double EffectiveOpacity { get; set; } public Rect TransformedOwnContentBounds { get; set; } diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index ad7f2200a6..4df5a91c4b 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -324,10 +324,16 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"protected override void Deactivated(){{}}")!).WithBody(deactivatedBody)); if (cl.Properties.Count > 0) + { server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration( $"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan commitedAt){{}}") !) - .WithBody(deserializeMethodBody)); + .WithBody(ApplyDeserializeChangesEpilogue(deserializeMethodBody, cl))); + server = server.AddMembers(MethodDeclaration(ParseTypeName("void"), "OnFieldsDeserialized") + .WithParameterList(ParameterList(SingletonSeparatedList(Parameter(Identifier("changed")) + .WithType(ParseTypeName(ChangedFieldsTypeName(cl)))))) + .AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon())); + } client = client.AddMembers( MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody)) @@ -545,6 +551,11 @@ DeserializeChangesExtra(reader); var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); ")); } + + private BlockSyntax ApplyDeserializeChangesEpilogue(BlockSyntax body, GClass cl) + { + return body.AddStatements(ParseStatement("OnFieldsDeserialized(changed);")); + } BlockSyntax ApplyDeserializeField(BlockSyntax body, GClass cl, GProperty prop, string serverType, bool isObject) {