Skip to content

feat(skia): collapse visual subtrees into one SKPicture when the subtree doesn't change for a bit #19829

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ void ISkiaSurface.UpdateSurface(bool recreateSurface)

var previousCompMode = Compositor.IsSoftwareRenderer;
Compositor.IsSoftwareRenderer = true;
SourceVisual.RenderRootVisual(_surface.Canvas, SourceOffset, null);
SourceVisual.RenderRootVisual(_surface.Canvas, SourceOffset, null, false);
Compositor.IsSoftwareRenderer = previousCompMode;
}
}
@@ -82,7 +82,7 @@ void ISkiaSurface.UpdateSurface(in Visual.PaintingSession session)
session.Canvas.ClipRect(new SKRect(0, 0, int.MaxValue, int.MaxValue));
}

SourceVisual.RenderRootVisual(session.Canvas, SourceOffset, null);
SourceVisual.RenderRootVisual(session.Canvas, SourceOffset, null, false);
session.Canvas.RestoreToCount(save);
}
}
4 changes: 2 additions & 2 deletions src/Uno.UI.Composition/Composition/Compositor.skia.cs
Original file line number Diff line number Diff line change
@@ -106,7 +106,7 @@ internal bool TryGetEffectiveBackgroundColor(CompositionSpriteShape shape, out C
return false;
}

internal void RenderRootVisual(SKCanvas canvas, ContainerVisual rootVisual, Action<SKCanvas, Visual>? postRenderAction)
internal void RenderRootVisual(SKCanvas canvas, ContainerVisual rootVisual, Action<SKCanvas, Visual>? postRenderAction, bool applyChildOptimization)
{
if (rootVisual is null)
{
@@ -118,7 +118,7 @@ internal void RenderRootVisual(SKCanvas canvas, ContainerVisual rootVisual, Acti
animation.RaiseAnimationFrame();
}

rootVisual.RenderRootVisual(canvas, null, postRenderAction);
rootVisual.RenderRootVisual(canvas, null, postRenderAction, applyChildOptimization);

for (var current = _backgroundTransitions.First; current != null; current = current.Next)
{
18 changes: 17 additions & 1 deletion src/Uno.UI.Composition/Composition/ContainerVisual.skia.cs
Original file line number Diff line number Diff line change
@@ -13,14 +13,24 @@ public partial class ContainerVisual : Visual
{
private List<Visual>? _childrenInRenderOrder;
private bool _hasCustomRenderOrder;
private int? _subtreeVisualCount;

private (Rect rect, bool isAncestorClip)? _layoutClip;

private GCHandle _gcHandle;

partial void InitializePartial()
{
Children.CollectionChanged += (s, e) => IsChildrenRenderOrderDirty = true;
Children.CollectionChanged += (s, e) =>
{
var parent = this;
while (parent is not null)
{
parent._subtreeVisualCount = null;
parent = parent.Parent;
}
IsChildrenRenderOrderDirty = true;
};

_gcHandle = GCHandle.Alloc(this, GCHandleType.Weak);
Handle = GCHandle.ToIntPtr(_gcHandle);
@@ -145,4 +155,10 @@ internal override bool SetMatrixDirty()

return false;
}

internal override int GetSubTreeVisualCount()
{
_subtreeVisualCount ??= Children.Count + Children.InnerList.Aggregate(0, (acc, visual) => acc + visual.GetSubTreeVisualCount());
return _subtreeVisualCount.Value;
}
}
2 changes: 1 addition & 1 deletion src/Uno.UI.Composition/Composition/RedirectVisual.skia.cs
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ internal override void Paint(in PaintingSession session)

if (Source is not null && session.Canvas is { } canvas)
{
Source.RenderRootVisual(canvas, null, null);
Source.RenderRootVisual(canvas, null, null, false);
}
}

72 changes: 61 additions & 11 deletions src/Uno.UI.Composition/Composition/Visual.skia.cs
Original file line number Diff line number Diff line change
@@ -7,15 +7,16 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using SkiaSharp;
using Uno.Extensions;
using Uno.Helpers;
using Uno.UI.Composition;
using Uno.UI.Composition.Composition;

namespace Microsoft.UI.Composition;

public partial class Visual : global::Microsoft.UI.Composition.CompositionObject
{
private const int PictureCollapsingOptimizationFrameThreshold = 150;
private const int PictureCollapsingOptimizationVisualCountThreshold = 100;

private static readonly SKPath _spareRenderPath = new SKPath();

private static readonly IPrivateSessionFactory _factory = new PaintingSession.SessionFactory();
@@ -31,8 +32,12 @@ public partial class Visual : global::Microsoft.UI.Composition.CompositionObject
private int _zIndex;
private Matrix4x4 _totalMatrix = Matrix4x4.Identity;
private SKPicture? _picture;
private SKPicture? _childrenPicture;

private VisualFlags _flags = VisualFlags.MatrixDirty | VisualFlags.PaintDirty | VisualFlags.SubtreePaintDirty;
private int _framesSinceSubtreePaintClean;

private VisualFlags _flags = VisualFlags.MatrixDirty | VisualFlags.PaintDirty;
internal virtual int GetSubTreeVisualCount() => 1;

internal bool IsNativeHostVisual => (_flags & VisualFlags.IsNativeHostVisualSet) != 0 ? (_flags & VisualFlags.IsNativeHostVisual) != 0 : (_flags & VisualFlags.IsNativeHostVisualInherited) != 0;

@@ -92,6 +97,7 @@ internal virtual bool SetMatrixDirty()
{
var matrixDirty = (_flags & VisualFlags.MatrixDirty) != 0;
_flags |= VisualFlags.MatrixDirty;
InvalidateParentSubtree();
return !matrixDirty;
}

@@ -180,6 +186,19 @@ internal void InvalidatePaint()
_picture?.Dispose();
_picture = null;
_flags |= VisualFlags.PaintDirty;
InvalidateParentSubtree();
}

private void InvalidateParentSubtree()
{
var parent = this.Parent;
while (parent is not null && (parent._flags & VisualFlags.SubtreePaintDirty) == 0)
{
parent._childrenPicture?.Dispose();
parent._childrenPicture = null;
parent._flags |= VisualFlags.SubtreePaintDirty;
parent = parent.Parent;
}
}

public CompositionClip? Clip
@@ -231,7 +250,8 @@ partial void OnIsVisibleChanged(bool value)
/// <param name="canvas">The canvas on which this visual should be rendered.</param>
/// <param name="offsetOverride">The offset (from the origin) to render the Visual at. If null, the offset properties on the Visual like <see cref="Offset"/> and <see cref="AnchorPoint"/> are used.</param>
/// <param name="postRenderAction">An action that gets invoked right after each visual finishes rendering. This can be used when there is a need to walk the visual tree regularly with minimal performance impact.</param>
internal void RenderRootVisual(SKCanvas canvas, Vector2? offsetOverride, Action<SKCanvas, Visual>? postRenderAction)
/// <param name="applyChildOptimization"> Applies heuristics to combine certain visual subtrees into one big SKPicture.</param>
internal void RenderRootVisual(SKCanvas canvas, Vector2? offsetOverride, Action<SKCanvas, Visual>? postRenderAction, bool applyChildOptimization)
{
if (this is { Opacity: 0 } or { IsVisible: false })
{
@@ -267,7 +287,7 @@ internal void RenderRootVisual(SKCanvas canvas, Vector2? offsetOverride, Action<

using (session)
{
Render(session, postRenderAction);
Render(session, postRenderAction, applyChildOptimization);
}

canvas.Restore();
@@ -278,18 +298,20 @@ internal void RenderRootVisual(SKCanvas canvas, Vector2? offsetOverride, Action<
/// </summary>
/// <param name="parentSession">The drawing session of the <see cref="Parent"/> visual.</param>
/// <param name="postRenderAction">An action that gets invoked right after the visual finishes rendering. This can be used when there is a need to walk the visual tree regularly with minimal performance impact.</param>
private void Render(in PaintingSession parentSession, Action<SKCanvas, Visual>? postRenderAction)
/// <param name="applyChildOptimization"> Applies heuristics to combine certain visual subtrees into one big SKPicture.</param>
private void Render(in PaintingSession parentSession, Action<SKCanvas, Visual>? postRenderAction, bool applyChildOptimization)
{
#if TRACE_COMPOSITION
var indent = int.TryParse(Comment?.Split(new char[] { '-' }, 2, StringSplitOptions.TrimEntries).FirstOrDefault(), out var depth)
? new string(' ', depth * 2)
: string.Empty;
global::System.Diagnostics.Debug.WriteLine($"{indent}{Comment} (Opacity:{parentSession.Opacity:F2}x{Opacity:F2} | IsVisible:{IsVisible})");
#endif

if (this is { Opacity: 0 } or { IsVisible: false })
_framesSinceSubtreePaintClean++;
if ((_flags & VisualFlags.SubtreePaintDirty) == VisualFlags.SubtreePaintDirty)
{
return;
_framesSinceSubtreePaintClean = 0;
_flags &= ~VisualFlags.SubtreePaintDirty;
}

CreateLocalSession(in parentSession, out var session);
@@ -344,9 +366,35 @@ private void Render(in PaintingSession parentSession, Action<SKCanvas, Visual>?
canvas.ClipPath(postClip, antialias: true);
}

foreach (var child in GetChildrenInRenderOrder())
if (_childrenPicture is not null)
{
child.Render(in session, postRenderAction);
canvas.DrawPicture(_childrenPicture);
}
else if (_framesSinceSubtreePaintClean < PictureCollapsingOptimizationFrameThreshold
|| !applyChildOptimization
|| GetSubTreeVisualCount() < PictureCollapsingOptimizationVisualCountThreshold)
{
foreach (var child in GetChildrenInRenderOrder())
{
child.Render(in session, postRenderAction, applyChildOptimization);
}
}
else
{
var recorder = new SKPictureRecorder();
var recordingCanvas = recorder.BeginRecording(new SKRect(-999999, -999999, 999999, 999999));
// child.Render will reapply the total transform matrix, so we need to invert ours.
Matrix4x4.Invert(TotalMatrix, out var rootTransform);
_factory.CreateInstance(this, recordingCanvas, ref rootTransform, session.Opacity, out var childSession);
using (childSession)
{
foreach (var child in GetChildrenInRenderOrder())
{
child.Render(in childSession, postRenderAction, applyChildOptimization: false);
}
}
_childrenPicture = recorder.EndRecording();
canvas.DrawPicture(_childrenPicture);
}
}
}
@@ -370,6 +418,7 @@ private Vector3 GetTotalOffset()
return Offset;
}


internal virtual bool GetPrePaintingClipping(SKPath dst)
{
// Apply the clipping defined on the element
@@ -433,5 +482,6 @@ internal enum VisualFlags : byte
IsNativeHostVisualInherited = 4,
MatrixDirty = 8,
PaintDirty = 16,
SubtreePaintDirty = 32, // some child in the subtree of this visual is dirty.
}
}
2 changes: 1 addition & 1 deletion src/Uno.UI.Runtime.Skia.Linux.FrameBuffer/Renderer.cs
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@ void Invalidate()

if (_host.RootElement?.Visual is { } rootVisual)
{
_host.RootElement.XamlRoot!.Compositor.RenderRootVisual(surface.Canvas, rootVisual, null);
_host.RootElement.XamlRoot!.Compositor.RenderRootVisual(surface.Canvas, rootVisual, null, FeatureConfiguration.Rendering.CollapseVisualSubtreeSKPictures);
}
else
{
7 changes: 7 additions & 0 deletions src/Uno.UI/FeatureConfiguration.cs
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
using Uno.Foundation.Logging;
using Uno.UI.Xaml.Controls;
using System.Runtime.InteropServices;
using Microsoft.UI.Composition;

namespace Uno.UI
{
@@ -851,6 +852,12 @@ public static class Rendering
/// Determines if OpenGL rendering should be enabled on the Android target when using the skia renderer.
/// </summary>
public static bool UseOpenGLOnSkiaAndroid { get; set; } = true;

/// <summary>
/// Applies heuristics to combine certain visual subtrees into one big SKPicture
/// instead of each visual having its own recorder.
/// </summary>
public static bool CollapseVisualSubtreeSKPictures { get; set; }
}

public static class DependencyProperty
4 changes: 2 additions & 2 deletions src/Uno.UI/Helpers/SkiaRenderHelper.skia.cs
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@ public static SKPath RenderRootVisualAndReturnNegativePath(int width, int height
{
if (!ContentPresenter.HasNativeElements())
{
rootVisual.Compositor.RenderRootVisual(canvas, rootVisual, null);
rootVisual.Compositor.RenderRootVisual(canvas, rootVisual, null, FeatureConfiguration.Rendering.CollapseVisualSubtreeSKPictures);
return null;
}
else
@@ -82,7 +82,7 @@ public static SKPath RenderRootVisualAndReturnNegativePath(int width, int height
mainPath = mainPath!.Op(
finalVisualPath,
visual.IsNativeHostVisual ? SKPathOp.Difference : SKPathOp.Union);
});
}, false);

return mainPath;
}
3 changes: 1 addition & 2 deletions src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.skia.cs
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@
using Microsoft.UI.Composition;
using Uno.UI.Xaml.Media;
using SkiaSharp;
using Uno.UI.Composition;

namespace Microsoft.UI.Xaml.Media.Imaging
{
@@ -58,7 +57,7 @@ private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIEle
var canvas = surface.Canvas;
canvas.Clear(SKColors.Transparent);
canvas.Scale((float)dpi);
visual.RenderRootVisual(canvas, offsetOverride: Vector2.Zero, null);
visual.RenderRootVisual(canvas, offsetOverride: Vector2.Zero, null, false);

var img = surface.Snapshot();