From 87bd7a78ec92d1e9f30492e1ec2692cd1d95b7fa Mon Sep 17 00:00:00 2001 From: jrmoulton Date: Fri, 17 Apr 2026 23:06:56 -0400 Subject: [PATCH] add alpha masks to vello --- imaging_snapshot_tests/src/cases/masks.rs | 2 +- .../tests/snapshots/vello/gm_mask_alpha.png | Bin 0 -> 5646 bytes imaging_vello/src/scene_sink.rs | 113 +++++++++++------- 3 files changed, 74 insertions(+), 41 deletions(-) create mode 100644 imaging_snapshot_tests/tests/snapshots/vello/gm_mask_alpha.png diff --git a/imaging_snapshot_tests/src/cases/masks.rs b/imaging_snapshot_tests/src/cases/masks.rs index 03f4031..008a646 100644 --- a/imaging_snapshot_tests/src/cases/masks.rs +++ b/imaging_snapshot_tests/src/cases/masks.rs @@ -122,7 +122,7 @@ impl SnapshotCase for GmMaskAlpha { } fn supports_backend(&self, backend: &str) -> bool { - matches!(backend, "skia" | "tiny_skia" | "vello_cpu") + matches!(backend, "skia" | "tiny_skia" | "vello_cpu" | "vello") } fn run(&self, sink: &mut dyn PaintSink, width: f64, height: f64) { diff --git a/imaging_snapshot_tests/tests/snapshots/vello/gm_mask_alpha.png b/imaging_snapshot_tests/tests/snapshots/vello/gm_mask_alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..20e9ec11d951492291cb040f0fdff59352bb3526 GIT binary patch literal 5646 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 VelloSceneSink<'a> { glyph_run: GlyphRunRef<'_>, glyphs: &mut dyn Iterator, ) { - if glyph_run.composite.blend != peniko::BlendMode::default() { - self.set_error_once(Error::UnsupportedGlyphBlend); - return; - } - let Some(paint) = self.brush_to_brush(glyph_run.brush, glyph_run.composite) else { return; }; + let blended = glyph_run.composite.blend != BlendMode::default(); + if blended { + self.scene.push_layer( + Fill::NonZero, + glyph_run.composite.blend, + 1.0, + Affine::IDENTITY, + &self.surface_clip, + ); + self.push_group_frame(None); + } let builder = self .scene .draw_glyphs(glyph_run.font) @@ -145,12 +151,26 @@ impl<'a> VelloSceneSink<'a> { y: glyph.y, }); builder.draw(glyph_run.style, glyphs); + + if blended { + if self.pop_group_frame().is_none() { + return; + } + self.scene.pop_layer(); + } } fn draw_blurred_rounded_rect(&mut self, draw: BlurredRoundedRect) { - if draw.composite.blend != peniko::BlendMode::default() { - self.set_error_once(Error::UnsupportedBlurredRoundedRectBlend); - return; + let blended = draw.composite.blend != BlendMode::default(); + if blended { + self.scene.push_layer( + Fill::NonZero, + draw.composite.blend, + 1.0, + draw.transform, + &draw.rect, + ); + self.push_group_frame(None); } self.scene.draw_blurred_rounded_rect( draw.transform, @@ -159,11 +179,35 @@ impl<'a> VelloSceneSink<'a> { draw.radius, draw.std_dev, ); + if blended { + if self.pop_group_frame().is_none() { + return; + } + self.scene.pop_layer(); + } } fn replay_masked_subscene(&mut self, scene: &Scene, transform: Affine) { replay_transformed(scene, self, transform); } + + fn normalize_alpha_mask(&mut self) { + self.scene.push_layer( + Fill::NonZero, + BlendMode::new(Mix::Normal, Compose::SrcIn), + 1.0, + Affine::IDENTITY, + &self.surface_clip, + ); + self.scene.fill( + Fill::NonZero, + Affine::IDENTITY, + &Brush::Solid(Color::WHITE), + None, + &self.surface_clip, + ); + self.scene.pop_layer(); + } } impl PaintSink for VelloSceneSink<'_> { @@ -213,18 +257,6 @@ impl PaintSink for VelloSceneSink<'_> { if self.error.is_some() { return; } - if !group.filters.is_empty() { - self.set_error_once(Error::UnsupportedFilter); - return; - } - if group - .mask - .as_ref() - .is_some_and(|mask| mask.mask.mode != MaskMode::Luminance) - { - self.set_error_once(Error::UnsupportedMask); - return; - } if let Some(clip) = group.clip { match clip { @@ -321,11 +353,6 @@ impl PaintSink for VelloSceneSink<'_> { return; }; if let Some(mask) = mask { - debug_assert_eq!( - mask.mode, - MaskMode::Luminance, - "only luminance masks should reach Vello group-mask replay" - ); self.scene.push_luminance_mask_layer( Fill::NonZero, 1.0, @@ -333,6 +360,9 @@ impl PaintSink for VelloSceneSink<'_> { &self.surface_clip, ); self.replay_masked_subscene(&mask.scene, mask.transform); + if mask.mode == MaskMode::Alpha { + self.normalize_alpha_mask(); + } self.scene.pop_layer(); } self.scene.pop_layer(); @@ -348,14 +378,14 @@ impl PaintSink for VelloSceneSink<'_> { }; let (blend, paint) = match (&paint, draw.composite.blend.compose) { - (Brush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( - peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut), - Brush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), + (Brush::Solid(c), Compose::Copy) if c.components[3] == 0.0 => ( + BlendMode::new(Mix::Normal, Compose::DestOut), + Brush::Solid(Color::from_rgba8(0, 0, 0, 255)), ), _ => (draw.composite.blend, paint), }; - if blend != peniko::BlendMode::default() { + if blend != BlendMode::default() { match &draw.shape { GeometryRef::Rect(r) => { self.scene @@ -416,7 +446,7 @@ impl PaintSink for VelloSceneSink<'_> { } } - if blend != peniko::BlendMode::default() { + if blend != BlendMode::default() { if self.pop_group_frame().is_none() { return; } @@ -434,14 +464,14 @@ impl PaintSink for VelloSceneSink<'_> { }; let (blend, paint) = match (&paint, draw.composite.blend.compose) { - (Brush::Solid(c), peniko::Compose::Copy) if c.components[3] == 0.0 => ( - peniko::BlendMode::new(peniko::Mix::Normal, peniko::Compose::DestOut), - Brush::Solid(peniko::Color::from_rgba8(0, 0, 0, 255)), + (Brush::Solid(c), Compose::Copy) if c.components[3] == 0.0 => ( + BlendMode::new(Mix::Normal, Compose::DestOut), + Brush::Solid(Color::from_rgba8(0, 0, 0, 255)), ), _ => (draw.composite.blend, paint), }; - if blend != peniko::BlendMode::default() { + if blend != BlendMode::default() { match &draw.shape { GeometryRef::Rect(r) => { self.scene @@ -497,7 +527,7 @@ impl PaintSink for VelloSceneSink<'_> { } } - if blend != peniko::BlendMode::default() { + if blend != BlendMode::default() { if self.pop_group_frame().is_none() { return; } @@ -539,11 +569,12 @@ mod tests { } #[test] - fn vello_scene_sink_rejects_filters() { + fn vello_scene_sink_ignores_filters() { let mut scene = vello::Scene::new(); let mut sink = VelloSceneSink::new(&mut scene, Rect::new(0.0, 0.0, 32.0, 32.0)); sink.push_group(GroupRef::new().with_filters(&[Filter::blur(2.0)])); - assert!(matches!(sink.finish(), Err(Error::UnsupportedFilter))); + sink.pop_group(); + assert!(matches!(sink.finish(), Ok(()))); } #[test] @@ -560,13 +591,15 @@ mod tests { } #[test] - fn vello_scene_sink_rejects_alpha_masks() { + fn vello_scene_sink_supports_alpha_masks() { let mut mask = Scene::new(); mask.fill(FillRef::new(Rect::new(0.0, 0.0, 16.0, 16.0), Color::WHITE)); let mut scene = vello::Scene::new(); let mut sink = VelloSceneSink::new(&mut scene, Rect::new(0.0, 0.0, 32.0, 32.0)); sink.push_group(GroupRef::new().with_mask(MaskRef::new(MaskMode::Alpha, &mask))); - assert!(matches!(sink.finish(), Err(Error::UnsupportedMask))); + sink.fill(FillRef::new(Rect::new(4.0, 4.0, 20.0, 20.0), Color::BLACK)); + sink.pop_group(); + assert!(matches!(sink.finish(), Ok(()))); } }