Skip to content

Commit

Permalink
perf: reduce SKPaint and SKPath allocations
Browse files Browse the repository at this point in the history
  • Loading branch information
ramezgerges committed Oct 16, 2024
1 parent 39528be commit 9264d5e
Show file tree
Hide file tree
Showing 10 changed files with 77 additions and 115 deletions.
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,11 @@ internal override void UpdatePaint(SKPaint paint, SKRect bounds)

void IOnlineBrush.Paint(in Visual.PaintingSession session, SKRect bounds)
{
_resultPaint ??= new SKPaint() { IsAntialias = true };
var resultPaint = SkiaExtensions.GetTempSKPaint();
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 +48,6 @@ private protected override void DisposeInternal()

_sourcePaint?.Dispose();
_maskPaint?.Dispose();
_resultPaint?.Dispose();
}
}
}
95 changes: 20 additions & 75 deletions src/Uno.UI.Composition/Composition/CompositionSpriteShape.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ 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 +17,7 @@ internal override void Paint(in Visual.PaintingSession session)
{
if (FillBrush is { } fill)
{
var fillPaint = TryCreateAndClearFillPaint(session.Filters.OpacityColorFilter);
var fillPaint = GetTempFillPaint(session.Filters.OpacityColorFilter);

if (Compositor.TryGetEffectiveBackgroundColor(this, out var colorFromTransition))
{
Expand Down Expand Up @@ -52,8 +50,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);
var fillPaint = GetTempFillPaint(session.Filters.OpacityColorFilter);
var strokePaint = GetTempStrokePaint(session.Filters.OpacityColorFilter);

// Set stroke thickness
strokePaint.StrokeWidth = StrokeThickness;
Expand All @@ -78,83 +76,28 @@ 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.

var strokeFillPath = SkiaExtensions.GetTempSKPath();
// Get the stroke geometry, after scaling has been applied.
var strokeGeometry = strokePaint.GetFillPath(geometryWithTransformations);
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 SKPaint GetTempStrokePaint(SKColorFilter? colorFilter)
=> GetTempPaint(true, colorFilter, CompositionConfiguration.UseBrushAntialiasing);

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

// TODO: profile the impact of this optimization and consider removing it
// It's hacky and can break if SkiaSharp exposes new properties that
// are then used and modified in CompositionBrush.UpdatePaint().
private static SKPaint TryCreateAndClearPaint(ref SKPaint? paint, bool isStroke, SKColorFilter? colorFilter, bool isHighQuality = false)
private static SKPaint GetTempPaint(bool isStroke, SKColorFilter? colorFilter, bool isHighQuality = false)
{
if (paint == null)
{
paint = new SKPaint();
}
else
{
// defaults
paint.IsAntialias = false;
paint.BlendMode = SKBlendMode.SrcOver;
paint.FakeBoldText = false;
paint.HintingLevel = SKPaintHinting.Normal;
paint.IsDither = false;
paint.IsEmbeddedBitmapText = false;
paint.IsLinearText = false;
paint.LcdRenderText = false;
paint.StrokeCap = SKStrokeCap.Butt;
paint.StrokeJoin = SKStrokeJoin.Miter;
paint.StrokeMiter = 4;
paint.StrokeWidth = 0;
paint.SubpixelText = false;
paint.TextAlign = SKTextAlign.Left;
paint.TextEncoding = SKTextEncoding.Utf8;
paint.TextScaleX = 1;
paint.TextSize = 12;

// Cleanup
if (paint.Shader is { } shader)
{
shader.Dispose();
paint.Shader = null;
}

if (paint.PathEffect is { } pathEffect)
{
pathEffect.Dispose();
paint.PathEffect = null;
}

if (paint.ImageFilter is { } imageFilter)
{
imageFilter.Dispose();
paint.ImageFilter = null;
}

if (paint.MaskFilter is { } maskFilter)
{
maskFilter.Dispose();
paint.MaskFilter = null;
}

if (paint.Typeface is { } typeface)
{
typeface.Dispose();
paint.Typeface = null;
}
}
var paint = SkiaExtensions.GetTempSKPaint();
paint.IsAntialias = true;
paint.ColorFilter = colorFilter;

paint.IsStroke = isStroke;

Expand Down Expand Up @@ -216,10 +159,12 @@ internal override bool HitTest(Point point)

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

var hitTestStrokeFillPath = SkiaExtensions.GetTempSKPath();
strokePaint.GetFillPath(geometryWithTransformations, hitTestStrokeFillPath);
if (hitTestStrokeFillPath.Contains((float)point.X, (float)point.Y))
{
return true;
}
Expand Down
23 changes: 14 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 @@ -9,12 +10,16 @@ namespace Microsoft.UI.Composition;

public partial class ShapeVisual
{
[ThreadStatic] // safety
private static SKPath? _prePaintingClipPath;

private protected override void ApplyPrePaintingClipping(in SKCanvas canvas)
{
base.ApplyPrePaintingClipping(in canvas);
if (GetViewBoxPathInElementCoordinateSpace() is { } path)
_prePaintingClipPath ??= new SKPath();
if (GetViewBoxPathInElementCoordinateSpace(_prePaintingClipPath))
{
canvas.ClipPath(path, antialias: true);
canvas.ClipPath(_prePaintingClipPath, antialias: true);
}
}

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

internal SKPath? GetViewBoxPathInElementCoordinateSpace()
/// <returns>The same <see cref="dst"/> object.</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
19 changes: 19 additions & 0 deletions src/Uno.UI.Composition/Composition/SkiaExtensions.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ namespace Microsoft.UI.Composition
{
public static class SkiaExtensions
{
[ThreadStatic] private static readonly SKPath _tempPath = new SKPath();
[ThreadStatic] private static readonly SKPaint _tempPaint = new SKPaint();

public static SKRect ToSKRect(this Rect rect)
=> new SKRect((float)rect.Left, (float)rect.Top, (float)rect.Right, (float)rect.Bottom);

Expand Down Expand Up @@ -141,5 +144,21 @@ public static SKBitmap ToSKBitmap(this SKImage image)

return bmp!;
}

public static SKPath GetTempSKPath()
{
// 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."
_tempPath.Rewind();
return _tempPath;
}

public static SKPaint GetTempSKPaint()
{
// https://api.skia.org/classSkPaint.html#a6c7118c97a0e8819d75aa757afbc4c49
// "This is equivalent to replacing SkPaint with the result of SkPaint()."
_tempPaint.Reset();
return _tempPaint;
}
}
}
11 changes: 6 additions & 5 deletions src/Uno.UI/UI/Xaml/Controls/TextBlock/TextBlock.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Uno.UI.Xaml.Core;
using Uno.UI.Xaml.Media;
using Uno.UI.Xaml.Core.Scaling;
using SkiaExtensions = Microsoft.UI.Composition.SkiaExtensions;

#nullable enable

Expand Down Expand Up @@ -188,11 +189,11 @@ 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
{
Color = SelectionHighlightColor.Color.ToSKColor(),
Style = SKPaintStyle.Fill
});

var paint = SkiaExtensions.GetTempSKPaint();
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
2 changes: 1 addition & 1 deletion src/Uno.UI/UI/Xaml/Controls/TextBox/TextBox.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ 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.
var caretPaint = Microsoft.UI.Composition.SkiaExtensions.GetTempSKPaint();
brush.UpdatePaint(caretPaint, caretRect.ToSKRect());
args.canvas.DrawRect(
new SKRect((float)caretRect.Left, (float)caretRect.Top, (float)caretRect.Right,
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
7 changes: 6 additions & 1 deletion src/Uno.UI/UI/Xaml/Documents/InlineCollection.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Windows.Foundation;
using Windows.UI.Text;
using Microsoft.UI.Composition;
using SkiaExtensions = Microsoft.UI.Composition.SkiaExtensions;

namespace Microsoft.UI.Xaml.Documents
{
Expand Down Expand Up @@ -499,7 +500,11 @@ internal void Draw(in Visual.PaintingSession session)
var segment = segmentSpan.Segment;
var inline = segment.Inline;
var fontInfo = segment.FallbackFont ?? inline.FontInfo;
var paint = inline.Paint;

var paint = SkiaExtensions.GetTempSKPaint();
paint.TextEncoding = SKTextEncoding.Utf16;
paint.IsStroke = false;
paint.IsAntialias = true;

if (inline.Foreground is SolidColorBrush scb)
{
Expand Down
1 change: 0 additions & 1 deletion src/Uno.UI/UI/Xaml/Documents/Run.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ private List<Segment> GetSegments()
var fontInfo = FontInfo;
var defaultTypeface = fontInfo.SKFont.Typeface;
var defaultFont = fontInfo.Font;
var paint = Paint;
var fontSize = fontInfo.SKFontSize;

defaultFont.GetScale(out int defaultFontScale, out _);
Expand Down
9 changes: 7 additions & 2 deletions src/Uno.UI/UI/Xaml/Media/VisualTreeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ namespace Microsoft.UI.Xaml.Media
{
public partial class VisualTreeHelper
{
#if __SKIA__
[ThreadStatic] // just in case SearchDownForTopMostElement is called from multiple threads somehow
private static readonly SkiaSharp.SKPath _hitTestViewBoxPath = new();
#endif

[Uno.NotImplemented]
public static void DisconnectChildrenRecursive(UIElement element)
{
Expand Down Expand Up @@ -517,8 +522,8 @@ internal static (UIElement? element, Branch? stale) SearchDownForTopMostElementA

// The maximum region where the current element and its children might draw themselves
// This is expressed in the window (absolute) coordinate space.
var clippingBounds = element.Visual.GetViewBoxPathInElementCoordinateSpace() is { } path
? transformToElement.Transform(path.TightBounds.ToRect())
var clippingBounds = element.Visual.GetViewBoxPathInElementCoordinateSpace(_hitTestViewBoxPath)
? transformToElement.Transform(_hitTestViewBoxPath.TightBounds.ToRect())
: Rect.Infinite;

if (element.Visual.Clip?.GetBounds(element.Visual) is { } clip)
Expand Down

0 comments on commit 9264d5e

Please sign in to comment.