Skip to content

vello_common: Optimize pixmap unpremultiplication - #1839

Open
LaurenzV wants to merge 2 commits into
mainfrom
laurenz/pixmap_unpremul
Open

vello_common: Optimize pixmap unpremultiplication#1839
LaurenzV wants to merge 2 commits into
mainfrom
laurenz/pixmap_unpremul

Conversation

@LaurenzV

@LaurenzV LaurenzV commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Builds on top of #1838. This PR adds SIMD acceleration for the pixmap unpremultiplication method. Unfortunately, unpremultiplying is fundamentally harder than premutiplying because we need to divide by (a non-constant) integer, which isn't well supported in SIMD. I experimented with various different approaches, such as using SIMD but with f32 instead, but this one seemed to be by far the fastest.

I also tested this in my PDF library and didn't see any regressions.

NEON

Before:

pixmap/take_unpremultiplied
                        time:   [2.2905 ms 2.3092 ms 2.3296 ms]
                        change: [+156.03% +161.53% +166.71%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) high mild
  1 (1.00%) high severe

After:

pixmap/take_unpremultiplied
                        time:   [920.59 µs 944.89 µs 969.06 µs]
                        change: [-61.151% -60.308% -59.391%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild

It's still 2-3 times slower than premultiplying, but at least it's faster than before! The gather for the reciprocals is unfortunately pretty expensive.

AVX2

I can't check currently, unfortunately, but will test this as soon as I have access to my device again.

@LaurenzV
LaurenzV requested a review from grebmeg August 22, 2026 08:37
@LaurenzV
LaurenzV force-pushed the laurenz/pixmap_unpremul branch from 6b48c78 to d8cc41a Compare August 22, 2026 08:41

#[inline(always)]
pub(crate) fn scalar(component: u8, reciprocal: u16) -> u8 {
(u16::from(component) * reciprocal).div_256() as u8

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we handle non-canonical premultiplied input here? A consumer can construct a Pixmap containing [2, 0, 0, 1], causing take_unpremultiplied() to overflow and panic in the scalar debug path. What do you think about either saturating these values or documenting and enforcing the RGB ≤ alpha requirement?

@LaurenzV LaurenzV Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I agree that we should document the invariant on the pixmap constructor! If it's just about the panic, then happy to change it to saturating (or rather wrapping_mul, since this is what's used in fearless_simd). I'm a bit hesitant to try to handle this correctly for the SIMD path, because there aren't dedicated SIMD instructions for saturating multiplication I believe, so we would have to clamp it manually, which would be a bit unfortunate.

So my proposal would be to use wrapping_mul to get rid of the panic, but not make any correctness guarantees for this case? What do you think? Internally, we should always be upholding the invariant already.

})
.collect()
pub fn take_unpremultiplied(mut self) -> Vec<Rgba8> {
unpremultiply_rgba8(bytemuck::cast_slice_mut(&mut self.buf));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we skip unpremultiply_rgba8 when may_have_transparency is false?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yep! It won't help much for now because when rendering we always set this to true, but in a future where we detect opaque backgrounds it will hopefully be helpful.

fn unpremultiply_rgba8_impl<S: Simd>(simd: S, data: &mut [u8]) {
let (body, tail) = data.as_chunks_mut::<64>();

for chunk in body {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we add an opaque fast path per SIMD chunk? Many images have large opaque regions, so checking whether all alpha lanes are 255 could avoid the reciprocal gather and multiplications for those chunks. It would also be useful to benchmark opaque and spatially clustered transparency separately.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes we can, but I did some testing, and while overall it seems to be worth it for fully opaque images, the advantage is much less visible with mixed images, likely due to the effect of the branch predictor. Therefore, I'd like to add this in a follow-up instead so I can evaluate it more carefully.


/// Unpremultiplies each RGBA8 pixel in `data`.
fn unpremultiply_rgba8(data: &mut [u8]) {
let level = Level::try_detect().unwrap_or(Level::baseline());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could the detected SIMD level be cached or supplied by the caller? Level::try_detect() currently runs for every conversion, so avoiding repeated detection may help small pixmaps where fixed overhead is significant.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I would suggest we just upgrade to new fearless_simd soon so we can profit off linebender/fearless_simd#278. Alternatively, we could have the user provide the level, but for consistency the same should exist for premultiply, so maybe better in a follow-up?

Base automatically changed from pixmap_bench to main August 24, 2026 07:00
@LaurenzV
LaurenzV force-pushed the laurenz/pixmap_unpremul branch from d8cc41a to 19406d2 Compare August 24, 2026 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants