Skip to content

Inverse blurred rounded rects: classic + fix sparse strips - #1718

Open
nicoburns wants to merge 12 commits into
linebender:mainfrom
nicoburns:inverse-blur-classic
Open

nicoburns wants to merge 12 commits into
linebender:mainfrom
nicoburns:inverse-blur-classic

Conversation

@nicoburns

@nicoburns nicoburns commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Followup to #1715 for Vello Classic. Implements inverse blurred rounded rects for Vello Classic. Also ports the tests from the sparse strips test suite. Fixes #1374. Only 41 lines of non-test code. The main thing I'm unsure about is whether this is a sensible representation of the invert bit in the encoding / GPU data layout. I have verified that this produces identical output as the sparse strips implementation when rendering my test shadow scene in Blitz (pictured below)

Changes:

  • Add invert parameter to Vello Classic's blurred_rounded_rect_in
  • Add blurred_rounded_rect_in to Hybrid/CPU
  • Remove invert parameter from non-_in blurred_rounded_rect function in Hybrid/CPU
  • Add a bunch of extra tests

Generated with Opus 4.8 High / Opus 5 Medium with manual review.

Screenshot 2026-07-02 at 00 13 44

@nicoburns nicoburns added enhancement New feature or request C-research Applies to the classic `vello_research` crate labels Jun 26, 2026
@nicoburns
nicoburns force-pushed the inverse-blur-classic branch 2 times, most recently from 3d69137 to 48e029d Compare July 1, 2026 23:13
@nicoburns

Copy link
Copy Markdown
Contributor Author

I've updated this encode the invert bit into the the unused sign bit from the std_dev parameter rather than adding a whole new 32bit parameter

@nicoburns
nicoburns requested a review from raphlinus July 4, 2026 00:51

@DJMcNab DJMcNab left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the actual wiring is pretty much fine, except for the single-rect constructor. I'm not really happy with how this is documented. I am also aware that the non-inverse rounded rectangle is also poorly documented, but I feel like this is misleadingly documented rather than just under-documented.

I do agree that this is a valuable feature, and hope this feedback makes sense.

Comment thread vello/src/scene.rs Outdated
let shape: Rect = rect.inflate(kernel_size, kernel_size);
self.draw_blurred_rounded_rect_in(&shape, transform, rect, brush, radius, std_dev);
let inflated_rect = if invert {
rect.inflate(rect.origin().x.abs(), rect.origin().y.abs())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a wild thing to drop without an explanatory comment... it seems to want to try to expand to the top-left corner (but not the bottom left corner?), and only if you have no transform?

In theory, you want this to have infinite extents, which I do know aren't really practical in current Vello Classic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DJMcNab See discussion in #1721 (same API for sparse strips). The key thing that this achieves is to offset the position of the paint relative to the position of the clip. This directly corresponds to the offset (x/y) parameters of a CSS box-shadow (https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/box-shadow).

We get away without this for "outset" box-shadows because we're painting the area "outside" the clip (i.e. the hole of a rounded-rect-donut clip) anyway (Blitz is applying it's own clip to clip this out which does account for the "offset").

Because "inset" box shadows are painted "inside the box" we are able to specify a tighter clip region, but this means that we have to account for the offset.


Agree that this needs more thought put into the API though.

Comment thread research/vello_research/src/scene.rs Outdated
Comment on lines +298 to +301
///
/// When `invert` is `true`, the inverse (`1 - alpha`) of the blur coverage is painted: the
/// brush is fully opaque outside the blurred rounded rectangle and fades to transparent inside
/// it. This can be used to implement inset box shadows.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "outside" here mean 'approximately 2.5 std deviations outside', or something else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The are effectively two rounded rects, offset from each other by a user-specified (x/y) offset (currently encoded by the rect parameter's origin):

  • A clip rect
  • A paint rect

Then:

  • The area which is outside the "clip rect" doesn't get painted at all (duh)
  • The area in the intersection of the two rects gets the inverted blurred_rounded_rect paint
  • The area which is inside the "clip rect" but outside the "paint rect" get a solid fill (this actually falls out of the definition of the inverted blurred_round_rect paint and isn't special cased in the implementation)

Probably this comment could be reworded.


You may find it helpful to play around with https://box-shadow.dev (with invert enabled) to get an intuition of the intended effect. In particular, consider the case where the blur radius is 0 (and potentially the spread too), but there is an offset.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way of encoding the parameters feels:

  • entirely unjustified
  • kind of unhinged
  • extremely unintuitive

It's an impressive combination of results!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is fair! It does have one redeeming quality, which is that it's non-API-breaking. But yeah, I agree that we ought to change this.

Comment thread vello/src/scene.rs Outdated
brush: Color,
radius: f64,
std_dev: f64,
invert: bool,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the cleanest option is to actually just not define the 'invert' parameter to this method, and force you to define the extents yourself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm in agreement that the user probably ought to define extents (I think this amounts to allowing the user to specify a custom impl Shape?), but I think we still need the invert parameter to invert the actual paint (apply 1 - alpha in the shader).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes... That's the null comment from my suggestion?

I'm saying you should only have the invert parameter on the blurred_rounded_rect_in method, and leave it completely off the not-in one.

Comment thread research/vello_encoding/src/encoding.rs Outdated
) {
// The `invert` flag is packed into the sign bit of `std_dev`, which is otherwise
// always non-negative.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'always'? We should probably at least have this as a validity requirement in the underlying docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean internal docs? If so, that makes sense.

In terms of public API, we could certainly document it, but we probably also ought to clamp the value to zero in case it is negative rather exposing weird behaviour to a user-facing API.

Comment thread research/vello_shaders/shader/fine.wgsl Outdated
Comment on lines +753 to +754
// The invert flag is packed into the sign bit of `std_dev`, which is otherwise
// always non-negative.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 'always non-negative' is the kind of comment which raises more questions than it answers.

@nicoburns
nicoburns force-pushed the inverse-blur-classic branch from 501c3cc to 22060b0 Compare July 27, 2026 06:57
@nicoburns nicoburns changed the title classic: Implement inverse blurred rounded rects Inverse blurred rounded rects: classic + fix sparse strips Jul 27, 2026
nicoburns and others added 8 commits September 30, 2026 18:37
Signed-off-by: Nico Burns <nico@nicoburns.com>
Signed-off-by: Nico Burns <nico@nicoburns.com>
…rage.

Adds the non-inverse, clipped-to-shape, and even-odd snapshot cases from
vello_sparse_tests so that the classic vello renderer is covered by the
same blurred rounded rectangle scenarios as the sparse strip renderers.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Classic tests: the test support crate is now `vello_research_tests`.
- Changelogs: `invert` on `fill_blurred_rounded_rect` shipped in 0.1.0, so
  leave that entry alone and record the new `fill_blurred_rounded_rect_in`
  method and the removal of the `invert` parameter under Unreleased.
The existing snapshot still passes; the regenerated one only differed by
<= 1/255 in a few hundred pixels.
- Make the even-odd test actually exercise `draw_blurred_rounded_rect_in`.
  Classic always fills the shape with the non-zero rule, so use two circles
  with opposite winding directions to get the same coverage.
- Use an explicit full-canvas rect as the shape in the invert property test.
- Scope the "shape should not extend more than ~2.5 std_dev beyond rect"
  guidance to the non-inverted case, and describe what is painted outside
  of rect when invert is true.
- Document that negative std_dev values are treated as zero.
- Explain the sign bit encoding of the invert flag (including the -0.0 case)
  rather than asserting that std_dev is "always non-negative".
- Fix "perceptably" typo.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-research Applies to the classic `vello_research` crate enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider support for "inverse" blurred rounded rectangles

2 participants