| // Copyright 2026 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| //! Visual regression tests specifically designed to exercise GPU scheduler paths. | |
| //! | |
| //! Note that these tests were designed to work with the initial version of the scheduler. | |
| //! It's possible that, if the scheduling algorithm changes, those paths aren't exercised anymore | |
| //! in the way they initially were intended to be. Nevertheless, regardless of the exact | |
| //! implementation, they will still always be useful to test the functionality of deeper | |
| //! layer stacks. | |
| use crate::renderer::Renderer; | |
| use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive}; | |
| use vello_common::kurbo::{Circle, Rect, Shape}; | |
| use vello_common::peniko::{BlendMode, Color, Compose, Mix}; | |
| use vello_dev_macros::vello_test; | |
| // Some of the comments below refer to a so-called "primer child". This is an | |
| // otherwise-unneeded child layer whose sole purpose is to introduce an earlier node, forcing the | |
| // scheduler to allocate atlas space for the parent before processing the following child. See the | |
| // scheduler documentation for more information. | |
| /// Test blending a child whose bounds extend beyond its clipped parent's allocation. | |
| #[vello_test(gpu_tolerance = 1, gpu_no_depth)] | |
| fn gpu_schedule_blended_child_outside_clipped_parent(ctx: &mut impl Renderer) { | |
| let parent_clip = Rect::new(40.0, 12.0, 92.0, 88.0).to_path(0.1); | |
| ctx.push_layer(Some(&parent_clip), None, None, None, None); | |
| ctx.set_paint(Color::from_rgb8(42, 154, 142)); | |
| ctx.fill_rect(&Rect::new(40.0, 12.0, 92.0, 88.0)); | |
| // The left side of the child lies outside the parent's clipped allocation, while the right | |
| // side remains visible inside it. | |
| ctx.push_blend_layer(BlendMode::new(Mix::Multiply, Compose::SrcOver)); | |
| ctx.set_paint(Color::from_rgb8(244, 174, 66)); | |
| ctx.fill_rect(&Rect::new(8.0, 28.0, 68.0, 72.0)); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| } | |
| /// Test that cropping a blended child also advances its source texture origin. | |
| #[vello_test(gpu_tolerance = 1)] | |
| fn gpu_schedule_cropped_blend_preserves_child_source_offset(ctx: &mut impl Renderer) { | |
| let parent_clip = Rect::new(40.0, 12.0, 92.0, 88.0).to_path(0.1); | |
| ctx.push_layer(Some(&parent_clip), None, None, None, None); | |
| ctx.set_paint(Color::from_rgb8(210, 210, 210)); | |
| ctx.fill_rect(&Rect::new(40.0, 12.0, 92.0, 88.0)); | |
| ctx.push_blend_layer(BlendMode::new(Mix::Multiply, Compose::SrcOver)); | |
| // This half is outside the parent and must not be sampled after cropping. | |
| ctx.set_paint(Color::from_rgb8(232, 72, 82)); | |
| ctx.fill_rect(&Rect::new(8.0, 28.0, 40.0, 72.0)); | |
| // Only this half overlaps the parent. | |
| ctx.set_paint(Color::from_rgb8(62, 116, 232)); | |
| ctx.fill_rect(&Rect::new(40.0, 28.0, 68.0, 72.0)); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| } | |
| /// Test an empty destructive child inside a filter layer with a non-zero source shift. | |
| #[vello_test(skip_multithreaded, gpu_tolerance = 1)] | |
| fn gpu_schedule_empty_destructive_child_in_shifted_filter(ctx: &mut impl Renderer) { | |
| ctx.set_paint(Color::from_rgb8(50, 66, 104)); | |
| ctx.fill_rect(&Rect::new(8.0, 8.0, 92.0, 92.0)); | |
| let filter = Filter::from_primitive(FilterPrimitive::Offset { dx: 16.0, dy: 12.0 }); | |
| ctx.push_filter_layer(filter); | |
| ctx.set_paint(Color::from_rgb8(230, 76, 92)); | |
| ctx.fill_rect(&Rect::new(16.0, 20.0, 72.0, 76.0)); | |
| // Clearing with an empty source should remove the filter layer's contents without affecting | |
| // the backdrop. | |
| ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::Clear)); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| } | |
| /// Test the behavior when a spill to a page 1 is forced and that page is later reused. | |
| #[vello_test(cpu_u8_tolerance = 2, gpu_tolerance = 1)] | |
| fn gpu_schedule_atlas_page_one_reuse(ctx: &mut impl Renderer) { | |
| const BOUNDS: Rect = Rect::new(6.0, 6.0, 94.0, 94.0); | |
| let clip = Rect::new(6.5, 6.5, 93.5, 93.5).to_path(0.1); | |
| // Depth one shifts the pressure layer below to even parity. | |
| ctx.push_layer(None, None, None, None, None); | |
| // This depth-two allocation remains live on even page zero while all three branches are | |
| // scheduled, forcing their depth-four allocations onto even page one. | |
| ctx.push_layer(None, None, None, None, None); | |
| ctx.set_paint(Color::from_rgb8(24, 28, 58)); | |
| ctx.fill_rect(&BOUNDS); | |
| // The primer child causes the depth-two target to be allocated before any of the depth-four | |
| // branches request storage. | |
| ctx.push_layer(None, None, None, None, None); // Depth three primer. | |
| ctx.set_paint(Color::from_rgb8(55, 38, 86).with_alpha(0.35)); | |
| ctx.fill_rect(&BOUNDS); | |
| ctx.pop_layer(); | |
| // The first depth-four branch occupies even page one while the depth-two allocation remains | |
| // live on even page zero. | |
| ctx.push_layer(None, None, None, None, None); | |
| ctx.set_paint(Color::from_rgb8(22, 124, 132).with_alpha(0.55)); | |
| ctx.fill_rect(&BOUNDS); | |
| ctx.push_layer(None, None, None, None, None); | |
| ctx.set_paint(Color::from_rgb8(255, 103, 122).with_alpha(0.82)); | |
| ctx.fill_path(&Circle::new((50.0, 50.0), 44.0).to_path(0.1)); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| // Once the first branch has been released, the second depth-four branch reuses even page one. | |
| ctx.push_layer(None, None, None, None, None); | |
| ctx.set_paint(Color::from_rgb8(96, 72, 168).with_alpha(0.45)); | |
| ctx.fill_rect(&BOUNDS); | |
| ctx.push_layer(Some(&clip), None, None, None, None); | |
| ctx.set_paint(Color::from_rgb8(112, 235, 184).with_alpha(0.8)); | |
| ctx.fill_path(&Circle::new((50.0, 50.0), 44.0).to_path(0.1)); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| // Once the second branch has been released, the third depth-four branch reuses the same | |
| // cleared region on even page one again. This time with a blend mode, for good measure. | |
| ctx.push_layer(None, None, None, None, None); | |
| ctx.set_paint(Color::from_rgb8(238, 156, 72).with_alpha(0.38)); | |
| ctx.fill_rect(&BOUNDS); | |
| ctx.push_layer( | |
| Some(&clip), | |
| Some(BlendMode::new(Mix::Multiply, Compose::SrcOver)), | |
| None, | |
| None, | |
| None, | |
| ); | |
| ctx.set_paint(Color::from_rgb8(255, 226, 94).with_alpha(0.86)); | |
| ctx.fill_path(&Circle::new((50.0, 50.0), 44.0).to_path(0.1)); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| } | |
| /// Test the behavior when forcing even deeper spills (in this case to page index 2). | |
| #[vello_test(cpu_u8_tolerance = 1, gpu_tolerance = 1)] | |
| fn gpu_schedule_atlas_page_index_two(ctx: &mut impl Renderer) { | |
| const BOUNDS: Rect = Rect::new(6.0, 6.0, 94.0, 94.0); | |
| ctx.push_layer(None, None, None, None, None); // Depth one. | |
| ctx.push_layer(None, None, None, None, None); // Depth two: even page zero. | |
| ctx.set_paint(Color::from_rgb8(64, 48, 132).with_alpha(0.75)); | |
| ctx.fill_rect(&BOUNDS); | |
| // The primer child causes the depth-two target to be allocated on even page zero before deeper | |
| // layers request storage. | |
| ctx.push_layer(None, None, None, None, None); // Depth three primer. | |
| ctx.set_paint(Color::from_rgb8(213, 70, 126).with_alpha(0.3)); | |
| ctx.fill_rect(&BOUNDS); | |
| ctx.pop_layer(); | |
| ctx.push_layer(None, None, None, None, None); // Depth three. | |
| ctx.push_layer(None, None, None, None, None); // Depth four: even page one. | |
| ctx.set_paint(Color::from_rgb8(36, 190, 166).with_alpha(0.62)); | |
| ctx.fill_rect(&BOUNDS); | |
| // The primer child causes the depth-four target to be allocated on even page one before the | |
| // depth-six child requests storage. | |
| ctx.push_layer(None, None, None, None, None); // Depth five primer. | |
| ctx.set_paint(Color::from_rgb8(255, 194, 86).with_alpha(0.32)); | |
| ctx.fill_rect(&BOUNDS); | |
| ctx.pop_layer(); | |
| ctx.push_layer(None, None, None, None, None); // Depth five. | |
| ctx.push_layer(None, None, None, None, None); // Depth six: even page two. | |
| ctx.set_paint(Color::from_rgb8(255, 103, 122).with_alpha(0.86)); | |
| ctx.fill_path(&Circle::new((50.0, 50.0), 44.0).to_path(0.1)); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| } | |
| /// Test the behavior when a whole filter layer needs to be processed on page 1. | |
| #[vello_test(skip_multithreaded, cpu_u8_tolerance = 3, gpu_tolerance = 1)] | |
| fn gpu_schedule_filter_atlas_page_one(ctx: &mut impl Renderer) { | |
| const BOUNDS: Rect = Rect::new(6.0, 6.0, 94.0, 94.0); | |
| let filter = Filter::from_primitive(FilterPrimitive::Offset { dx: 0.0, dy: 0.0 }); | |
| ctx.push_layer(None, None, None, None, None); // Depth one. | |
| ctx.push_layer(None, None, None, None, None); // Depth two: even page zero. | |
| ctx.set_paint(Color::from_rgb8(54, 72, 154).with_alpha(0.62)); | |
| ctx.fill_rect(&BOUNDS); | |
| // The primer child causes the depth-two target to be allocated on even page zero before the | |
| // next child requests storage. | |
| ctx.push_layer(None, None, None, None, None); // Depth three primer. | |
| ctx.set_paint(Color::from_rgb8(172, 104, 214).with_alpha(0.24)); | |
| ctx.fill_rect(&BOUNDS); | |
| ctx.pop_layer(); | |
| ctx.push_layer(None, None, None, None, None); // Depth three: odd page zero. | |
| ctx.set_paint(Color::from_rgb8(32, 168, 148).with_alpha(0.58)); | |
| ctx.fill_rect(&BOUNDS); | |
| // The primer child causes the depth-three target to be allocated on odd page zero before the | |
| // next child requests storage. | |
| ctx.push_layer(None, None, None, None, None); // Depth four primer. | |
| ctx.set_paint(Color::from_rgb8(255, 180, 92).with_alpha(0.26)); | |
| ctx.fill_rect(&BOUNDS); | |
| ctx.pop_layer(); | |
| // The filter source cannot fit beside the live even allocation, and its padded temporary | |
| // cannot fit beside the live odd allocation. | |
| ctx.push_filter_layer(filter); // Depth four: even page one, odd temporary page one. | |
| ctx.set_paint(Color::from_rgb8(112, 126, 232).with_alpha(0.9)); | |
| ctx.fill_path(&Circle::new((50.0, 50.0), 44.0).to_path(0.1)); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| ctx.pop_layer(); | |
| } | |
| /// Test the behavior when multiple filter layers are batched together in a single layer texture. | |
| #[vello_test(skip_multithreaded, gpu_tolerance = 2)] | |
| fn gpu_schedule_filter_batch_page_zero(ctx: &mut impl Renderer) { | |
| let blur_two = Filter::from_primitive(FilterPrimitive::GaussianBlur { | |
| std_deviation: 2.0, | |
| edge_mode: EdgeMode::None, | |
| }); | |
| let blur_four = Filter::from_primitive(FilterPrimitive::GaussianBlur { | |
| std_deviation: 4.0, | |
| edge_mode: EdgeMode::None, | |
| }); | |
| let drop_shadow = Filter::from_primitive(FilterPrimitive::DropShadow { | |
| dx: 4.0, | |
| dy: 4.0, | |
| std_deviation: 4.0, | |
| color: Color::from_rgb8(40, 28, 72).with_alpha(0.8), | |
| edge_mode: EdgeMode::None, | |
| }); | |
| let offset_zero = Filter::from_primitive(FilterPrimitive::Offset { dx: 0.0, dy: 0.0 }); | |
| let texture_sizing_filter = Filter::from_primitive(FilterPrimitive::Offset { | |
| dx: 180.0, | |
| dy: 180.0, | |
| }); | |
| // Make sure that the minimum texture size is expanded enough by drawing a large, nearly | |
| // invisible layer. | |
| ctx.push_filter_layer(texture_sizing_filter); | |
| ctx.set_paint(Color::WHITE); | |
| ctx.fill_rect(&Rect::new(0.0, 0.0, 20.0, 20.0)); | |
| ctx.pop_layer(); | |
| for (filter, bounds, color) in [ | |
| ( | |
| blur_two, | |
| Rect::new(12.5, 12.5, 42.5, 42.5), | |
| Color::from_rgb8(255, 105, 120), | |
| ), | |
| ( | |
| blur_four, | |
| Rect::new(57.5, 12.5, 87.5, 42.5), | |
| Color::from_rgb8(55, 190, 178), | |
| ), | |
| ( | |
| drop_shadow, | |
| Rect::new(12.5, 57.5, 42.5, 87.5), | |
| Color::from_rgb8(255, 199, 82), | |
| ), | |
| ( | |
| offset_zero, | |
| Rect::new(57.5, 57.5, 87.5, 87.5), | |
| Color::from_rgb8(118, 112, 235), | |
| ), | |
| ] { | |
| ctx.push_filter_layer(filter); | |
| ctx.set_paint(color); | |
| ctx.fill_rect(&bounds); | |
| ctx.pop_layer(); | |
| } | |
| } |