Skip to content
Merged
Show file tree
Hide file tree
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
111 changes: 111 additions & 0 deletions benchmarks/SkiaSharp.Benchmarks/Benchmarks/PaintFastBoundsBenchmark.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System;
using BenchmarkDotNet.Attributes;

namespace SkiaSharp.Benchmarks;

// SKPaint.GetFastBounds exists to power quick-reject culling: before issuing an expensive
// draw, a renderer can ask the paint for the conservative device-space rect the draw would touch
// - composing the path effect, stroke, mask filter and image filter - and skip the draw entirely
// when that rect misses the clip/viewport.
//
// This benchmark models a frame that submits many blurred rects scattered across a virtual space
// four times larger than the viewport, so roughly three quarters of the items land off-screen and
// are cullable. It compares drawing every item unconditionally against using GetFastBounds to
// cull off-screen items first, plus an isolated measurement of the raw GetFastBounds call cost
// (the P/Invoke crossing plus the native geometry math).
//
// Runs on the project's current TFM with the default job, matching SurfaceCanvasBenchmark.
[MemoryDiagnoser]
public class PaintFastBoundsBenchmark
{
private const int Width = 256;
private const int Height = 256;

// Frames rendered per invocation. 60 ~= one second at 60 FPS.
[Params(1, 60)]
public int Frames { get; set; }

// Draw items submitted per frame.
[Params(500)]
public int ItemsPerFrame { get; set; }

private SKSurface surface;
private SKCanvas canvas;
private SKPaint paint;
private SKMaskFilter blur;
private SKRect clip;
private SKRect[] items;

[GlobalSetup]
public void GlobalSetup()
{
surface = SKSurface.Create(new SKImageInfo(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul));
canvas = surface.Canvas;
blur = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 6f);
paint = new SKPaint { IsAntialias = true, Color = SKColors.SteelBlue, MaskFilter = blur };

// The on-screen viewport that draws must intersect to be visible.
clip = SKRect.Create(0, 0, Width, Height);

// Scatter items across a 4x larger virtual space so ~75% land off-screen and are
// cullable. A fixed seed keeps the scene identical across runs.
items = new SKRect[ItemsPerFrame];
var rnd = new Random(42);
for (var i = 0; i < items.Length; i++)
{
var x = rnd.Next(-Width, Width * 3);
var y = rnd.Next(-Height, Height * 3);
items[i] = SKRect.Create(x, y, 24, 24);
}
}

[GlobalCleanup]
public void GlobalCleanup()
{
paint?.Dispose();
blur?.Dispose();
surface?.Dispose();
}

// Baseline: draw every blurred item, letting the raster backend reject off-screen draws.
[Benchmark(Baseline = true)]
public void DrawAll()
{
for (var f = 0; f < Frames; f++)
{
foreach (var item in items)
canvas.DrawRect(item, paint);
}
}

// Optimized: use GetFastBounds to skip items whose drawn bounds miss the viewport.
[Benchmark]
public void DrawWithFastBoundsCulling()
{
for (var f = 0; f < Frames; f++)
{
foreach (var item in items)
{
paint.GetFastBounds(item, out var bounds);
if (bounds.IntersectsWith(clip))
canvas.DrawRect(item, paint);
}
}
}

// Isolates the raw GetFastBounds cost (P/Invoke crossing plus native geometry).
[Benchmark]
public float GetFastBoundsOnly()
{
var sum = 0f;
for (var f = 0; f < Frames; f++)
{
foreach (var item in items)
{
paint.GetFastBounds(item, out var bounds);
sum += bounds.Width;
}
}
return sum;
}
}
17 changes: 17 additions & 0 deletions binding/SkiaSharp/SKPaint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,23 @@ private bool GetFillPath (SKPath src, SKPathBuilder dst, SKRect* cullRect, SKMat
return result;
}

// GetFastBounds

public bool GetFastBounds (SKRect bounds, out SKRect fastBounds)
{
if (!SkiaApi.sk_paint_can_compute_fast_bounds (Handle)) {
GC.KeepAlive (this);
fastBounds = SKRect.Empty;
return false;
}

fixed (SKRect* storage = &fastBounds) {
SkiaApi.sk_paint_compute_fast_bounds (Handle, &bounds, storage);
}
GC.KeepAlive (this);
return true;
}

// CountGlyphs

[Obsolete ($"Use {nameof (SKFont)}.{nameof (SKFont.CountGlyphs)}() instead.", error: true)]
Expand Down
41 changes: 41 additions & 0 deletions binding/SkiaSharp/SkiaApi.generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8514,6 +8514,28 @@ internal static bool sk_matrix_try_invert (SKMatrix* matrix, SKMatrix* result) =

#region sk_paint.h

// bool sk_paint_can_compute_fast_bounds(const sk_paint_t* cpaint)
#if !USE_DELEGATES
#if USE_LIBRARY_IMPORT
[LibraryImport (SKIA)]
[return: MarshalAs (UnmanagedType.I1)]
internal static partial bool sk_paint_can_compute_fast_bounds (sk_paint_t cpaint);
#else // !USE_LIBRARY_IMPORT
[DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)]
[return: MarshalAs (UnmanagedType.I1)]
internal static extern bool sk_paint_can_compute_fast_bounds (sk_paint_t cpaint);
#endif
#else
private partial class Delegates {
[UnmanagedFunctionPointer (CallingConvention.Cdecl)]
[return: MarshalAs (UnmanagedType.I1)]
internal delegate bool sk_paint_can_compute_fast_bounds (sk_paint_t cpaint);
}
private static Delegates.sk_paint_can_compute_fast_bounds sk_paint_can_compute_fast_bounds_delegate;
internal static bool sk_paint_can_compute_fast_bounds (sk_paint_t cpaint) =>
(sk_paint_can_compute_fast_bounds_delegate ??= GetSymbol<Delegates.sk_paint_can_compute_fast_bounds> ("sk_paint_can_compute_fast_bounds")).Invoke (cpaint);
#endif

// sk_paint_t* sk_paint_clone(sk_paint_t*)
#if !USE_DELEGATES
#if USE_LIBRARY_IMPORT
Expand All @@ -8533,6 +8555,25 @@ internal static sk_paint_t sk_paint_clone (sk_paint_t param0) =>
(sk_paint_clone_delegate ??= GetSymbol<Delegates.sk_paint_clone> ("sk_paint_clone")).Invoke (param0);
#endif

// void sk_paint_compute_fast_bounds(const sk_paint_t* cpaint, const sk_rect_t* orig, sk_rect_t* storage)
#if !USE_DELEGATES
#if USE_LIBRARY_IMPORT
[LibraryImport (SKIA)]
internal static partial void sk_paint_compute_fast_bounds (sk_paint_t cpaint, SKRect* orig, SKRect* storage);
#else // !USE_LIBRARY_IMPORT
[DllImport (SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern void sk_paint_compute_fast_bounds (sk_paint_t cpaint, SKRect* orig, SKRect* storage);
#endif
#else
private partial class Delegates {
[UnmanagedFunctionPointer (CallingConvention.Cdecl)]
internal delegate void sk_paint_compute_fast_bounds (sk_paint_t cpaint, SKRect* orig, SKRect* storage);
}
private static Delegates.sk_paint_compute_fast_bounds sk_paint_compute_fast_bounds_delegate;
internal static void sk_paint_compute_fast_bounds (sk_paint_t cpaint, SKRect* orig, SKRect* storage) =>
(sk_paint_compute_fast_bounds_delegate ??= GetSymbol<Delegates.sk_paint_compute_fast_bounds> ("sk_paint_compute_fast_bounds")).Invoke (cpaint, orig, storage);
#endif

// void sk_paint_delete(sk_paint_t*)
#if !USE_DELEGATES
#if USE_LIBRARY_IMPORT
Expand Down
2 changes: 1 addition & 1 deletion externals/skia
125 changes: 125 additions & 0 deletions samples/Gallery/Shared/Samples/PaintFastBoundsSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using SkiaSharp;
using SkiaSharpSample.Controls;

namespace SkiaSharpSample.Samples;

public class PaintFastBoundsSample : CanvasSampleBase
{
private static readonly string[] blurStyles = ["Normal", "Solid", "Outer", "Inner"];

private float sigma = 12f;
private float strokeWidth = 6f;
private int blurStyleIndex = 0;
private bool showBounds = true;

public override string Title => "Paint Fast Bounds";

public override DateOnly? DateAdded => new DateOnly(2026, 6, 27);

public override string Category => SampleManager.ImageFilters;

public override string Description =>
"Visualize SKPaint.GetFastBounds. The dashed rectangle is the conservative " +
"device-space region the paint can draw into - the renderer uses it to quick-reject " +
"draws and to size offscreen layers. GetFastBounds composes every effect on the " +
"paint, so the bounds account for both the stroke width and the blur mask filter. " +
"Increase the stroke or blur and watch the bounds grow.";

public override IReadOnlyList<string> ApiTags =>
[
"SKPaint", "SKPaint.GetFastBounds",
"SKMaskFilter.CreateBlur", "SKBlurStyle", "SKCanvas.DrawRect",
];

public override IReadOnlyList<SampleControl> Controls =>
[
new SliderControl("sigma", "Blur Sigma", 0, 40, sigma, 0.5f),
new SliderControl("stroke", "Stroke Width", 0, 30, strokeWidth, 0.5f),
new PickerControl("style", "Blur Style", blurStyles, blurStyleIndex),
new ToggleControl("bounds", "Show Fast Bounds", showBounds),
];

protected override void OnControlChanged(string id, object value)
{
switch (id)
{
case "sigma":
sigma = (float)value;
break;
case "stroke":
strokeWidth = (float)value;
break;
case "style":
blurStyleIndex = (int)value;
break;
case "bounds":
showBounds = (bool)value;
break;
}
}

protected override void OnDrawSample(SKCanvas canvas, int width, int height)
{
canvas.Clear(SKColors.White);

var style = (SKBlurStyle)blurStyleIndex;

// The geometry that will be drawn, centered in the canvas.
var size = Math.Min(width, height) * 0.4f;
var shape = new SKRect(
(width - size) / 2f,
(height - size) / 2f,
(width + size) / 2f,
(height + size) / 2f);

using var blur = SKMaskFilter.CreateBlur(style, sigma);

// A paint that both strokes and blurs the shape.
using var paint = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = strokeWidth,
StrokeJoin = SKStrokeJoin.Round,
StrokeCap = SKStrokeCap.Round,
Color = SKColors.MediumPurple,
MaskFilter = blur,
};

// The conservative bounds the paint can draw into, composing the stroke and the blur.
paint.GetFastBounds(shape, out var fastBounds);

// Draw the stroked, blurred shape.
canvas.DrawRect(shape, paint);

if (showBounds)
{
// The original geometry, for reference.
using var shapeStroke = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = 1,
Color = SKColors.Gray,
};
canvas.DrawRect(shape, shapeStroke);

// The fast bounds returned by GetFastBounds.
using var dash = SKPathEffect.CreateDash([8f, 6f], 0);
using var boundsStroke = new SKPaint
{
IsAntialias = true,
Style = SKPaintStyle.Stroke,
StrokeWidth = 2,
Color = SKColors.OrangeRed,
PathEffect = dash,
};
canvas.DrawRect(fastBounds, boundsStroke);

// Label the bounds size.
using var font = new SKFont { Size = 16 };
using var label = new SKPaint { IsAntialias = true, Color = SKColors.OrangeRed };
canvas.DrawText($"bounds {fastBounds.Width:0} x {fastBounds.Height:0}px", 12, height - 16, SKTextAlign.Left, font, label);
}
}
}
Loading
Loading