diff --git a/sparse_strips/vello_bench/src/coarse.rs b/sparse_strips/vello_bench/src/coarse.rs index 6ea75d980c..ffa021b537 100644 --- a/sparse_strips/vello_bench/src/coarse.rs +++ b/sparse_strips/vello_bench/src/coarse.rs @@ -33,6 +33,7 @@ pub fn coarse(c: &mut Criterion) { 0, None, &[], + false, ); std::hint::black_box(&wide); }); @@ -93,6 +94,7 @@ pub fn coarse_with_layer(c: &mut Criterion) { 0, None, &[], + false, ); // Pop all layers @@ -167,6 +169,7 @@ pub fn coarse_with_layer_large_viewport(c: &mut Criterion) { 0, None, &[], + false, ); // Pop all layers diff --git a/sparse_strips/vello_bench/src/fine/fill.rs b/sparse_strips/vello_bench/src/fine/fill.rs index e462349cf1..fc9df58942 100644 --- a/sparse_strips/vello_bench/src/fine/fill.rs +++ b/sparse_strips/vello_bench/src/fine/fill.rs @@ -68,6 +68,7 @@ pub(crate) fn fill_single>( &NoOpImageResolver, None, None, + false, ); std::hint::black_box(&fine); diff --git a/sparse_strips/vello_bench/src/fine/strip.rs b/sparse_strips/vello_bench/src/fine/strip.rs index 43f4ec1d2f..eeebdb0b1c 100644 --- a/sparse_strips/vello_bench/src/fine/strip.rs +++ b/sparse_strips/vello_bench/src/fine/strip.rs @@ -60,6 +60,7 @@ fn strip_single>( &NoOpImageResolver, Some(&alphas), None, + false, ); std::hint::black_box(&fine); diff --git a/sparse_strips/vello_common/src/coarse.rs b/sparse_strips/vello_common/src/coarse.rs index edcc0e0674..94221b1a39 100644 --- a/sparse_strips/vello_common/src/coarse.rs +++ b/sparse_strips/vello_common/src/coarse.rs @@ -502,6 +502,7 @@ impl Wide { thread_idx: u8, mask: Option, encoded_paints: &[EncodedPaint], + gamma_correction: bool, ) { if strip_buf.is_empty() { return; @@ -516,6 +517,7 @@ impl Wide { thread_idx, paint, blend_mode, + gamma_correction, mask, alpha_base_idx, }); @@ -2044,6 +2046,8 @@ pub struct FillAttrs { pub paint: Paint, /// The blend mode to apply before drawing the contents. pub blend_mode: BlendMode, + /// Whether to use gamma-corrected compositing for solid color fills. + pub gamma_correction: bool, /// A mask to apply to the command. pub mask: Option, /// Base index into the alpha buffer for this path's commands. @@ -2348,6 +2352,7 @@ mod tests { 0, None, &[], + false, ); assert!(wide.tiles_dirty); @@ -2384,6 +2389,7 @@ mod tests { 0, None, &[], + false, ); assert!(!wide.tiles_dirty); } diff --git a/sparse_strips/vello_cpu/src/dispatch/mod.rs b/sparse_strips/vello_cpu/src/dispatch/mod.rs index 5032342290..bcb947c9d1 100644 --- a/sparse_strips/vello_cpu/src/dispatch/mod.rs +++ b/sparse_strips/vello_cpu/src/dispatch/mod.rs @@ -37,6 +37,7 @@ pub(crate) trait Dispatcher: Debug + Send + Sync { aliasing_threshold: Option, mask: Option, encoded_paints: &[EncodedPaint], + gamma_correction: bool, ); fn stroke_path( &mut self, @@ -48,6 +49,7 @@ pub(crate) trait Dispatcher: Debug + Send + Sync { aliasing_threshold: Option, mask: Option, encoded_paints: &[EncodedPaint], + gamma_correction: bool, ); /// Fill a pixel-aligned rectangle with the current paint. fn fill_rect_fast( @@ -57,6 +59,7 @@ pub(crate) trait Dispatcher: Debug + Send + Sync { blend_mode: BlendMode, mask: Option, encoded_paints: &[EncodedPaint], + gamma_correction: bool, ); fn push_clip_path( &mut self, diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs index e3708ba2ae..a50b72210c 100644 --- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs +++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs @@ -308,6 +308,7 @@ impl MultiThreadedDispatcher { blend_mode, thread_id, mask, + gamma_correction, } => self.wide.generate( &task.allocation_group.strips [strip_range.start as usize..strip_range.end as usize], @@ -316,6 +317,7 @@ impl MultiThreadedDispatcher { thread_id, mask, encoded_paints, + gamma_correction, ), CoarseTaskType::RenderWideCommand { strips, @@ -330,6 +332,7 @@ impl MultiThreadedDispatcher { thread_id, mask, encoded_paints, + false, ), CoarseTaskType::PushLayer { thread_id, @@ -441,6 +444,7 @@ impl Dispatcher for MultiThreadedDispatcher { aliasing_threshold: Option, mask: Option, _encoded_paints: &[EncodedPaint], + gamma_correction: bool, ) { let start = self.allocation_group.path.len() as u32; self.allocation_group.path.extend(path); @@ -453,6 +457,7 @@ impl Dispatcher for MultiThreadedDispatcher { blend_mode, aliasing_threshold, mask, + gamma_correction, }); } @@ -466,6 +471,7 @@ impl Dispatcher for MultiThreadedDispatcher { aliasing_threshold: Option, mask: Option, _encoded_paints: &[EncodedPaint], + gamma_correction: bool, ) { let start = self.allocation_group.path.len() as u32; self.allocation_group.path.extend(path); @@ -478,6 +484,7 @@ impl Dispatcher for MultiThreadedDispatcher { blend_mode, aliasing_threshold, mask, + gamma_correction, }); } @@ -488,6 +495,7 @@ impl Dispatcher for MultiThreadedDispatcher { blend_mode: BlendMode, mask: Option, _encoded_paints: &[EncodedPaint], + gamma_correction: bool, ) { // For multi-threaded, fall back to path-based rendering. // TODO: Implement optimized rect strip generation in worker threads. @@ -508,6 +516,7 @@ impl Dispatcher for MultiThreadedDispatcher { blend_mode, aliasing_threshold: None, mask, + gamma_correction, }); } @@ -834,6 +843,7 @@ pub(crate) enum RenderTaskType { blend_mode: BlendMode, aliasing_threshold: Option, mask: Option, + gamma_correction: bool, }, WideCommand { strip_buf: Box<[Strip]>, @@ -849,6 +859,7 @@ pub(crate) enum RenderTaskType { blend_mode: BlendMode, aliasing_threshold: Option, mask: Option, + gamma_correction: bool, }, PushLayer { clip_path: Option<(Range, Affine)>, @@ -873,6 +884,7 @@ pub(crate) enum CoarseTaskType { blend_mode: BlendMode, paint: Paint, mask: Option, + gamma_correction: bool, }, RenderWideCommand { thread_id: u8, @@ -959,6 +971,7 @@ mod tests { None, None, &[], + false, ); dispatcher.flush(&[]); } diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded/worker.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded/worker.rs index 3d3154420e..92f989f8cc 100644 --- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded/worker.rs +++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded/worker.rs @@ -70,6 +70,7 @@ impl Worker { blend_mode, aliasing_threshold, mask, + gamma_correction, } => { let start = self.strip_storage.strips.len() as u32; let path = &render_task.allocation_group.path @@ -91,6 +92,7 @@ impl Worker { blend_mode, paint, mask, + gamma_correction, }; render_task @@ -106,6 +108,7 @@ impl Worker { stroke, aliasing_threshold, mask, + gamma_correction, } => { let start = self.strip_storage.strips.len() as u32; let path = &render_task.allocation_group.path @@ -127,6 +130,7 @@ impl Worker { blend_mode, paint, mask, + gamma_correction, }; render_task diff --git a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs index 19a73e656e..06c619c07e 100644 --- a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs +++ b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs @@ -539,6 +539,7 @@ impl Dispatcher for SingleThreadedDispatcher { aliasing_threshold: Option, mask: Option, encoded_paints: &[EncodedPaint], + gamma_correction: bool, ) { let wide = &mut self.wide; @@ -560,6 +561,7 @@ impl Dispatcher for SingleThreadedDispatcher { 0, mask, encoded_paints, + gamma_correction, ); } @@ -573,6 +575,7 @@ impl Dispatcher for SingleThreadedDispatcher { aliasing_threshold: Option, mask: Option, encoded_paints: &[EncodedPaint], + gamma_correction: bool, ) { let wide = &mut self.wide; @@ -594,6 +597,7 @@ impl Dispatcher for SingleThreadedDispatcher { 0, mask, encoded_paints, + gamma_correction, ); } @@ -604,6 +608,7 @@ impl Dispatcher for SingleThreadedDispatcher { blend_mode: BlendMode, mask: Option, encoded_paints: &[EncodedPaint], + gamma_correction: bool, ) { let wide = &mut self.wide; @@ -622,6 +627,7 @@ impl Dispatcher for SingleThreadedDispatcher { 0, mask, encoded_paints, + gamma_correction, ); } @@ -738,14 +744,7 @@ impl Dispatcher for SingleThreadedDispatcher { { // This case never gets hit because there is a compile_error in the root. // But have this code disables some warnings and makes the compile error easier to read - let _ = ( - buffer, - render_mode, - width, - height, - encoded_paints, - image_resolver, - ); + let _ = (buffer, render_mode, width, height, encoded_paints, image_resolver); } } @@ -827,16 +826,8 @@ impl Dispatcher for SingleThreadedDispatcher { #[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))] { let _ = ( - buffer, - width, - height, - dst_x, - dst_y, - dst_buffer_width, - dst_buffer_height, - render_mode, - encoded_paints, - image_resolver, + buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, + render_mode, encoded_paints, image_resolver, ); } } @@ -850,7 +841,7 @@ impl Dispatcher for SingleThreadedDispatcher { ) { // Generate coarse-level commands from pre-computed strips (thread_idx 0 for single-threaded). self.wide - .generate(strip_buf, paint, blend_mode, 0, None, encoded_paints); + .generate(strip_buf, paint, blend_mode, 0, None, encoded_paints, false); } fn strip_storage_mut(&mut self) -> &mut StripStorage { @@ -927,6 +918,7 @@ mod tests { None, None, &[], + false, ); // Ensure there is data to clear. diff --git a/sparse_strips/vello_cpu/src/fine/highp/blend.rs b/sparse_strips/vello_cpu/src/fine/highp/blend.rs index cb2174da11..53b2baa4cf 100644 --- a/sparse_strips/vello_cpu/src/fine/highp/blend.rs +++ b/sparse_strips/vello_cpu/src/fine/highp/blend.rs @@ -6,19 +6,27 @@ use crate::util::Premultiply; use vello_common::fearless_simd::*; #[derive(Copy, Clone)] -struct Channels { - r: f32x4, - g: f32x4, - b: f32x4, +pub(crate) struct Channels { + pub(crate) r: f32x4, + pub(crate) g: f32x4, + pub(crate) b: f32x4, } impl Channels { #[inline(always)] - fn unpremultiply(mut self, a: f32x4) -> Self { + pub(crate) fn unpremultiply(mut self, a: f32x4) -> Self { self.r = self.r.unpremultiply(a); self.g = self.g.unpremultiply(a); self.b = self.b.unpremultiply(a); + self + } + #[inline(always)] + pub(crate) fn square(mut self) -> Self { + self.r *= self.r; + self.g *= self.g; + self.b *= self.b; + self } } diff --git a/sparse_strips/vello_cpu/src/fine/highp/mod.rs b/sparse_strips/vello_cpu/src/fine/highp/mod.rs index 8117456763..3632bafb86 100644 --- a/sparse_strips/vello_cpu/src/fine/highp/mod.rs +++ b/sparse_strips/vello_cpu/src/fine/highp/mod.rs @@ -213,6 +213,30 @@ impl FineKernel for F32Kernel { } } + /// Implements the hybrid alpha blending from *Random-Access Rendering of General Vector Graphics* + /// (§4.1, Nehab and Hoppe, 2008, ), for solid colour paths. + /// + /// This currently uses a gamma of two for computation simplicity. + #[inline(always)] + fn alpha_composite_solid_hybrid_gamma( + simd: S, + dest: &mut [Self::Numeric], + src: [Self::Numeric; 4], + alphas: Option<&[u8]>, + ) { + if let Some(alphas) = alphas { + alpha_fill::alpha_composite_solid_hybrid_gamma( + simd, + dest, + src, + bytemuck::cast_slice::(alphas).iter().copied(), + ); + } else { + // When the coverage is solid, this is the same as Porter-Duff over. + fill::alpha_composite_solid(simd, dest, src); + } + } + /// Composites a source buffer onto a destination buffer using alpha blending. /// /// Dispatches to either the masked or unmasked implementation based on the @@ -417,6 +441,7 @@ mod alpha_fill { //! (e.g., from anti-aliasing or clip masks) that modulates the source alpha. use crate::fine::Splat4thExt; + use crate::fine::highp::blend::Channels; use crate::fine::highp::compose::ComposeExt; use crate::fine::highp::{blend, extract_masks}; use crate::peniko::BlendMode; @@ -446,6 +471,110 @@ mod alpha_fill { ); } + /// Implements the hybrid alpha blending from *Random-Access Rendering of General Vector Graphics* + /// (§4.1, Nehab and Hoppe, 2008, ), for solid colour paths. + /// + /// This makes the blending due to partial coverage happen in a linear space, with colour blending + /// happening in sRGB. + /// This effectively implements gamma correction for path coverage. + /// + /// Note that this function uses a gamma of 2 to trade off correctness for simplicity; we should actually + /// use the sRGB transfer function directly, or the standard power approximation for sRGB. + #[inline(always)] + pub(super) fn alpha_composite_solid_hybrid_gamma( + s: S, + dest: &mut [f32], + src: [f32; 4], + alphas: impl Iterator, + ) { + s.vectorize( + #[inline(always)] + || { + let src_a = f32x16::splat(s, src[3]); + let src_c = f32x16::block_splat(src.simd_into(s)); + let one = f32x16::splat(s, 1.0); + let one_4 = f32x4::splat(s, 1.0); + + for (next_dest, next_mask) in dest.chunks_exact_mut(16).zip(alphas) { + let bg_c = f32x16::from_slice(s, next_dest); + // 1 - src_a + let inv_src_a = src_a.mul_add(-one, one); + // Compute src over dst, which is the result which would be + let src_over_dst = bg_c.mul_add(inv_src_a, src_c); + + // Copied from `blend`. Ideally, we'd make Channels a more thoughtful abstraction here. + let split = { + #[inline(always)] + |input: f32x16| { + let mut storage = [0.0; 16]; + s.store_interleaved_128_f32x16(input, &mut storage); + let input_v = f32x16::from_slice(s, &storage); + + let p1 = s.split_f32x16(input_v); + let (r, g) = s.split_f32x8(p1.0); + let (b, a) = s.split_f32x8(p1.1); + + (Channels { r, g, b }, a) + } + }; + + // Convert the sRGB colour to linear, for the coverage based blending. + let (src_over_dst_c, src_over_dst_a) = split(src_over_dst); + let src_over_dst_straight = src_over_dst_c.unpremultiply(src_over_dst_a); + let src_over_dst_linear = src_over_dst_straight.square(); + + let (bg_ch, bg_a) = split(bg_c); + let bg_straight = bg_ch.unpremultiply(bg_a); + let bg_linear = bg_straight.square(); + + // Get the sparse strip mask as f32x4. + let mut mask_a: f32x4 = [ + next_mask[0] as f32, + next_mask[1] as f32, + next_mask[2] as f32, + next_mask[3] as f32, + ] + .simd_into(s); + mask_a *= f32x4::splat(s, 1.0 / 255.0); + + // 1-mask_a + let inv_mask_a = mask_a.mul_add(-one_4, one_4); + let lerp_channel_into_srgb = { + #[inline(always)] + |background_channel: f32x4, src_over_dst_channel: f32x4| { + // Lerp between the channel values in linear space, based on the alpha mask. + let res_linear = src_over_dst_channel + .mul_add(mask_a, background_channel * inv_mask_a); + // Convert this channel back into sRGB. + res_linear.sqrt() + } + }; + + let res_straight_r = lerp_channel_into_srgb(bg_linear.r, src_over_dst_linear.r); + let res_straight_g = lerp_channel_into_srgb(bg_linear.g, src_over_dst_linear.g); + let res_straight_b = lerp_channel_into_srgb(bg_linear.b, src_over_dst_linear.b); + + let src_a_4 = f32x4::splat(s, src[3]); + let effective_src_a = src_a_4 * mask_a; + let result_alpha = bg_a.mul_add(one_4 - effective_src_a, effective_src_a); + + let combined = s.combine_f32x8( + s.combine_f32x4( + res_straight_r * result_alpha, + res_straight_g * result_alpha, + ), + s.combine_f32x4(res_straight_b * result_alpha, result_alpha), + ); + let mut storage = [0.0; 16]; + // re-interleave into four sRGB colours. + s.store_interleaved_128_f32x16(combined, &mut storage); + + next_dest.copy_from_slice(storage.as_slice()); + } + }, + ); + } + /// Composites a buffer of colors with per-pixel alpha masks. /// /// Each pixel's source alpha is modulated by its corresponding mask value. diff --git a/sparse_strips/vello_cpu/src/fine/mod.rs b/sparse_strips/vello_cpu/src/fine/mod.rs index 649b5ea2a5..3f931dcf77 100644 --- a/sparse_strips/vello_cpu/src/fine/mod.rs +++ b/sparse_strips/vello_cpu/src/fine/mod.rs @@ -418,6 +418,23 @@ pub trait FineKernel: Send + Sync + 'static { alphas: Option<&[u8]>, ); + /// Perform gamma-corrected alpha compositing with a solid color over the target buffer. + /// + /// Uses the formula from "Random-Access Rendering of General Vector Graphics" + /// (Nehab & Hoppe 2008) with a γ=2 approximation (sqrt/square): + /// `blend(c, f̃, o) = lerp(sRGB⁻¹(over(f̃, sRGB(c))), c, o)` + /// + /// This is currently only implemented for the `f32` pipeline, for expedience and . + /// The default will fall back to [`Self::alpha_composite_solid`] + fn alpha_composite_solid_hybrid_gamma( + simd: S, + target: &mut [Self::Numeric], + src: [Self::Numeric; 4], + alphas: Option<&[u8]>, + ) { + Self::alpha_composite_solid(simd, target, src, alphas); + } + /// Perform alpha compositing with a source buffer over the destination buffer. /// /// Blends the source buffer contents over the destination using standard alpha compositing. @@ -561,6 +578,7 @@ impl> Fine { image_resolver, None, fill_attrs.mask.as_ref(), + fill_attrs.gamma_correction, ); } Cmd::AlphaFill(s) => { @@ -575,6 +593,7 @@ impl> Fine { image_resolver, Some(&alphas[alpha_idx..]), fill_attrs.mask.as_ref(), + fill_attrs.gamma_correction, ); } Cmd::Filter(_filter, _) => { @@ -678,6 +697,7 @@ impl> Fine { image_resolver: &dyn ImageResolver, alphas: Option<&[u8]>, mask: Option<&Mask>, + gamma_correction: bool, ) { let blend_buf = &mut self.blend_buf.last_mut().unwrap()[x * TILE_HEIGHT_COMPONENTS..] [..TILE_HEIGHT_COMPONENTS * width]; @@ -700,7 +720,11 @@ impl> Fine { } if default_blend && mask.is_none() { - T::alpha_composite_solid(self.simd, blend_buf, color, alphas); + if gamma_correction { + T::alpha_composite_solid_hybrid_gamma(self.simd, blend_buf, color, alphas); + } else { + T::alpha_composite_solid(self.simd, blend_buf, color, alphas); + } } else { let start_x = self.wide_coords.0 * WideTile::WIDTH + x as u16; let start_y = self.wide_coords.1 * Tile::HEIGHT; diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index c46620c3ab..43062a2835 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs @@ -105,6 +105,8 @@ pub struct RenderContext { pub(crate) temp_path: BezPath, /// Optional threshold for aliasing. pub(crate) aliasing_threshold: Option, + /// Whether to use gamma-corrected compositing for solid color fills. + pub(crate) gamma_correction: bool, pub(crate) encoded_paints: Vec, pub(crate) filter: Option, #[cfg_attr( @@ -178,6 +180,7 @@ impl RenderContext { let encoded_paints = vec![]; let temp_path = BezPath::new(); let aliasing_threshold = None; + let gamma_correction = false; Self { width, @@ -185,6 +188,7 @@ impl RenderContext { dispatcher, state: RenderState::default(), aliasing_threshold, + gamma_correction, render_settings: settings, mask: None, temp_path, @@ -225,6 +229,7 @@ impl RenderContext { ctx.aliasing_threshold, ctx.mask.clone(), &ctx.encoded_paints, + ctx.gamma_correction, ); }); } @@ -242,6 +247,7 @@ impl RenderContext { ctx.aliasing_threshold, ctx.mask.clone(), &ctx.encoded_paints, + ctx.gamma_correction, ); }); } @@ -263,6 +269,7 @@ impl RenderContext { ctx.state.blend_mode, ctx.mask.clone(), &ctx.encoded_paints, + ctx.gamma_correction, ); } else { // Fall back to path-based rendering for rotated/skewed transforms. @@ -276,6 +283,7 @@ impl RenderContext { ctx.aliasing_threshold, ctx.mask.clone(), &ctx.encoded_paints, + ctx.gamma_correction, ); } }); @@ -295,6 +303,7 @@ impl RenderContext { ctx.aliasing_threshold, ctx.mask.clone(), &ctx.encoded_paints, + ctx.gamma_correction, ); }); } @@ -350,6 +359,7 @@ impl RenderContext { self.aliasing_threshold, self.mask.clone(), &self.encoded_paints, + self.gamma_correction, ); } @@ -459,6 +469,19 @@ impl RenderContext { self.aliasing_threshold = aliasing_threshold; } + /// Set whether to use gamma-corrected compositing for solid color fills. + /// + /// When enabled, uses the formula from "Random-Access Rendering of General Vector Graphics" + /// (Nehab & Hoppe 2008): `blend(c, f̃, o) = lerp(sRGB⁻¹(over(f̃, sRGB(c))), c, o)`, using + /// sqrt/square as a γ=2 approximation of the sRGB transfer function. + /// + /// This only affects solid color fills rendered with the `f32` pipeline + /// (`OptimizeQuality` render mode). When using the `u8` pipeline (`OptimizeSpeed`), + /// gamma correction is silently ignored. + pub fn set_gamma_correction(&mut self, gamma_correction: bool) { + self.gamma_correction = gamma_correction; + } + /// Pop the last-pushed layer. pub fn pop_layer(&mut self) { self.dispatcher.pop_layer(); @@ -1124,6 +1147,75 @@ mod tests { ctx.render_to_pixmap(&mut resources, &mut pixmap); } + /// Test that gamma-corrected compositing of a semi-transparent fill over an opaque white + /// background matches the expected formula: + /// `result = (sqrt(src_c * src_a) + sqrt(bg) * (1 - src_a))²` + /// + /// Semi-transparent red (alpha=0.5) over opaque white (r=g=b=a=1): + /// - Red: sqrt(0.5*0.5) + sqrt(1)*0.5 = 0.5 + 0.5 = 1.0 → 1.0² = 1.0 + /// - Green: sqrt(0*0.5) + sqrt(1)*0.5 = 0.0 + 0.5 = 0.5 → 0.5² = 0.25 + /// - Blue: same as green = 0.25 + /// - Alpha: sqrt(0.5*0.5) + sqrt(1)*0.5 = 0.5 + 0.5 = 1.0 → 1.0² = 1.0 + #[cfg(feature = "f32_pipeline")] + #[test] + fn gamma_correction_semi_transparent() { + use crate::{RenderMode, RenderSettings}; + use vello_common::color::palette::css::{RED, WHITE}; + use vello_common::pixmap::Pixmap; + + let width: u16 = 4; + let height: u16 = 4; + + let settings = RenderSettings { + level: vello_common::fearless_simd::Level::new(), + num_threads: 0, + render_mode: RenderMode::OptimizeQuality, + }; + let mut ctx = RenderContext::new_with(width, height, settings); + let mut resources = crate::Resources::new(); + + // First fill with opaque white (hits the copy_solid fast path, unaffected by gamma). + ctx.set_paint(WHITE); + ctx.fill_rect(&Rect::new(0.0, 0.0, f64::from(width), f64::from(height))); + + // Then fill with 50% opaque red using gamma correction. + ctx.set_gamma_correction(true); + ctx.set_paint(RED.with_alpha(0.5)); + ctx.fill_rect(&Rect::new(0.0, 0.0, f64::from(width), f64::from(height))); + + let mut pixmap = Pixmap::new(width, height); + ctx.flush(); + ctx.render_to_pixmap(&mut resources, &mut pixmap); + + let expected_r = (1.00_f32 * 255.0 + 0.5) as u8; // 255 + let expected_g = (0.25_f32 * 255.0 + 0.5) as u8; // 64 + let expected_b = (0.25_f32 * 255.0 + 0.5) as u8; // 64 + let expected_a = (1.00_f32 * 255.0 + 0.5) as u8; // 255 + + for pixel in pixmap.data() { + assert_eq!( + pixel.r, expected_r, + "red channel: got {}, expected {}", + pixel.r, expected_r + ); + assert_eq!( + pixel.g, expected_g, + "green channel: got {}, expected {}", + pixel.g, expected_g + ); + assert_eq!( + pixel.b, expected_b, + "blue channel: got {}, expected {}", + pixel.b, expected_b + ); + assert_eq!( + pixel.a, expected_a, + "alpha channel: got {}, expected {}", + pixel.a, expected_a + ); + } + } + #[cfg(feature = "text")] #[test] fn glyph_atlas_resources_are_lazy() { diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs index 6803bbbb47..9be28ce33e 100644 --- a/sparse_strips/vello_hybrid/src/scene.rs +++ b/sparse_strips/vello_hybrid/src/scene.rs @@ -270,6 +270,7 @@ macro_rules! submit_strips { 0, None, &$self.encoded_paints.borrow(), + false, ); } }; @@ -587,6 +588,7 @@ impl Scene { 0, None, &self.encoded_paints.borrow(), + false, ); } FastStripCommand::Rect(r) => { @@ -606,6 +608,7 @@ impl Scene { 0, None, &self.encoded_paints.borrow(), + false, ); } } @@ -1103,6 +1106,7 @@ impl Scene { 0, None, &self.encoded_paints.borrow(), + false, ); } } diff --git a/sparse_strips/vello_toy/src/debug.rs b/sparse_strips/vello_toy/src/debug.rs index 7ff5ce1bc9..62ecd111c6 100644 --- a/sparse_strips/vello_toy/src/debug.rs +++ b/sparse_strips/vello_toy/src/debug.rs @@ -98,6 +98,7 @@ fn main() { 0, None, &[], + false, ); }