From aa015ae18781fb5de82cc6a67c8b2b21f4ede15b Mon Sep 17 00:00:00 2001 From: Adrian Eddy Date: Wed, 30 Sep 2026 03:40:48 +0200 Subject: [PATCH 1/4] vello_gpu: Keep transparent draws while a filter is active Draws with a fully transparent solid paint were skipped before the inline filter layer was pushed or the draw was recorded into an enclosing filter layer. A filter can make that geometry visible (a flood fills it, a color matrix can add an alpha offset), so the draw disappeared on the GPU while vello_cpu, which never skips it, rendered it. Only skip transparent draws when no filter is active. --- vello_common/src/record.rs | 10 ++++++++ vello_gpu/src/scene.rs | 12 ++++++++-- .../filter_flood_transparent_paint.png | 3 +++ vello_tests/tests/filter.rs | 24 ++++++++++++++++++- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 vello_tests/snapshots/filter_flood_transparent_paint.png diff --git a/vello_common/src/record.rs b/vello_common/src/record.rs index 47e5e3423..d0542a694 100644 --- a/vello_common/src/record.rs +++ b/vello_common/src/record.rs @@ -239,6 +239,16 @@ impl CommandRecorder { !self.layer_stack.is_empty() } + /// Whether any of the currently open layers is a filter layer. + pub fn in_filter_layer(&self) -> bool { + self.layer_stack.iter().any(|layer| { + matches!( + self.layers[layer.id as usize].kind, + RecordedLayerKind::Filter { .. } + ) + }) + } + /// Reset the command recorder. #[inline] pub fn reset(&mut self, width: u16, height: u16) { diff --git a/vello_gpu/src/scene.rs b/vello_gpu/src/scene.rs index 5f7a4fb02..010ad03a8 100644 --- a/vello_gpu/src/scene.rs +++ b/vello_gpu/src/scene.rs @@ -315,9 +315,17 @@ impl Scene { invert_paint_transform(self.effective_paint_transform()).is_some() } + /// Whether a draw with the current paint can change the output. + /// + /// A fully transparent paint draws nothing, unless a filter is active: filters such as a flood + /// can make the transparent geometry visible. + fn paint_is_visible(&self) -> bool { + self.paint_visible || self.filter.is_some() || self.recorder.in_filter_layer() + } + /// Whether drawing with the current paint can produce any pixels. fn paint_has_area(&self) -> bool { - self.paint_visible + self.paint_is_visible() && (matches!(self.render_state.paint, PaintType::Solid(_)) || self.paint_transform_has_area()) } @@ -559,7 +567,7 @@ impl Scene { std_dev: f32, invert: bool, ) { - if !self.paint_visible || !self.paint_transform_has_area() { + if !self.paint_is_visible() || !self.paint_transform_has_area() { return; } diff --git a/vello_tests/snapshots/filter_flood_transparent_paint.png b/vello_tests/snapshots/filter_flood_transparent_paint.png new file mode 100644 index 000000000..c66d9f923 --- /dev/null +++ b/vello_tests/snapshots/filter_flood_transparent_paint.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c09ccf697bceeb468aa0fcec29cac4c73da7c482d4787afecae0530a9230498b +size 105 diff --git a/vello_tests/tests/filter.rs b/vello_tests/tests/filter.rs index c93ab183f..ada4bebde 100644 --- a/vello_tests/tests/filter.rs +++ b/vello_tests/tests/filter.rs @@ -8,7 +8,7 @@ use crate::util::{circular_star, stops_blue_green_red_yellow}; use crate::{renderer::Renderer, util::layout_glyphs_roboto}; use vello_common::color::AlphaColor; use vello_common::color::palette::css::{ - BLACK, LIME, PURPLE, REBECCA_PURPLE, ROYAL_BLUE, SEA_GREEN, TOMATO, VIOLET, + BLACK, LIME, PURPLE, REBECCA_PURPLE, ROYAL_BLUE, SEA_GREEN, TOMATO, TRANSPARENT, VIOLET, }; use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive}; use vello_common::kurbo::{Affine, BezPath, Circle, Point, Rect, Shape, Stroke}; @@ -52,6 +52,28 @@ fn filter_flood_star(ctx: &mut impl Renderer) { ctx.pop_layer(); } +/// A flood covers the drawn geometry even when the paint itself is fully transparent, both for a +/// filter layer and for an inline filter effect. +#[vello_test(skip_multithreaded)] +fn filter_flood_transparent_paint(ctx: &mut impl Renderer) { + let filter_flood = Filter::from_primitive(FilterPrimitive::Flood { color: TOMATO }); + let left = Rect::new(8.0, 8.0, 48.0, 92.0); + let right = Rect::new(52.0, 8.0, 92.0, 92.0); + ctx.set_paint(TRANSPARENT); + + ctx.push_clip_layer(&left.to_path(0.1)); + ctx.push_filter_layer(filter_flood.clone()); + ctx.fill_rect(&left); + ctx.pop_layer(); + ctx.pop_layer(); + + ctx.push_clip_layer(&right.to_path(0.1)); + ctx.set_filter_effect(filter_flood); + ctx.fill_rect(&right); + ctx.reset_filter_effect(); + ctx.pop_layer(); +} + #[vello_test(skip_multithreaded)] fn filter_offset_simple(ctx: &mut impl Renderer) { let filter = Filter::from_primitive(FilterPrimitive::Offset { dx: 15.0, dy: 15.0 }); From 17e83809ca3a4ed6928f8c14c7b0e1e6101cf356 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Thu, 1 Oct 2026 10:14:06 +0200 Subject: [PATCH 2/4] vello_gpu: Stop skipping transparent draws --- vello_common/src/record.rs | 10 -------- vello_gpu/src/scene.rs | 46 ++++--------------------------------- vello_tests/tests/filter.rs | 10 ++++---- 3 files changed, 9 insertions(+), 57 deletions(-) diff --git a/vello_common/src/record.rs b/vello_common/src/record.rs index d0542a694..47e5e3423 100644 --- a/vello_common/src/record.rs +++ b/vello_common/src/record.rs @@ -239,16 +239,6 @@ impl CommandRecorder { !self.layer_stack.is_empty() } - /// Whether any of the currently open layers is a filter layer. - pub fn in_filter_layer(&self) -> bool { - self.layer_stack.iter().any(|layer| { - matches!( - self.layers[layer.id as usize].kind, - RecordedLayerKind::Filter { .. } - ) - }) - } - /// Reset the command recorder. #[inline] pub fn reset(&mut self, width: u16, height: u16) { diff --git a/vello_gpu/src/scene.rs b/vello_gpu/src/scene.rs index 010ad03a8..10108b121 100644 --- a/vello_gpu/src/scene.rs +++ b/vello_gpu/src/scene.rs @@ -219,8 +219,6 @@ pub struct Scene { pub(crate) aliasing_threshold: Option, /// Storage for encoded non-solid paint data. pub(crate) encoded_paints: Vec, - /// Whether the current paint is visible (e.g., alpha > 0). - paint_visible: bool, /// Storage for generated strips and alpha values. pub(crate) strip_storage: RefCell, /// Current filter effect applied to individual draw operations. @@ -249,7 +247,6 @@ impl Scene { root_transforms: RootTransforms::default(), aliasing_threshold: None, encoded_paints: vec![], - paint_visible: true, strip_storage: RefCell::new(StripStorage::new(GenerationMode::Append)), filter: None, recorder: CommandRecorder::new(width, height), @@ -315,19 +312,9 @@ impl Scene { invert_paint_transform(self.effective_paint_transform()).is_some() } - /// Whether a draw with the current paint can change the output. - /// - /// A fully transparent paint draws nothing, unless a filter is active: filters such as a flood - /// can make the transparent geometry visible. - fn paint_is_visible(&self) -> bool { - self.paint_visible || self.filter.is_some() || self.recorder.in_filter_layer() - } - /// Whether drawing with the current paint can produce any pixels. fn paint_has_area(&self) -> bool { - self.paint_is_visible() - && (matches!(self.render_state.paint, PaintType::Solid(_)) - || self.paint_transform_has_area()) + matches!(self.render_state.paint, PaintType::Solid(_)) || self.paint_transform_has_area() } /// Fill a path with the current paint and fill rule. @@ -567,7 +554,7 @@ impl Scene { std_dev: f32, invert: bool, ) { - if !self.paint_is_visible() || !self.paint_transform_has_area() { + if !self.paint_transform_has_area() { return; } @@ -785,14 +772,6 @@ impl Scene { // render time into a texture usable by the renderer backend. pub fn set_paint(&mut self, paint: impl Into) { self.render_state.paint = paint.into(); - self.set_paint_visible(); - } - - fn set_paint_visible(&mut self) { - self.paint_visible = match &self.render_state.paint { - PaintType::Solid(color) => color.components[3] != 0.0, - _ => true, - }; } /// Set the tint for subsequent image paint operations. @@ -886,8 +865,6 @@ impl Scene { self.root_transforms.reset(); self.render_state.reset(); self.aliasing_threshold = None; - self.set_paint_visible(); - self.recorder.reset(self.width, self.height); self.filter = None; } @@ -904,10 +881,7 @@ impl Scene { /// Take current rendering state and reset the existing state to its default. pub fn take_current_state(&mut self) -> RenderState { - let state = core::mem::take(&mut self.render_state); - self.set_paint_visible(); - - state + core::mem::take(&mut self.render_state) } /// Save a copy of the current rendering state. @@ -918,7 +892,6 @@ impl Scene { /// Restore rendering state. pub fn restore_state(&mut self, state: RenderState) { self.render_state = state; - self.set_paint_visible(); } } @@ -936,7 +909,7 @@ mod tests { use vello_common::kurbo::{BezPath, Rect}; use vello_common::paint::{Image, ImageSource, Paint, PremulColor}; use vello_common::peniko::ImageSampler; - use vello_common::peniko::color::palette::css::{BLUE, TRANSPARENT}; + use vello_common::peniko::color::palette::css::BLUE; #[cfg(feature = "text")] use vello_common::peniko::{Blob, FontData}; use vello_common::record::Drawable; @@ -966,17 +939,6 @@ mod tests { assert!(scene.strip_storage.borrow().strips.is_empty()); } - #[test] - fn reset_restores_default_paint_visibility() { - let mut scene = Scene::new(100, 100); - scene.set_paint(TRANSPARENT); - - scene.reset(); - scene.fill_rect(&Rect::new(0.0, 0.0, 10.0, 10.0)); - - assert_eq!(scene.recorder.draws.len(), 1); - } - #[test] fn reset_restores_default_aliasing_threshold() { let mut scene = Scene::new(4, 4); diff --git a/vello_tests/tests/filter.rs b/vello_tests/tests/filter.rs index ada4bebde..c099d89d8 100644 --- a/vello_tests/tests/filter.rs +++ b/vello_tests/tests/filter.rs @@ -25,13 +25,13 @@ use vello_dev_macros::vello_test; fn filter_flood(ctx: &mut impl Renderer) { let filter_flood = Filter::from_primitive(FilterPrimitive::Flood { color: TOMATO }); - ctx.push_filter_layer(filter_flood); - ctx.set_paint(REBECCA_PURPLE); + ctx.set_filter_effect(filter_flood); + ctx.set_paint(TRANSPARENT); ctx.fill_rect(&Rect::new(0.0, 8.0, 256.0, 32.0)); - ctx.pop_layer(); + ctx.reset_filter_effect(); } -/// Test flood filter filling a star shape with solid color using a mask. +/// Test flood filter filling a star shape drawn with transparent paint using a mask. /// /// Note: SVG-compliant flood would use `feComposite` with `operator="in"`, which requires /// implementing the composite primitive and filter subregions. @@ -46,7 +46,7 @@ fn filter_flood_star(ctx: &mut impl Renderer) { // not trying to test here. ctx.push_clip_layer(&star_path); ctx.push_filter_layer(filter_flood); - ctx.set_paint(REBECCA_PURPLE); + ctx.set_paint(TRANSPARENT); ctx.fill_path(&star_path); ctx.pop_layer(); ctx.pop_layer(); From 21c4a6bb90e97eed3ff796cb67317a60d05ad025 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Thu, 1 Oct 2026 10:55:59 +0200 Subject: [PATCH 3/4] undo changes --- vello_tests/tests/filter.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vello_tests/tests/filter.rs b/vello_tests/tests/filter.rs index c099d89d8..ada4bebde 100644 --- a/vello_tests/tests/filter.rs +++ b/vello_tests/tests/filter.rs @@ -25,13 +25,13 @@ use vello_dev_macros::vello_test; fn filter_flood(ctx: &mut impl Renderer) { let filter_flood = Filter::from_primitive(FilterPrimitive::Flood { color: TOMATO }); - ctx.set_filter_effect(filter_flood); - ctx.set_paint(TRANSPARENT); + ctx.push_filter_layer(filter_flood); + ctx.set_paint(REBECCA_PURPLE); ctx.fill_rect(&Rect::new(0.0, 8.0, 256.0, 32.0)); - ctx.reset_filter_effect(); + ctx.pop_layer(); } -/// Test flood filter filling a star shape drawn with transparent paint using a mask. +/// Test flood filter filling a star shape with solid color using a mask. /// /// Note: SVG-compliant flood would use `feComposite` with `operator="in"`, which requires /// implementing the composite primitive and filter subregions. @@ -46,7 +46,7 @@ fn filter_flood_star(ctx: &mut impl Renderer) { // not trying to test here. ctx.push_clip_layer(&star_path); ctx.push_filter_layer(filter_flood); - ctx.set_paint(TRANSPARENT); + ctx.set_paint(REBECCA_PURPLE); ctx.fill_path(&star_path); ctx.pop_layer(); ctx.pop_layer(); From 9860d651e69521b86527c38afdd26f3684694c0d Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Thu, 1 Oct 2026 11:09:52 +0200 Subject: [PATCH 4/4] remove comment --- vello_tests/tests/filter.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/vello_tests/tests/filter.rs b/vello_tests/tests/filter.rs index ada4bebde..3335cd373 100644 --- a/vello_tests/tests/filter.rs +++ b/vello_tests/tests/filter.rs @@ -52,8 +52,6 @@ fn filter_flood_star(ctx: &mut impl Renderer) { ctx.pop_layer(); } -/// A flood covers the drawn geometry even when the paint itself is fully transparent, both for a -/// filter layer and for an inline filter effect. #[vello_test(skip_multithreaded)] fn filter_flood_transparent_paint(ctx: &mut impl Renderer) { let filter_flood = Filter::from_primitive(FilterPrimitive::Flood { color: TOMATO });