use cranpose_render_common::graph::{ProjectiveTransform, RenderGraph, RenderNode};
use cranpose_render_wgpu::CapturedFrame;
use cranpose_ui_graphics::{
GradientBlurDirection, GraphicsLayer, Rect, RenderEffect, SubstrateSpec, gradient_blur_effect,
};
use support::{ReferenceEdge, SubstrateProbeRead, region_pixels};
use crate::{shared_test_support, support};
const FRAME_WIDTH: u32 = 240;
const FRAME_HEIGHT: u32 = 120;
const GLASS: Rect = Rect {
x: 40.0,
y: 20.0,
width: 96.0,
height: 72.0,
};
const SUBSTRATE_RADIUS: f32 = 12.0;
#[test]
fn child_shader_tails_receive_their_substrates() {
let mut renderer = support::headless_renderer().expect("child substrates require GPU");
for spec in [
SubstrateSpec::Mean,
SubstrateSpec::Average { block: 4 },
SubstrateSpec::Blur { radius_px: 2.0 },
] {
let probe = support::substrate_probe(spec, SubstrateProbeRead::Held);
let graph = |effect| {
page(Some(RenderNode::Layer(Box::new(
shared_test_support::layer_node(
rect(0.0, 0.0, GLASS.width, GLASS.height),
ProjectiveTransform::translation(GLASS.x, GLASS.y),
GraphicsLayer {
render_effect: Some(effect),
..Default::default()
},
support::striped_page(GLASS.width as u32, GLASS.height as u32),
),
))))
};
let expected = capture(
&mut renderer,
graph(probe.clone().then(RenderEffect::offset(0.0, 0.0))),
);
let actual = capture(&mut renderer, graph(probe));
assert!(
region_pixels(&actual, GLASS) == region_pixels(&expected, GLASS),
"child {spec:?} substrate differs from its full effect path"
);
}
}
#[test]
fn shader_chains_receive_each_stages_substrates() {
let mut renderer = support::headless_renderer().expect("chained substrates require GPU");
for spec in [
SubstrateSpec::Mean,
SubstrateSpec::Average { block: 4 },
SubstrateSpec::Blur { radius_px: 2.0 },
] {
let probe = support::substrate_probe(spec, SubstrateProbeRead::Held);
let expected = capture(
&mut renderer,
page(Some(effect_layer(GLASS, probe.clone()))),
);
for terminal in [false, true] {
let mut chained = RenderEffect::offset(0.0, 0.0).then(probe.clone());
if !terminal {
chained = chained.then(RenderEffect::offset(0.0, 0.0));
}
let actual = capture(&mut renderer, page(Some(effect_layer(GLASS, chained))));
for (a, b) in region_pixels(&actual, GLASS)
.iter()
.zip(region_pixels(&expected, GLASS))
{
assert!(
a.abs_diff(b) <= 1,
"a chained {spec:?} substrate, terminal={terminal}, must equal the standalone path: {a} != {b}"
);
}
}
}
}
const BAND_TOP: f32 = 30.0;
const BAND_HEIGHT: f32 = 60.0;
const WIDE_RADIUS: f32 = 12.0;
fn rect(x: f32, y: f32, width: f32, height: f32) -> Rect {
Rect {
x,
y,
width,
height,
}
}
fn effect_layer(bounds: Rect, effect: RenderEffect) -> RenderNode {
RenderNode::Layer(Box::new(shared_test_support::layer_node(
rect(0.0, 0.0, bounds.width, bounds.height),
ProjectiveTransform::translation(bounds.x, bounds.y),
GraphicsLayer {
backdrop_effect: Some(effect),
..GraphicsLayer::default()
},
Vec::new(),
)))
}
fn page(layer: Option<RenderNode>) -> RenderGraph {
let mut children = support::striped_page(FRAME_WIDTH, FRAME_HEIGHT);
children.extend(layer);
support::page_graph(FRAME_WIDTH, FRAME_HEIGHT, children)
}
fn capture(renderer: &mut support::LockedRenderer, graph: RenderGraph) -> CapturedFrame {
support::capture_graph(renderer, graph, FRAME_WIDTH, FRAME_HEIGHT)
}
fn reference_blur_of(page: &CapturedFrame, region: Rect, radius: f32) -> Vec<f32> {
let pixels: Vec<f32> = region_pixels(page, region)
.iter()
.map(|value| f32::from(*value))
.collect();
support::reference_blur(
&pixels,
region.width as usize,
region.height as usize,
4,
radius,
ReferenceEdge::Clamp,
)
}
fn worst_deviation(
frame: &CapturedFrame,
region: Rect,
expected: &[f32],
rows: std::ops::Range<usize>,
inset: usize,
) -> (f32, (usize, usize)) {
let actual = region_pixels(frame, region);
let width = region.width as usize;
let mut worst = (0.0f32, (0, 0));
for y in rows {
for x in inset..width - inset {
for channel in 0..3 {
let index = (y * width + x) * 4 + channel;
let delta = (f32::from(actual[index]) - expected[index]).abs();
if delta > worst.0 {
worst = (delta, (x, y));
}
}
}
}
worst
}
const BLURRED_SUBSTRATE_BUDGET: f32 = 14.0;
const BLURRED_SUBSTRATE_EDGE_BUDGET: f32 = 36.0;
#[test]
fn transformed_child_receives_its_shared_mean_after_blur() {
let mut renderer = support::headless_renderer().expect("mean substrate requires GPU");
let mut glass = effect_layer(
GLASS,
RenderEffect::blur(6.0).then(support::substrate_probe(
SubstrateSpec::Mean,
SubstrateProbeRead::Held,
)),
);
if let RenderNode::Layer(layer) = &mut glass {
layer.transform_to_parent = ProjectiveTransform::uniform_scale(1.05)
.then(ProjectiveTransform::translation(GLASS.x, GLASS.y));
}
let graph = support::page_graph(
FRAME_WIDTH,
FRAME_HEIGHT,
vec![
support::solid_rect(
rect(0.0, 0.0, FRAME_WIDTH as f32, FRAME_HEIGHT as f32),
cranpose_ui_graphics::Color(0.35, 0.35, 0.35, 1.0),
),
glass,
],
);
let frame = capture(&mut renderer, graph);
for pixel in region_pixels(&frame, rect(65.0, 45.0, 30.0, 25.0))
.as_chunks::<4>()
.0
{
assert!(
pixel[..3].iter().all(|c| c.abs_diff(89) <= 1),
"transformed child must receive its mean: {pixel:?}"
);
}
}
#[test]
fn shared_mean_uses_the_layer_extent_without_filter_padding() {
let mut renderer = support::headless_renderer().expect("mean substrate requires GPU");
let graph = support::page_graph(
FRAME_WIDTH,
FRAME_HEIGHT,
vec![
support::solid_rect(
rect(0.0, 0.0, FRAME_WIDTH as f32, FRAME_HEIGHT as f32),
cranpose_ui_graphics::Color::BLACK,
),
support::solid_rect(GLASS, cranpose_ui_graphics::Color(0.5, 0.5, 0.5, 1.0)),
effect_layer(
GLASS,
RenderEffect::blur(6.0).then(support::substrate_probe(
SubstrateSpec::Mean,
SubstrateProbeRead::Held,
)),
),
],
);
let frame = capture(&mut renderer, graph);
for pixel in region_pixels(&frame, rect(65.0, 45.0, 30.0, 25.0))
.as_chunks::<4>()
.0
{
assert!(
pixel[..3].iter().all(|c| c.abs_diff(128) <= 1),
"padding must not bias the mean: {pixel:?}"
);
}
}
#[test]
fn shared_mean_includes_every_texel_of_each_odd_sized_capture() {
let mut renderer = support::headless_renderer().expect("mean substrate requires GPU");
let regions = [rect(7.0, 5.0, 73.0, 51.0), rect(137.0, 11.0, 59.0, 83.0)];
let plain = capture(&mut renderer, page(None));
let mut children = support::striped_page(FRAME_WIDTH, FRAME_HEIGHT);
for region in regions {
children.push(effect_layer(
region,
support::substrate_probe(SubstrateSpec::Mean, SubstrateProbeRead::Held),
));
}
let actual = capture(
&mut renderer,
support::page_graph(FRAME_WIDTH, FRAME_HEIGHT, children),
);
for region in regions {
let source = region_pixels(&plain, region);
let mut mean = [0.0; 4];
for pixel in source.as_chunks::<4>().0 {
for channel in 0..4 {
mean[channel] += f64::from(pixel[channel]);
}
}
for value in &mut mean {
*value /= (source.len() / 4) as f64;
}
for pixel in region_pixels(&actual, region).as_chunks::<4>().0 {
for channel in 0..4 {
assert!(
(f64::from(pixel[channel]) - mean[channel]).abs() <= 1.0,
"shared mean for {region:?}: {pixel:?}, expected {mean:?}"
);
}
}
}
}
#[test]
fn a_blurred_substrate_is_the_capture_blurred_by_its_radius() {
let Ok(mut renderer) = support::headless_renderer() else {
eprintln!("skipping (headless WGPU init failed)");
return;
};
let plain = capture(&mut renderer, page(None));
let probe = capture(
&mut renderer,
page(Some(effect_layer(
GLASS,
support::substrate_probe(
SubstrateSpec::Blur {
radius_px: SUBSTRATE_RADIUS,
},
SubstrateProbeRead::Held,
),
))),
);
let expected = reference_blur_of(&plain, GLASS, SUBSTRATE_RADIUS);
let height = GLASS.height as usize;
let interior = worst_deviation(&probe, GLASS, &expected, 4..height - 4, 4);
let edge = worst_deviation(&probe, GLASS, &expected, 0..height, 0);
assert!(
interior.0 <= BLURRED_SUBSTRATE_BUDGET && edge.0 <= BLURRED_SUBSTRATE_EDGE_BUDGET,
"the substrate blurred by {SUBSTRATE_RADIUS} diverges from the kernel by {} at {:?} \
inside and by {} at {:?} at its edge",
interior.0,
interior.1,
edge.0,
edge.1
);
}
fn band() -> Rect {
rect(0.0, BAND_TOP, FRAME_WIDTH as f32, BAND_HEIGHT)
}
fn band_capture() -> Rect {
let reach = WIDE_RADIUS.ceil();
rect(
0.0,
BAND_TOP - reach,
FRAME_WIDTH as f32,
BAND_HEIGHT + 2.0 * reach,
)
}
fn gradient_band(renderer: &mut support::LockedRenderer) -> CapturedFrame {
capture(
renderer,
page(Some(effect_layer(
band(),
gradient_blur_effect(WIDE_RADIUS, 0.0, GradientBlurDirection::TopToBottom),
))),
)
}
#[test]
fn a_gradient_blur_realises_its_wide_radius_as_the_wide_level() {
let Ok(mut renderer) = support::headless_renderer() else {
eprintln!("skipping (headless WGPU init failed)");
return;
};
let plain = capture(&mut renderer, page(None));
let frame = gradient_band(&mut renderer);
let capture_rect = band_capture();
let expected = reference_blur_of(&plain, capture_rect, WIDE_RADIUS);
let top_row = (BAND_TOP - capture_rect.y) as usize + 1;
let (worst, at) = worst_deviation(
&frame,
capture_rect,
&expected,
top_row..top_row + 1,
WIDE_RADIUS as usize + 2,
);
assert!(
worst <= LEVEL_ROW_BUDGET,
"the band's top row diverges from the radius-{WIDE_RADIUS} kernel by {worst} at {at:?}"
);
}
fn band_row_for(share: f32) -> usize {
let progress = 1.0 - share;
let mut low = 0.0f32;
let mut high = 1.0f32;
for _ in 0..40 {
let mid = (low + high) * 0.5;
if mid * mid * (3.0 - 2.0 * mid) < progress {
low = mid;
} else {
high = mid;
}
}
let local = (low + high) * 0.5;
(BAND_TOP + BAND_HEIGHT * local - band_capture().y).round() as usize
}
fn band_row_deviation(frame: &CapturedFrame, plain: &CapturedFrame, share: f32) -> f32 {
let capture_rect = band_capture();
let expected = reference_blur_of(plain, capture_rect, WIDE_RADIUS * share);
let row = band_row_for(share);
worst_deviation(
frame,
capture_rect,
&expected,
row..row + 1,
WIDE_RADIUS as usize + 2,
)
.0
}
const LEVEL_ROW_BUDGET: f32 = 7.0;
const BLENDED_LEVEL_BUDGET: f32 = 14.0;
#[test]
fn a_gradient_blur_blends_its_levels_between_sharp_and_wide() {
let Ok(mut renderer) = support::headless_renderer() else {
eprintln!("skipping (headless WGPU init failed)");
return;
};
let plain = capture(&mut renderer, page(None));
let frame = gradient_band(&mut renderer);
let bottom = rect(0.0, BAND_TOP + BAND_HEIGHT - 2.0, FRAME_WIDTH as f32, 1.0);
assert_eq!(
region_pixels(&frame, bottom),
region_pixels(&plain, bottom),
"the band's bottom row asks for no blur and shows the page"
);
let rows = [
(0.5, LEVEL_ROW_BUDGET),
(0.75, BLENDED_LEVEL_BUDGET),
(0.375, BLENDED_LEVEL_BUDGET),
];
let deviations: Vec<(f32, f32, f32)> = rows
.iter()
.map(|(share, budget)| (*share, band_row_deviation(&frame, &plain, *share), *budget))
.collect();
assert!(
deviations
.iter()
.all(|(_, deviation, budget)| deviation <= budget),
"the band's rows diverge from the kernels at their radii (share of the wide radius, \
deviation, budget): {deviations:?}"
);
}