Skip to content
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

perf: reduce SKPaint and SKPath allocations #18476

Merged
9 changes: 8 additions & 1 deletion src/SamplesApp/UITests.Shared/UITests.Shared.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -5350,6 +5350,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\Path_Clipping.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\Path_Geometries.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
Expand Down Expand Up @@ -8797,6 +8801,9 @@
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\Path_Custom.xaml.cs">
<DependentUpon>Path_Custom.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\Path_Clipping.xaml.cs">
<DependentUpon>Path_Clipping.xaml</DependentUpon>
</Compile>
<Compile Include="$(MSBuildThisFileDirectory)Windows_UI_Xaml_Shapes\Path_Geometries.xaml.cs">
<DependentUpon>Path_Geometries.xaml</DependentUpon>
</Compile>
Expand Down Expand Up @@ -9841,4 +9848,4 @@
</Compile>
</ItemGroup>
<Import Project="ItemExclusions.props" />
</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Page
x:Class="UITests.Windows_UI_Xaml_Shapes.Path_Clipping"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:UITests.Windows_UI_Xaml_Shapes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
mc:Ignorable="d">

<StackPanel>
<TextBlock Text="The two icons below should not be the same (the first one should be clipped)." />
<Border Height="16" HorizontalAlignment="Stretch" />
<ScrollViewer Height="30" x:Name="sv">
<Path Fill="Red" Data="m 0,0 c -0.639,0 -1.276,0.243 -1.765,0.729 -0.977,0.974 -0.98,2.557 -0.006,3.535 L 16.925,23.032 -1.768,41.725 c -0.976,0.976 -0.976,2.559 0,3.535 0.977,0.976 2.559,0.976 3.536,0 L 22.225,24.803 c 0.975,-0.975 0.976,-2.555 0.004,-3.532 L 1.771,0.736 C 1.283,0.245 0.642,0 0,0" />
</ScrollViewer>
<TextBlock Text="------------" />
<Path Fill="Red" Data="m 0,0 c -0.639,0 -1.276,0.243 -1.765,0.729 -0.977,0.974 -0.98,2.557 -0.006,3.535 L 16.925,23.032 -1.768,41.725 c -0.976,0.976 -0.976,2.559 0,3.535 0.977,0.976 2.559,0.976 3.536,0 L 22.225,24.803 c 0.975,-0.975 0.976,-2.555 0.004,-3.532 L 1.771,0.736 C 1.283,0.245 0.642,0 0,0" />
</StackPanel>
</Page>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Uno.UI.Samples.Controls;
using Microsoft.UI.Xaml.Controls;

namespace UITests.Windows_UI_Xaml_Shapes
{
[Sample("Shapes", IsManualTest = true)]
public sealed partial class Path_Clipping : Page
{
public Path_Clipping()
{
this.InitializeComponent();
Loaded += (_, _) => sv.ScrollToVerticalOffset(9999);
}
}
}
11 changes: 6 additions & 5 deletions src/Uno.UI.Composition/Composition/CompositionMaskBrush.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ public partial class CompositionMaskBrush : CompositionBrush, IOnlineBrush
{
private SKPaint? _sourcePaint;
private SKPaint? _maskPaint;
private SKPaint? _resultPaint;

bool IOnlineBrush.IsOnline
{
Expand All @@ -36,10 +35,13 @@ internal override void UpdatePaint(SKPaint paint, SKRect bounds)

void IOnlineBrush.Paint(in Visual.PaintingSession session, SKRect bounds)
{
_resultPaint ??= new SKPaint() { IsAntialias = true };
using (SkiaHelper.GetTempSKPaint(out var resultPaint))
{
resultPaint.IsAntialias = true;

UpdatePaint(_resultPaint, bounds);
session.Canvas?.DrawRect(bounds, _resultPaint);
UpdatePaint(resultPaint, bounds);
session.Canvas?.DrawRect(bounds, resultPaint);
}
}

private protected override void DisposeInternal()
Expand All @@ -48,7 +50,6 @@ private protected override void DisposeInternal()

_sourcePaint?.Dispose();
_maskPaint?.Dispose();
_resultPaint?.Dispose();
}
}
}
91 changes: 41 additions & 50 deletions src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
using Windows.Foundation;
using SkiaSharp;
using Uno;
using Uno.Disposables;
using Uno.Extensions;

namespace Microsoft.UI.Composition
{
public partial class CompositionSpriteShape : CompositionShape
{
private SKPaint? _strokePaint;
private SKPaint? _fillPaint;
private SKPath? _geometryWithTransformations;

internal override void Paint(in Visual.PaintingSession session)
Expand All @@ -19,7 +18,7 @@ internal override void Paint(in Visual.PaintingSession session)
{
if (FillBrush is { } fill)
{
var fillPaint = TryCreateAndClearFillPaint(session.Filters.OpacityColorFilter);
using var fillPaintDisposable = GetTempFillPaint(session.Filters.OpacityColorFilter, out var fillPaint);

if (Compositor.TryGetEffectiveBackgroundColor(this, out var colorFromTransition))
{
Expand Down Expand Up @@ -52,8 +51,8 @@ internal override void Paint(in Visual.PaintingSession session)

if (StrokeBrush is { } stroke && StrokeThickness > 0)
{
var fillPaint = TryCreateAndClearFillPaint(session.Filters.OpacityColorFilter);
var strokePaint = TryCreateAndClearStrokePaint(session.Filters.OpacityColorFilter);
using var fillPaintDisposable = GetTempFillPaint(session.Filters.OpacityColorFilter, out var fillPaint);
using var strokePaintDisposable = GetTempStrokePaint(session.Filters.OpacityColorFilter, out var strokePaint);

// Set stroke thickness
strokePaint.StrokeWidth = StrokeThickness;
Expand All @@ -78,59 +77,45 @@ internal override void Paint(in Visual.PaintingSession session)
// On Windows, the stroke is simply 1px, it doesn't scale with the height.
// So, to get a correct stroke geometry, we must apply the transformations first.

// Get the stroke geometry, after scaling has been applied.
var strokeGeometry = strokePaint.GetFillPath(geometryWithTransformations);
using (SkiaHelper.GetTempSKPath(out var strokeFillPath))
{
// Get the stroke geometry, after scaling has been applied.
strokePaint.GetFillPath(geometryWithTransformations, strokeFillPath);

stroke.UpdatePaint(fillPaint, strokeGeometry.Bounds);
stroke.UpdatePaint(fillPaint, strokeFillPath.Bounds);

session.Canvas.DrawPath(strokeGeometry, fillPaint);
session.Canvas.DrawPath(strokeFillPath, fillPaint);
}
}
}
}

private SKPaint TryCreateAndClearStrokePaint(SKColorFilter? colorFilter)
=> TryCreateAndClearPaint(ref _strokePaint, true, colorFilter, CompositionConfiguration.UseBrushAntialiasing);
private DisposableStruct<SKPaint> GetTempStrokePaint(SKColorFilter? colorFilter, out SKPaint paint)
=> GetTempPaint(out paint, true, colorFilter, CompositionConfiguration.UseBrushAntialiasing);

private SKPaint TryCreateAndClearFillPaint(SKColorFilter? colorFilter)
=> TryCreateAndClearPaint(ref _fillPaint, false, colorFilter, CompositionConfiguration.UseBrushAntialiasing);
private DisposableStruct<SKPaint> GetTempFillPaint(SKColorFilter? colorFilter, out SKPaint paint)
=> GetTempPaint(out paint, false, colorFilter, CompositionConfiguration.UseBrushAntialiasing);

private static SKPaint TryCreateAndClearPaint(ref SKPaint? paint, bool isStroke, SKColorFilter? colorFilter, bool isHighQuality = false)
private static DisposableStruct<SKPaint> GetTempPaint(out SKPaint paint, bool isStroke, SKColorFilter? colorFilter, bool isHighQuality = false)
{
if (paint == null)
{
// Initialization
paint = new SKPaint();
paint.IsStroke = isStroke;
paint.IsAntialias = true;
paint.IsAutohinted = true;
var disposable = SkiaHelper.GetTempSKPaint(out paint);
paint.IsAntialias = true;
paint.ColorFilter = colorFilter;

if (isHighQuality)
{
paint.FilterQuality = SKFilterQuality.High;
}
}
else
{
// Cleanup
// - Brushes can change, we cant leave color and shader garbage
// from last rendering around for the next pass.
paint.Color = SKColors.White; // Transparent color wouldn't draw anything
if (paint.Shader is { } shader)
{
shader.Dispose();
paint.Shader = null;
}
paint.IsStroke = isStroke;

if (paint.PathEffect is { } pathEffect)
{
pathEffect.Dispose();
paint.PathEffect = null;
}
}
// uno-specific defaults
paint.Color = SKColors.White; // Transparent color wouldn't draw anything
paint.IsAutohinted = true;
// paint.IsAntialias = true; // IMPORTANT: don't set this to true by default. It breaks canvas clipping on Linux for some reason.
ramezgerges marked this conversation as resolved.
Show resolved Hide resolved

paint.ColorFilter = colorFilter;
if (isHighQuality)
{
paint.FilterQuality = SKFilterQuality.High;
}

return paint;
return disposable;
}

private protected override void OnPropertyChangedCore(string? propertyName, bool isSubPropertyChange)
Expand Down Expand Up @@ -175,14 +160,20 @@ internal override bool HitTest(Point point)
return true;
}

if (StrokeBrush is { } stroke && StrokeThickness > 0)
if (StrokeBrush is { } && StrokeThickness > 0)
{
var strokePaint = TryCreateAndClearStrokePaint(null);
strokePaint.StrokeWidth = StrokeThickness;
var strokeGeometry = strokePaint.GetFillPath(geometryWithTransformations);
if (strokeGeometry.Contains((float)point.X, (float)point.Y))
using (GetTempStrokePaint(null, out var strokePaint))
{
return true;
strokePaint.StrokeWidth = StrokeThickness;

using (SkiaHelper.GetTempSKPath(out var hitTestStrokeFillPath))
{
strokePaint.GetFillPath(geometryWithTransformations, hitTestStrokeFillPath);
if (hitTestStrokeFillPath.Contains((float)point.X, (float)point.Y))
{
return true;
}
}
}
}
}
Expand Down
22 changes: 13 additions & 9 deletions src/Uno.UI.Composition/Composition/ShapeVisual.skia.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable

using System;
using System.Numerics;
using Windows.Foundation;
using SkiaSharp;
Expand All @@ -12,9 +13,12 @@ public partial class ShapeVisual
private protected override void ApplyPrePaintingClipping(in SKCanvas canvas)
{
base.ApplyPrePaintingClipping(in canvas);
if (GetViewBoxPathInElementCoordinateSpace() is { } path)
using (SkiaHelper.GetTempSKPath(out var prePaintingClipPath))
{
canvas.ClipPath(path, antialias: true);
if (GetViewBoxPathInElementCoordinateSpace(prePaintingClipPath))
{
canvas.ClipPath(prePaintingClipPath, antialias: true);
}
}
}

Expand All @@ -32,28 +36,28 @@ internal override void Paint(in PaintingSession session)
base.Paint(in session);
}

internal SKPath? GetViewBoxPathInElementCoordinateSpace()
/// <returns>true if a ViewBox exists</returns>
internal bool GetViewBoxPathInElementCoordinateSpace(SKPath dst)
{
if (ViewBox is not { } viewBox)
{
return null;
return false;
}

var shape = new SKPath();
dst.Rewind();
var clipRect = new SKRect(viewBox.Offset.X, viewBox.Offset.Y, viewBox.Offset.X + viewBox.Size.X, viewBox.Offset.Y + viewBox.Size.Y);
shape.AddRect(clipRect);
dst.AddRect(clipRect);
if (viewBox.IsAncestorClip)
{
Matrix4x4.Invert(TotalMatrix, out var totalMatrixInverted);
var childToParentTransform = Parent!.TotalMatrix * totalMatrixInverted;
if (!childToParentTransform.IsIdentity)
{

shape.Transform(childToParentTransform.ToSKMatrix());
dst.Transform(childToParentTransform.ToSKMatrix());
}
}

return shape;
return true;
}

/// <remarks>This does NOT take the clipping into account.</remarks>
Expand Down
33 changes: 33 additions & 0 deletions src/Uno.UI.Composition/Composition/SkiaHelper.skia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#nullable enable

using SkiaSharp;
using Microsoft.CodeAnalysis.PooledObjects;
using Uno.Disposables;
using SKPaint = SkiaSharp.SKPaint;

namespace Microsoft.UI.Composition
{
internal static class SkiaHelper
{
private static readonly ObjectPool<SKPaint> _paintPool = new(() => new SKPaint(), 8);
private static readonly ObjectPool<SKPath> _pathPool = new(() => new SKPath(), 8);

public static DisposableStruct<SKPath> GetTempSKPath(out SKPath path)
{
path = _pathPool.Allocate();
// Note the difference between Rewind and Reset
// https://api.skia.org/classSkPath.html#a8dc858ee4c95a59b3dd4bdd3f7b85fdc : "Use rewind() instead of reset() if SkPath storage will be reused and performance is critical."
path.Rewind();
return new DisposableStruct<SKPath>(p => _pathPool.Free(p), path);
}

public static DisposableStruct<SKPaint> GetTempSKPaint(out SKPaint paint)
{
paint = _paintPool.Allocate();
// https://api.skia.org/classSkPaint.html#a6c7118c97a0e8819d75aa757afbc4c49
// "This is equivalent to replacing SkPaint with the result of SkPaint()."
paint.Reset();
return new DisposableStruct<SKPaint>(p => _paintPool.Free(p), paint);
}
}
}
10 changes: 6 additions & 4 deletions src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,13 @@ partial void SetupInlines()
{
var canvas = t.canvas;
var rect = t.rect;
canvas.DrawRect(new SKRect((float)rect.Left, (float)rect.Top, (float)rect.Right, (float)rect.Bottom), new SKPaint

using (SkiaHelper.GetTempSKPaint(out var paint))
{
Color = SelectionHighlightColor.Color.ToSKColor(),
Style = SKPaintStyle.Fill
});
paint.Color = SelectionHighlightColor.Color.ToSKColor();
paint.Style = SKPaintStyle.Fill;
canvas.DrawRect(new SKRect((float)rect.Left, (float)rect.Top, (float)rect.Right, (float)rect.Bottom), paint);
}
};

_inlines.RenderSelection = IsTextSelectionEnabled;
Expand Down
12 changes: 7 additions & 5 deletions src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,13 @@ private void UpdateTextBoxView()
var caretRect = args.rect;
var compositor = _visual.Compositor;
var brush = DefaultBrushes.TextForegroundBrush.GetOrCreateCompositionBrush(compositor);
var caretPaint = new SKPaint(); // we create a new caret everytime to dodge the complications that occur when trying to "reset" an SKPaint.
brush.UpdatePaint(caretPaint, caretRect.ToSKRect());
args.canvas.DrawRect(
new SKRect((float)caretRect.Left, (float)caretRect.Top, (float)caretRect.Right,
(float)caretRect.Bottom), caretPaint);
using (SkiaHelper.GetTempSKPaint(out var caretPaint))
{
brush.UpdatePaint(caretPaint, caretRect.ToSKRect());
args.canvas.DrawRect(
new SKRect((float)caretRect.Left, (float)caretRect.Top, (float)caretRect.Right,
(float)caretRect.Bottom), caretPaint);
}
}

if ((CaretMode == CaretDisplayMode.CaretWithThumbsOnlyEndShowing && args.endCaret) ||
Expand Down
16 changes: 0 additions & 16 deletions src/Uno.UI/UI/Xaml/Documents/Inline.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,6 @@ namespace Microsoft.UI.Xaml.Documents
partial class Inline
{
private FontDetails? _fontInfo;
private SKPaint? _paint;

internal SKPaint Paint
{
get
{
var paint = _paint ??= new SKPaint()
{
TextEncoding = SKTextEncoding.Utf16,
IsStroke = false,
IsAntialias = true,
};

return paint;
}
}

internal FontDetails FontInfo
{
Expand Down
Loading
Loading