.
diff --git a/sparse_strips/vello_common/src/pixmap.rs b/sparse_strips/vello_common/src/pixmap.rs index 95b05da..c1ecff6 100644 --- a/sparse_strips/vello_common/src/pixmap.rs +++ b/sparse_strips/vello_common/src/pixmap.rs
@@ -8,7 +8,7 @@ #[cfg(feature = "png")] use std::io::{BufRead, Seek}; -use crate::fearless_simd::{Level, Simd, SimdBase, SimdInt, SimdMask, dispatch, mask8x16}; +use crate::fearless_simd::{Level, Simd, SimdBase, SimdInt, SimdMask, dispatch, mask8x16, u16x16}; use crate::peniko::{ImageAlphaType, color::PremulRgba8}; use crate::util::Div255Ext; @@ -391,20 +391,7 @@ pub fn take(self, alpha_type: ImageAlphaType) -> Vec<u8> { let mut data = bytemuck::cast_vec(self.buf); if alpha_type == ImageAlphaType::Alpha { - for pixel in data.chunks_exact_mut(4) { - let alpha = pixel[3]; - if alpha != 0 { - let scale = 255.0 / f32::from(alpha); - for component in &mut pixel[..3] { - #[expect( - clippy::cast_possible_truncation, - reason = "deliberate quantization" - )] - let unpremultiplied = (f32::from(*component) * scale + 0.5) as u8; - *component = unpremultiplied; - } - } - } + unpremultiply_rgba8(&mut data); } data } @@ -452,6 +439,64 @@ dispatch!(level, simd => premultiply_rgba8_impl(simd, data)) } +/// Unpremultiplies each RGBA8 pixel in `data` using an eight-bit fixed-point reciprocal. +fn unpremultiply_rgba8(data: &mut [u8]) { + let level = Level::try_detect().unwrap_or(Level::baseline()); + dispatch!(level, simd => unpremultiply_rgba8_impl(simd, data)); +} + +#[inline(always)] +fn unpremultiply_rgba8_impl<S: Simd>(simd: S, data: &mut [u8]) { + let (body, tail) = data.as_chunks_mut::<64>(); + let rounding = u16x16::splat(simd, 128); + + for chunk in body { + let rgba = simd.load_interleaved_128_u8x64(chunk); + let (rg, ba) = simd.split_u8x64(rgba); + let (r, g) = simd.split_u8x32(rg); + let (b, a) = simd.split_u8x32(ba); + + let reciprocal = + u16x16::from_fn(simd, |lane| UNPREMULTIPLY_RECIPROCALS[usize::from(a[lane])]); + let unpremultiply = |component| { + let product = simd.widen_u8x16(component) * reciprocal + rounding; + simd.narrow_u16x16(product >> 8) + }; + + let rgba = simd.combine_u8x32( + simd.combine_u8x16(unpremultiply(r), unpremultiply(g)), + simd.combine_u8x16(unpremultiply(b), a), + ); + simd.store_interleaved_128_u8x64(rgba, chunk); + } + + for pixel in tail.chunks_exact_mut(4) { + let reciprocal = UNPREMULTIPLY_RECIPROCALS[usize::from(pixel[3])]; + for component in &mut pixel[..3] { + let unpremultiplied = ((u16::from(*component) * reciprocal + 128) >> 8) as u8; + *component = unpremultiplied; + } + } +} + +#[expect( + clippy::cast_possible_truncation, + reason = "all generated reciprocals fit into a u16" +)] +const fn unpremultiply_reciprocals() -> [u16; 256] { + let mut values = [0; 256]; + // Preserve RGB for fully transparent pixels. + values[0] = 256; + let mut alpha = 1; + while alpha < 256 { + values[alpha] = ((255 * 256 + alpha / 2) / alpha) as u16; + alpha += 1; + } + values +} + +const UNPREMULTIPLY_RECIPROCALS: [u16; 256] = unpremultiply_reciprocals(); + #[inline(always)] fn premultiply_rgba8_impl<S: Simd>(simd: S, data: &mut [u8]) -> bool { let (body, tail) = data.as_chunks_mut::<64>(); @@ -493,7 +538,7 @@ #[cfg(test)] mod tests { - use alloc::vec; + use alloc::{vec, vec::Vec}; use super::{PixelMetadata, Pixmap}; use crate::peniko::ImageAlphaType; @@ -595,4 +640,41 @@ [199, 100, 50, 128, 9, 8, 7, 255, 1, 2, 3, 0] ); } + + #[test] + fn fixed_unpremultiply_is_within_one_for_all_valid_components() { + let mut data = Vec::new(); + for alpha in 0_u8..=255 { + for component in 0..=alpha { + data.extend_from_slice(&[component, alpha - component, component / 2, alpha]); + } + } + let pixel_count = data.len() / 4; + let pixmap = Pixmap::from_parts( + data.clone(), + pixel_count.try_into().unwrap(), + 1, + PixelMetadata::new(ImageAlphaType::AlphaPremultiplied, true), + ); + let unpremultiplied = pixmap.take(ImageAlphaType::Alpha); + + for (source, result) in data.chunks_exact(4).zip(unpremultiplied.chunks_exact(4)) { + let alpha = source[3]; + assert_eq!(result[3], alpha); + for (&source, &result) in source[..3].iter().zip(&result[..3]) { + let expected = if alpha == 0 { + source + } else { + (f32::from(source) * 255.0 / f32::from(alpha) + 0.5) as u8 + }; + assert!( + result.abs_diff(expected) <= 1, + "component {source} with alpha {alpha} produced {result} instead of {expected}" + ); + if source == alpha && alpha != 0 { + assert_eq!(result, 255); + } + } + } + } }