Skip to content

Bind SKPaint.GetFastBounds#4271

Merged
mattleibow merged 1 commit into
mainfrom
mattleibow-bind-maskfilter-computefastbounds
Jun 29, 2026
Merged

Bind SKPaint.GetFastBounds#4271
mattleibow merged 1 commit into
mainfrom
mattleibow-bind-maskfilter-computefastbounds

Conversation

@mattleibow

@mattleibow mattleibow commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Binds the public SkPaint::computeFastBounds / SkPaint::canComputeFastBounds pair as a single Try-style method:

  • bool SKPaint.GetFastBounds(SKRect bounds, out SKRect fastBounds)

GetFastBounds returns the conservative device-space rectangle a paint can draw into for a given source rect, and composes every effect on the paint — path effect, stroke inflation, mask filter and image filter — so the result accounts for blurs, thick/mitered strokes, dashes, drop shadows, etc. Paired with the already-bound SKCanvas.QuickReject, this completes the quick-reject culling pattern that Skia documents in its own headers:

if (!path.isInverseFillType() && paint.canComputeFastBounds()) {
    SkRect storage;
    if (canvas->quickReject(paint.computeFastBounds(path.getBounds(), &storage)))
        return; // do not draw
}

In C# that whole guard becomes one call:

if (paint.GetFastBounds(path.Bounds, out var fast) && canvas.QuickReject(fast))
    return; // do not draw

Companion C API PR: mono/skia#271.

API shape: one method, not two

Skia's own contract is that canComputeFastBounds() is a guard that must be checked before calling computeFastBounds() — the latter is unreliable for paints whose image filter or path effect cannot report bounds (the header literally says "Only call this if canComputeFastBounds() returned true"). That maps exactly onto the .NET Try-pattern, so the two are collapsed into a single GetFastBounds(..., out ...) → bool:

  • The guard is folded into the return value, so the Skia contract cannot be misused (a false short-circuits before QuickReject).
  • It matches the existing SkiaSharp out-bool bounds family — SKPath.GetBounds(out), SKPath.GetTightBounds(out), SKCanvas.GetLocalClipBounds(out), SKCanvas.GetDeviceClipBounds(out) — none of which expose a separate Can… property (there is no CanGetTightBounds).
  • Compute in SkiaSharp is reserved for value-returning mirrors of Skia computeXxx() (SKPath.ComputeTightBounds, SKPixmap.ComputeIsOpaque); since this member returns a bool, GetFastBounds is the consistent name.

GetFastBounds returns false (and SKRect.Empty) when the paint cannot compute fast bounds. The two underlying C exports remain 1:1 with Skia for any future direct use.

Why the public SKPaint API and not SKMaskFilter?

This started as a binding of SkMaskFilter::computeFastBounds, but that method is private. The investigation below explains what happened and why the public SkPaint entry point is the correct thing to bind.

History (traced in the Skia git log)

When Commit What
2012-01-30 9efd9a048a computeFastBounds added to the public SkMaskFilter — explicitly "to perform quick-rejects when drawing objects with shadows (esp. text)."
2018-01-23 80747ef591 (Mike Reed) "move the guts of SkMaskFilter.h into SkMaskFilterBase.h" — made it private. Empty bug field; pure encapsulation. Part of Skia's sweeping move to make every effect type an opaque public handle (factory + serialization only), pushing all implementation methods (filterMask, computeFastBounds, getFormat, …) into internal *Base classes — same as SkShaderBase, SkColorFilterBase, SkPathEffectBase.

So SkMaskFilter::computeFastBounds has been a deliberate internal implementation detail for ~8 years. Binding it would force our C shim to #include "src/core/SkMaskFilterBase.h" and call as_MFB() — exactly the kind of private coupling that broke in 2018 and can break again on any Skia bump.

What about the (to be made private) note on SkPaint::computeFastBounds?

SkPaint::computeFastBounds carries a (to be made private) doc annotation, added 2018-01-03 (2823f9f06c1, Cary Clark) as a bulk note across several SkPaint methods. Eight years later it is still public, with no churn toward privatizing it (no commits in that direction since 2023). It remains the documented, canonical quick-reject API, and SkiaSharp already wraps a number of APIs with similar long-standing soft-deprecation notes.

Notably, SkImageFilter::computeFastBounds stayed public the whole time — Skia kept the image-filter bounds public but routed mask-filter bounds through SkPaint, reinforcing that SkPaint is the intended entry point.

Community signal (groups.google.com/g/skia-discuss)

Real threads on the Skia discussion group show this is a recurring user need, and that computeFastBounds is the recommended answer:

  • "How to get the path bound including border?" (EFbFlfJjNxE, 2024-01-28) — a user asks for an API to get the visual bound including shadow/blur/non-zero border, noting it is unreliable to compute manually because it depends on Skia's implementation. The reply recommends computeFastBounds().
  • "Custom ImageFilter and affected area bounds" (HHT3w0Kmm6Q) — a user whose content is being culled references SkBlurImageFilter overriding computeFastBounds and wants to expand the affected bounds.

Conclusion

Bind the public SkPaint::computeFastBounds / canComputeFastBounds. It is genuinely public, idiomatic, stable, composes the mask-filter bounds internally (so it subsumes the original blur use case), and — as a single GetFastBounds(out) → bool — completes the quick-reject culling trio with SKCanvas.QuickReject. The // TODO: computeFastBounds on SKMaskFilter is intentionally left in place since that method remains private upstream.

Changes

  • C API (Add sk_paint_compute_fast_bounds C API skia#271): sk_paint_compute_fast_bounds, sk_paint_can_compute_fast_bounds.
  • Bindings: regenerated SkiaApi.generated.cs.
  • Wrapper: SKPaint.GetFastBounds(SKRect, out SKRect) → bool.
  • Tests (SKPaintTest): blur outsets by 3×sigma; stroke (round join/cap) outsets by strokeWidth/2; stroke+blur composition; fill with no effects returns source unchanged; source rect not mutated; returns true for simple paints; managed/native parity via direct SkiaApi call.
  • Sample: PaintFastBoundsSample (Gallery) — visualizes the composed fast bounds with sigma/stroke/blur-style/toggle controls.
  • Benchmark: PaintFastBoundsBenchmark — quick-reject culling vs. unconditional draw, plus isolated call cost.

Testing

  • Native rebuilt from source (macOS arm64); both sk_paint_*_fast_bounds symbols exported.
  • SKPaintTest (fast-bounds + paint): 13 passed, 0 failed (1 unrelated pre-existing skip).
  • Full managed suite previously: 5743 passed, 0 failed, 13 pre-existing skips.
  • Benchmark and Gallery shared projects build clean.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Try the packages from this PR

Warning

Do not run these scripts without first reviewing the code in this PR.

Step 1 — Download the packages

bash / macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.sh | bash -s -- 4271

PowerShell / Windows:

iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4271"

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-4271/packages --name skiasharp-pr-4271
More options
Option Description
--successful-only / -SuccessfulOnly Only use successful builds
--force / -Force Overwrite previously downloaded packages
--list / -List List available artifacts without downloading
--build-id ID / -BuildId ID Download from a specific build

Or download manually from Azure Pipelines — look for the nuget artifact on the build for this PR.

Remove the source when you're done:

dotnet nuget remove source skiasharp-pr-4271

@github-actions

Copy link
Copy Markdown
Contributor

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Site
🔗 View Staging Docs
🔗 View Staging Gallery (Blazor)
🔗 View Staging Gallery (Uno Platform)
🔗 View Staging SkiaFiddle

This preview will be updated automatically when you push new commits to this PR.


This comment is automatically updated by the documentation staging workflow.

@mattleibow mattleibow force-pushed the mattleibow-bind-maskfilter-computefastbounds branch from 29fbadd to 4a1805f Compare June 28, 2026 11:13
@mattleibow mattleibow changed the title Bind SKMaskFilter.ComputeFastBounds Bind SKPaint.ComputeFastBounds and SKPaint.CanComputeFastBounds Jun 28, 2026
mattleibow added a commit to mono/skia that referenced this pull request Jun 29, 2026
Add sk_paint_compute_fast_bounds C API (#271)

Context: consumer PR mono/SkiaSharp#4271

Expose the public SkPaint quick-reject bounds API through the C shim so
SkiaSharp can ask a paint for the conservative device-space rect a draw
would touch:

  bool sk_paint_can_compute_fast_bounds(const sk_paint_t*);
  void sk_paint_compute_fast_bounds(const sk_paint_t*, const sk_rect_t* orig,
                                    sk_rect_t* storage);

sk_paint_compute_fast_bounds composes every effect on the paint - path
effect, stroke radius, mask filter and image filter - so the result accounts
for blurs, thick/mitered strokes, dashes and drop shadows. It pairs with the
existing sk_canvas_quick_reject to complete the quick-reject culling pattern.

This deliberately wraps the public SkPaint::computeFastBounds rather than
SkMaskFilter::computeFastBounds. The mask-filter method was public from 2012
(9efd9a0) but was moved to the internal SkMaskFilterBase in 2018
(80747ef, "move the guts of SkMaskFilter.h into SkMaskFilterBase.h") as
part of making all effect types opaque handles, and has been a private
implementation detail ever since. Binding it would force this shim to include
src/core/SkMaskFilterBase.h and call as_MFB() - fragile private coupling.
SkPaint::computeFastBounds calls the mask-filter bounds internally via
doComputeFastBounds, so it is a strict superset and the canonical entry point.

Implementation note: SkPaint::computeFastBounds returns a const SkRect& that
is either orig (the ultra-fast fill-with-no-effects case, where storage is
left untouched) or *storage. The shim assigns the returned reference into
*storage so callers always read the final bounds from a single out-param;
self-assignment in the effects case is safe for the trivially-copyable SkRect.
Uses only the already-included public SkPaint.h - no private headers.

Co-authored-by: Matthew Leibowitz <mattleibow@live.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow force-pushed the mattleibow-bind-maskfilter-computefastbounds branch from 4a1805f to 6661f95 Compare June 29, 2026 15:16
Expose the public SkPaint::computeFastBounds / SkPaint::canComputeFastBounds
pair as a single SKPaint.GetFastBounds(SKRect bounds, out SKRect fastBounds)
method. It returns the conservative device-space bounds a paint can draw into
for a given source rect, composing the path effect, stroke radius, mask filter
and image filter. Together with the already-bound SKCanvas.QuickReject this
completes the documented quick-reject culling pattern.

Skia's contract is that canComputeFastBounds() is a guard that must be checked
before calling computeFastBounds(); the latter is unreliable for paints whose
image filter or path effect cannot report bounds. Collapsing the two into one
Try-style method folds that guard into the bool return value, so the API cannot
be misused and matches the existing SKPath.GetBounds / SKPath.GetTightBounds and
SKCanvas.GetLocalClipBounds out-bool family (note there is likewise no
CanGetTightBounds). GetFastBounds returns false (and SKRect.Empty) when the
paint cannot compute fast bounds.

This binds the public SkPaint API rather than the private
SkMaskFilterBase::computeFastBounds, which has been an internal implementation
detail since 2018. SkPaint::computeFastBounds composes the mask filter bounds
internally, so it is a strict superset of the per-mask-filter method.

- C API: sk_paint_compute_fast_bounds, sk_paint_can_compute_fast_bounds
- Regenerated SkiaApi.generated.cs
- Tests in SKPaintTest (blur 3x sigma, stroke half-width, composition,
  fill no-op, native parity, returns-true for simple paints)
- Gallery sample PaintFastBoundsSample
- Benchmark PaintFastBoundsBenchmark (quick-reject culling)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow force-pushed the mattleibow-bind-maskfilter-computefastbounds branch from 6661f95 to fd206b0 Compare June 29, 2026 15:56
@mattleibow mattleibow changed the title Bind SKPaint.ComputeFastBounds and SKPaint.CanComputeFastBounds Bind SKPaint.GetFastBounds Jun 29, 2026
@mattleibow mattleibow merged commit e19ecc5 into main Jun 29, 2026
6 of 7 checks passed
@mattleibow mattleibow deleted the mattleibow-bind-maskfilter-computefastbounds branch June 29, 2026 16:41
@mattleibow mattleibow added this to the 4.150.0-rc.1 milestone Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant