Bind SKPaint.GetFastBounds#4271
Merged
Merged
Conversation
Contributor
📦 Try the packages from this PRWarning 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 -- 4271PowerShell / 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-4271More options
Or download manually from Azure Pipelines — look for the Remove the source when you're done: dotnet nuget remove source skiasharp-pr-4271 |
Contributor
|
📖 Documentation Preview The documentation for this PR has been deployed and is available at: 🔗 View Staging Site This preview will be updated automatically when you push new commits to this PR. This comment is automatically updated by the documentation staging workflow. |
29fbadd to
4a1805f
Compare
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>
4a1805f to
6661f95
Compare
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>
6661f95 to
fd206b0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Binds the public
SkPaint::computeFastBounds/SkPaint::canComputeFastBoundspair as a single Try-style method:bool SKPaint.GetFastBounds(SKRect bounds, out SKRect fastBounds)GetFastBoundsreturns 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-boundSKCanvas.QuickReject, this completes the quick-reject culling pattern that Skia documents in its own headers:In C# that whole guard becomes one call:
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 callingcomputeFastBounds()— the latter is unreliable for paints whose image filter or path effect cannot report bounds (the header literally says "Only call this ifcanComputeFastBounds()returned true"). That maps exactly onto the .NET Try-pattern, so the two are collapsed into a singleGetFastBounds(..., out ...) → bool:falseshort-circuits beforeQuickReject).SKPath.GetBounds(out),SKPath.GetTightBounds(out),SKCanvas.GetLocalClipBounds(out),SKCanvas.GetDeviceClipBounds(out)— none of which expose a separateCan…property (there is noCanGetTightBounds).Computein SkiaSharp is reserved for value-returning mirrors of SkiacomputeXxx()(SKPath.ComputeTightBounds,SKPixmap.ComputeIsOpaque); since this member returns abool,GetFastBoundsis the consistent name.GetFastBoundsreturnsfalse(andSKRect.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
SKPaintAPI and notSKMaskFilter?This started as a binding of
SkMaskFilter::computeFastBounds, but that method is private. The investigation below explains what happened and why the publicSkPaintentry point is the correct thing to bind.History (traced in the Skia git log)
9efd9a048acomputeFastBoundsadded to the publicSkMaskFilter— explicitly "to perform quick-rejects when drawing objects with shadows (esp. text)."80747ef591(Mike Reed)SkMaskFilter.hintoSkMaskFilterBase.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*Baseclasses — same asSkShaderBase,SkColorFilterBase,SkPathEffectBase.So
SkMaskFilter::computeFastBoundshas been a deliberate internal implementation detail for ~8 years. Binding it would force our C shim to#include "src/core/SkMaskFilterBase.h"and callas_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 onSkPaint::computeFastBounds?SkPaint::computeFastBoundscarries a(to be made private)doc annotation, added 2018-01-03 (2823f9f06c1, Cary Clark) as a bulk note across severalSkPaintmethods. 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::computeFastBoundsstayed public the whole time — Skia kept the image-filter bounds public but routed mask-filter bounds throughSkPaint, reinforcing thatSkPaintis 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
computeFastBoundsis the recommended answer:computeFastBounds().SkBlurImageFilteroverridingcomputeFastBoundsand 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 singleGetFastBounds(out) → bool— completes the quick-reject culling trio withSKCanvas.QuickReject. The// TODO: computeFastBoundsonSKMaskFilteris intentionally left in place since that method remains private upstream.Changes
sk_paint_compute_fast_bounds,sk_paint_can_compute_fast_bounds.SkiaApi.generated.cs.SKPaint.GetFastBounds(SKRect, out SKRect) → bool.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; returnstruefor simple paints; managed/native parity via directSkiaApicall.PaintFastBoundsSample(Gallery) — visualizes the composed fast bounds with sigma/stroke/blur-style/toggle controls.PaintFastBoundsBenchmark— quick-reject culling vs. unconditional draw, plus isolated call cost.Testing
sk_paint_*_fast_boundssymbols exported.SKPaintTest(fast-bounds + paint): 13 passed, 0 failed (1 unrelated pre-existing skip).