vello_common: Optimize pixmap unpremultiplication - #1839
Conversation
6b48c78 to
d8cc41a
Compare
|
|
||
| #[inline(always)] | ||
| pub(crate) fn scalar(component: u8, reciprocal: u16) -> u8 { | ||
| (u16::from(component) * reciprocal).div_256() as u8 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
Could we skip unpremultiply_rgba8 when may_have_transparency is false?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
d8cc41a to
19406d2
Compare
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:
After:
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.