Browse Source

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 <maxkatz6@outlook.com>
release/11.1.0-beta2
Nikita Tsukanov 2 years ago
committed by Max Katz
parent
commit
42b2d95464
  1. 4
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  2. 9
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  3. 152
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs
  4. 166
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  5. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  6. 4
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  7. 8
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  8. 5
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  9. 11
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs

4
src/Avalonia.Base/Collections/Pooled/PooledList.cs

@ -525,6 +525,10 @@ namespace Avalonia.Collections.Pooled
public ReadOnlyCollection<T> AsReadOnly()
=> new ReadOnlyCollection<T>(this);
/// <summary>Gets a <see cref="T:System.Span`1" /> view over the data in a list.
/// Items should not be added or removed from the list while the returned span is in use.</summary>
public Span<T> AsSpan() => new(_items, 0, _size);
/// <summary>
/// Searches a section of the list for a given element using a binary search
/// algorithm.

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

152
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<PendingCommand> _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();
}
}

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

@ -11,13 +11,9 @@ using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server;
/// <summary>
/// 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.
/// </summary>
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();
}
}

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

4
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();

8
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)

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

11
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<PaintWrapper> _maskStack = new();
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;
@ -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)));
}
/// <inheritdoc />
@ -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);
}

Loading…
Cancel
Save