From 42b2d95464b29c68806bb0455b940d2c8516691e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 29 Mar 2024 12:55:13 +0500 Subject: [PATCH] Use DrawingContextProxy as a staging area for Push/Pop/SetTransform operations (#15113) * Skip drawing visual content if bounds don't intersect with the current clip and dirty rect clip * PopOpacityMask should reset the current transform to what it was during PushOpacityMask call before applying the mask * Buffer pending Push/Transform commands until an actual drawing command gets issued. That way we can avoid otherwise expensive platform calls for visual subtrees that don't actually produce any render results due to being clipped --------- Co-authored-by: Max Katz --- .../Collections/Pooled/PooledList.cs | 4 + .../Media/ImmediateDrawingContext.cs | 9 +- .../DrawingContextProxy.PendingCommands.cs | 152 ++++++++++++++++ .../Composition/Server/DrawingContextProxy.cs | 166 ++++++++++++++---- .../Server/ServerCompositionDrawListVisual.cs | 4 +- .../Server/ServerCompositionTarget.cs | 4 +- .../Server/ServerCompositionVisual.cs | 8 +- .../Server/ServerCustomCompositionVisual.cs | 5 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 11 +- 9 files changed, 310 insertions(+), 53 deletions(-) create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index 73e1b28798..a6e02c355e 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -525,6 +525,10 @@ namespace Avalonia.Collections.Pooled public ReadOnlyCollection AsReadOnly() => new ReadOnlyCollection(this); + /// Gets a view over the data in a list. + /// Items should not be added or removed from the list while the returned span is in use. + public Span AsSpan() => new(_items, 0, _size); + /// /// Searches a section of the list for a given element using a binary search /// algorithm. diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index 1db53bd576..7eb9b91f9a 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -35,11 +35,16 @@ namespace Avalonia.Media } } - internal ImmediateDrawingContext(IDrawingContextImpl impl, bool ownsImpl) + internal ImmediateDrawingContext(IDrawingContextImpl impl, bool ownsImpl) : this(impl, impl.Transform, ownsImpl) + { + + } + + internal ImmediateDrawingContext(IDrawingContextImpl impl, Matrix transform, bool ownsImpl) { _ownsImpl = ownsImpl; PlatformImpl = impl; - _currentContainerTransform = impl.Transform; + _currentContainerTransform = transform; } public IDrawingContextImpl PlatformImpl { get; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs new file mode 100644 index 0000000000..bbf648d71f --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs @@ -0,0 +1,152 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Avalonia.Collections.Pooled; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class CompositorDrawingContextProxy +{ + + private PooledList _commands = new(); + private bool _autoFlush; + + + enum PendingCommandType + { + SetTransform, + PushClip, + PushOpacity, + PushOpacityMask, + PushGeometryClip, + PushRenderOptions, + PushEffect + } + + [StructLayout(LayoutKind.Explicit)] + struct PendingCommandObjectUnion + { + [FieldOffset(0)] public IEffect? Effect; + [FieldOffset(0)] public IBrush? Mask; + [FieldOffset(0)] public IGeometryImpl? Clip; + } + + [StructLayout(LayoutKind.Explicit)] + struct PendingCommandDataUnion + { + // PushOpacity + [FieldOffset(0)] public double Opacity; + [FieldOffset(8)] public Rect? NullableOpacityRect; + + [FieldOffset(0)] public Matrix Transform; + + [FieldOffset(0)] public RenderOptions RenderOptions; + + // PushClip/PushOpacityMask + [FieldOffset(0)] public bool IsRoundRect; + [FieldOffset(4)] public RoundedRect RoundRect; + [FieldOffset(4)] public Rect NormalRect; + } + + struct PendingCommand + { + public PendingCommandType Type; + public PendingCommandObjectUnion ObjectUnion; + public PendingCommandDataUnion DataUnion; + + } + + public bool AutoFlush + { + get => _autoFlush; + set + { + _autoFlush = value; + if (value) + Flush(); + } + } + + public void SetTransform(Matrix m) + { + if (_autoFlush) + { + SetImplTransform(m); + return; + } + var cmd = new PendingCommand + { + Type = PendingCommandType.SetTransform, + DataUnion = { Transform = m } + }; + if (_commands.Count > 0 && _commands[_commands.Count - 1].Type == PendingCommandType.SetTransform) + _commands[_commands.Count - 1] = cmd; + else + _commands.Add(cmd); + } + + + private bool TryDiscard(PendingCommandType type) + { + while (_commands.Count > 0 && _commands[_commands.Count - 1].Type == PendingCommandType.SetTransform) + _commands.RemoveAt(_commands.Count - 1); + if (_commands.Count == 0) + return false; + if (_commands[_commands.Count - 1].Type == type) + { + _commands.RemoveAt(_commands.Count - 1); + return true; + } + + // Not sure how exactly can we get here, but flush commands just in case + Flush(); + return false; + } + + void AddCommand(PendingCommand command) + { + if(_autoFlush) + ExecCommand(ref command); + else + _commands.Add(command); + } + + void ExecCommand(ref PendingCommand cmd) + { + if (cmd.Type == PendingCommandType.SetTransform) + SetImplTransform(cmd.DataUnion.Transform); + else if (cmd.Type == PendingCommandType.PushOpacity) + _impl.PushOpacity(cmd.DataUnion.Opacity, cmd.DataUnion.NullableOpacityRect); + else if (cmd.Type == PendingCommandType.PushOpacityMask) + _impl.PushOpacityMask(cmd.ObjectUnion.Mask!, cmd.DataUnion.NormalRect); + else if (cmd.Type == PendingCommandType.PushClip) + { + if (cmd.DataUnion.IsRoundRect) + _impl.PushClip(cmd.DataUnion.RoundRect); + else + _impl.PushClip(cmd.DataUnion.NormalRect); + } + else if (cmd.Type == PendingCommandType.PushGeometryClip) + _impl.PushGeometryClip(cmd.ObjectUnion.Clip!); + else if (cmd.Type == PendingCommandType.PushEffect) + { + if (_impl is IDrawingContextImplWithEffects effects) + effects.PushEffect(cmd.ObjectUnion.Effect!); + } + else if (cmd.Type == PendingCommandType.PushRenderOptions) + _impl.PushRenderOptions(cmd.DataUnion.RenderOptions); + else + Debug.Assert(false); + } + + public void Flush() + { + var commands = _commands.AsSpan(); + for (var index = 0; index < commands.Length; index++) + ExecCommand(ref commands[index]); + + _commands.Clear(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 0533278056..75dc13b1e0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -11,13 +11,9 @@ using Avalonia.Utilities; namespace Avalonia.Rendering.Composition.Server; /// -/// A bunch of hacks to make the existing rendering operations and IDrawingContext -/// to work with composition rendering infrastructure. -/// 1) Keeps and applies the transform of the current visual since drawing operations think that -/// they have information about the full render transform (they are not) -/// 2) Keeps the draw list for the VisualBrush contents of the current drawing operation. + /// -internal class CompositorDrawingContextProxy : IDrawingContextImpl, +internal partial class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport, IDrawingContextImplWithEffects { private readonly IDrawingContextImpl _impl; @@ -27,60 +23,84 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl = impl; } - public Matrix PostTransform { get; set; } = Matrix.Identity; - public void Dispose() { - _impl.Dispose(); + Flush(); + _commands.Dispose(); } - Matrix _transform; + public Matrix? PostTransform { get; set; } + Matrix _transform; + public Matrix Transform { get => _transform; - set => _impl.Transform = (_transform = value) * PostTransform; + set + { + _transform = value; + SetTransform(value); + } + } + + void SetImplTransform(Matrix m) + { + _transform = m; + if (PostTransform.HasValue) + m = m * PostTransform.Value; + _impl.Transform = m; } public void Clear(Color color) { + Flush(); _impl.Clear(color); } public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { + Flush(); _impl.DrawBitmap(source, opacity, sourceRect, destRect); } public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { + Flush(); _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); } public void DrawLine(IPen? pen, Point p1, Point p2) { + Flush(); _impl.DrawLine(pen, p1, p2); } public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) { + Flush(); _impl.DrawGeometry(brush, pen, geometry); } public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default) { + Flush(); _impl.DrawRectangle(brush, pen, rect, boxShadows); } - public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region) => + public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region) + { + Flush(); _impl.DrawRegion(brush, pen, region); + } public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) { + Flush(); _impl.DrawEllipse(brush, pen, rect); } public void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun) { + Flush(); _impl.DrawGlyphRun(foreground, glyphRun); } @@ -91,70 +111,140 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, public void PushClip(Rect clip) { - _impl.PushClip(clip); + AddCommand(new() + { + Type = PendingCommandType.PushClip, + DataUnion = + { + NormalRect = clip + } + }); } public void PushClip(RoundedRect clip) { - _impl.PushClip(clip); + AddCommand(new() + { + Type = PendingCommandType.PushClip, + DataUnion = + { + IsRoundRect = true, + RoundRect = clip + } + }); } - public void PushClip(IPlatformRenderInterfaceRegion region) => _impl.PushClip(region); + public void PushClip(IPlatformRenderInterfaceRegion region) + { + Flush(); + _impl.PushClip(region); + } public void PopClip() { - _impl.PopClip(); + if (!TryDiscard(PendingCommandType.PushClip)) + _impl.PopClip(); } - public void PushLayer(Rect bounds) => _impl.PushLayer(bounds); + public void PushLayer(Rect bounds) + { + Flush(); + _impl.PushLayer(bounds); + } - public void PopLayer() => _impl.PopLayer(); + public void PopLayer() + { + Flush(); + _impl.PopLayer(); + } public void PushOpacity(double opacity, Rect? bounds) { - _impl.PushOpacity(opacity, bounds); + AddCommand(new PendingCommand + { + Type = PendingCommandType.PushOpacity, + DataUnion = + { + Opacity = opacity, + NullableOpacityRect = bounds + } + }); } public void PopOpacity() { - _impl.PopOpacity(); + if (!TryDiscard(PendingCommandType.PushOpacity)) + _impl.PopOpacity(); } public void PushOpacityMask(IBrush mask, Rect bounds) { - _impl.PushOpacityMask(mask, bounds); - } - - public void PushRenderOptions(RenderOptions renderOptions) - { - _impl.PushRenderOptions(renderOptions); + AddCommand(new() + { + Type = PendingCommandType.PushOpacityMask, + DataUnion = + { + NormalRect = bounds + }, + ObjectUnion = + { + Mask = mask + } + }); } public void PopOpacityMask() { - _impl.PopOpacityMask(); + if (!TryDiscard(PendingCommandType.PushOpacityMask)) + _impl.PopOpacityMask(); } public void PushGeometryClip(IGeometryImpl clip) { - _impl.PushGeometryClip(clip); + AddCommand(new PendingCommand + { + Type = PendingCommandType.PushGeometryClip, + ObjectUnion = + { + Clip = clip + } + }); } public void PopGeometryClip() { - _impl.PopGeometryClip(); + if (!TryDiscard(PendingCommandType.PushGeometryClip)) + _impl.PopGeometryClip(); + } + + public void PushRenderOptions(RenderOptions renderOptions) + { + AddCommand(new() + { + Type = PendingCommandType.PushRenderOptions, + DataUnion = + { + RenderOptions = renderOptions + } + }); } public void PopRenderOptions() { - _impl.PopRenderOptions(); + if (!TryDiscard(PendingCommandType.PushRenderOptions)) + _impl.PopRenderOptions(); + } + + public object? GetFeature(Type t) + { + Flush(); + return _impl.GetFeature(t); } - public object? GetFeature(Type t) => _impl.GetFeature(t); - public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) { + Flush(); if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) acrylic.DrawRectangle(material, rect); else @@ -163,13 +253,19 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, public void PushEffect(IEffect effect) { - if (_impl is IDrawingContextImplWithEffects effects) - effects.PushEffect(effect); + AddCommand(new() + { + Type = PendingCommandType.PushEffect, + ObjectUnion = + { + Effect = effect + } + }); } public void PopEffect() { - if (_impl is IDrawingContextImplWithEffects effects) + if (!TryDiscard(PendingCommandType.PushEffect) && _impl is IDrawingContextImplWithEffects effects) effects.PopEffect(); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 060ba2c820..3adf028438 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -43,7 +43,9 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, IDirtyRectTracker dirtyRects) { - if (_renderCommands != null) + if (_renderCommands != null + && currentTransformedClip.Intersects(TransformedOwnContentBounds) + && dirtyRects.Intersects(TransformedOwnContentBounds)) { _renderCommands.Render(canvas); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 9eef4bd7e2..a39b3ae03f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -214,8 +214,8 @@ namespace Avalonia.Rendering.Composition.Server if (useLayerClip) context.PushLayer(DirtyRects.CombinedRect.ToRectUnscaled()); - - root.Render(new CompositorDrawingContextProxy(context), null, DirtyRects); + using (var proxy = new CompositorDrawingContextProxy(context)) + root.Render(proxy, null, DirtyRects); if (useLayerClip) context.PopLayer(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index 8b083ba8ff..ccc4b658be 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -49,14 +49,12 @@ namespace Avalonia.Rendering.Composition.Server if (AdornedVisual != null) { - canvas.PostTransform = Matrix.Identity; canvas.Transform = Matrix.Identity; if (AdornerIsClipped) canvas.PushClip(AdornedVisual._combinedTransformedClipBounds.ToRect()); } var transform = GlobalTransformMatrix; - canvas.PostTransform = transform; - canvas.Transform = Matrix.Identity; + canvas.Transform = transform; var applyRenderOptions = RenderOptions != default; @@ -76,10 +74,6 @@ namespace Avalonia.Rendering.Composition.Server RenderCore(canvas, currentTransformedClip, dirtyRects); - // Hack to force invalidation of SKMatrix - canvas.PostTransform = transform; - canvas.Transform = Matrix.Identity; - if (OpacityMaskBrush != null) canvas.PopOpacityMask(); if (Clip != null) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs index db33a92728..cfcffb00f0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs @@ -74,7 +74,8 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, IDirtyRectTracker dirtyRects) { - using var context = new ImmediateDrawingContext(canvas, false); + canvas.AutoFlush = true; + using var context = new ImmediateDrawingContext(canvas, GlobalTransformMatrix, false); try { _handler.Render(context, currentTransformedClip.ToRect()); @@ -84,5 +85,7 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer Logger.TryGet(LogEventLevel.Error, LogArea.Visual) ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e); } + + canvas.AutoFlush = false; } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ca0f020794..6572ba9db6 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Skia // TODO: Get rid of this value, it's currently used to calculate intermediate sizes for tile brushes // but does so ignoring the current transform private readonly Vector _intermediateSurfaceDpi; - private readonly Stack _maskStack = new(); + private readonly Stack<(SKMatrix matrix, PaintWrapper paint)> _maskStack = new(); private readonly Stack _opacityStack = new(); private readonly Stack _renderOptionsStack = new(); private readonly Matrix? _postTransform; @@ -813,7 +813,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); Canvas.SaveLayer(bounds.ToSKRect(), paint); - _maskStack.Push(CreatePaint(paint, mask, bounds)); + _maskStack.Push((Canvas.TotalMatrix, CreatePaint(paint, mask, bounds))); } /// @@ -826,9 +826,10 @@ namespace Avalonia.Skia Canvas.SaveLayer(paint); SKPaintCache.Shared.ReturnReset(paint); - - PaintWrapper paintWrapper; - using (paintWrapper = _maskStack.Pop()) + + var (transform, paintWrapper) = _maskStack.Pop(); + Canvas.SetMatrix(transform); + using (paintWrapper) { Canvas.DrawPaint(paintWrapper.Paint); }