mod support;
#[path = "../src/test_support.rs"]
mod shared_test_support;
use cranpose_render_common::graph::{ProjectiveTransform, RenderGraph, RenderNode};
use cranpose_ui_graphics::{Color, GraphicsLayer, Rect, RenderEffect};
use support::{ReferenceEdge, region_pixels, solid_rect};
const FRAME: u32 = 160;
const RADIUS: f32 = 4.0;
const WIDE_RADIUS: f32 = 20.0;
const GLASS: Rect = Rect {
x: 40.0,
y: 40.0,
width: 80.0,
height: 80.0,
};
fn rect(x: f32, y: f32, width: f32, height: f32) -> Rect {
Rect {
x,
y,
width,
height,
}
}
fn page(blur: Option<f32>) -> RenderGraph {
let mut children = vec![
solid_rect(
rect(0.0, 0.0, FRAME as f32, FRAME as f32),
Color::from_rgb_u8(30, 40, 60),
),
solid_rect(
rect(50.0, 0.0, 9.0, FRAME as f32),
Color::from_rgb_u8(240, 80, 40),
),
solid_rect(
rect(0.0, 70.0, FRAME as f32, 7.0),
Color::from_rgb_u8(60, 220, 90),
),
solid_rect(
rect(80.0, 90.0, 25.0, 13.0),
Color::from_rgb_u8(250, 240, 120),
),
solid_rect(
rect(95.0, 45.0, 3.0, 40.0),
Color::from_rgb_u8(255, 255, 255),
),
];
if let Some(radius) = blur {
children.push(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 {
backdrop_effect: Some(RenderEffect::blur(radius)),
..GraphicsLayer::default()
},
Vec::new(),
),
)));
}
support::page_graph(FRAME, FRAME, children)
}
struct KernelDeviation {
worst: f32,
worst_at: (usize, usize, usize),
changed: usize,
channels: usize,
}
fn worst_kernel_deviation(radius: f32, density: f32) -> Option<KernelDeviation> {
let Ok(mut renderer) = support::headless_renderer() else {
eprintln!("skipping (headless WGPU init failed)");
return None;
};
let frame = (FRAME as f32 * density) as u32;
let plain = support::capture_graph_with_scale(&mut renderer, page(None), frame, frame, density);
let blurred =
support::capture_graph_with_scale(&mut renderer, page(Some(radius)), frame, frame, density);
let plain_values: Vec<f32> = plain.pixels.iter().map(|value| f32::from(*value)).collect();
let expected = support::reference_blur(
&plain_values,
frame as usize,
frame as usize,
4,
radius,
ReferenceEdge::Clamp,
);
let inside = rect(
(GLASS.x + 2.0) * density,
(GLASS.y + 2.0) * density,
(GLASS.width - 4.0) * density,
(GLASS.height - 4.0) * density,
);
let actual = region_pixels(&blurred, inside);
let mut worst = 0.0f32;
let mut worst_at = (0, 0, 0);
let mut changed = 0usize;
for (index, value) in actual.iter().enumerate() {
let x = inside.x as usize + index / 4 % inside.width as usize;
let y = inside.y as usize + index / 4 / inside.width as usize;
let c = index % 4;
let want = expected[(y * frame as usize + x) * 4 + c];
let delta = (*value as f32 - want).abs();
if delta > worst {
worst = delta;
worst_at = (x, y, c);
}
if *value != plain.pixels[(y * frame as usize + x) * 4 + c] {
changed += 1;
}
}
Some(KernelDeviation {
worst,
worst_at,
changed,
channels: actual.len(),
})
}
fn assert_blur_follows_its_kernel(radius: f32, budget: f32) {
let Some(KernelDeviation {
worst,
worst_at,
changed,
channels,
}) = worst_kernel_deviation(radius, 1.0)
else {
return;
};
assert!(
changed > channels / 4,
"the blur must change most pixels under the glass"
);
assert!(
worst <= budget,
"the radius-{radius} blur diverges from its kernel by {worst} at {worst_at:?}; every \
weight and tap offset must reproduce the kernel within {budget}"
);
}
#[test]
fn a_blur_matches_its_kernel_applied_by_the_cpu_within_one_step() {
assert_blur_follows_its_kernel(RADIUS, 1.0);
}
const DOWNSCALE_BUDGET: f32 = 8.0;
#[test]
fn a_wide_blur_matches_its_kernel_within_the_downscale_budget() {
assert_blur_follows_its_kernel(WIDE_RADIUS, DOWNSCALE_BUDGET);
}
#[test]
fn a_wide_blur_runs_both_passes_at_the_scratch_size() {
let Ok(mut renderer) = support::headless_renderer() else {
eprintln!("skipping (headless WGPU init failed)");
return;
};
let pass_pixels = |renderer: &mut support::LockedRenderer, radius: f32| {
support::capture_graph(renderer, page(Some(radius)), FRAME, FRAME);
renderer.last_frame_stats().expect("stats").pass_pixels
};
let narrow = pass_pixels(&mut renderer, RADIUS);
let wide = pass_pixels(&mut renderer, WIDE_RADIUS);
let wide_capture = (GLASS.width + 2.0 * WIDE_RADIUS) * (GLASS.height + 2.0 * WIDE_RADIUS);
let saved = narrow.saturating_sub(wide);
assert!(
saved as f32 > wide_capture / 2.0,
"the wide blur must run both passes at the scratch size: narrow={narrow} wide={wide} \
saved={saved} wide capture={wide_capture}"
);
}
#[test]
fn backdrop_blur_radius_is_already_in_pixels_at_high_display_density() {
for density in [2.0, 3.0] {
let result =
worst_kernel_deviation(RADIUS, density).expect("density regression requires a GPU");
assert!(result.changed > 0);
assert!(
result.worst <= 1.0,
"density {density} multiplied an already physical radius: deviation {} at {:?}",
result.worst,
result.worst_at
);
}
}