blob: 16b27dfdbddec59d3bdeec8f651e3320ba632672 [file] [edit]
// 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();
}
}