use cranpose_render_common::{
Renderer,
graph::{ProjectiveTransform, RenderGraph, RenderNode},
};
use cranpose_render_wgpu::CapturedFrame;
use cranpose_ui_graphics::{
Color, GraphicsLayer, PlaceholderShape, RUNTIME_SHADER_PRELUDE_WGSL, Rect, RenderEffect,
RuntimeShader, ShaderPlaceholder,
};
use crate::{shared_test_support, support};
const SIZE: u32 = 32;
const PANE: Rect = Rect {
x: 8.0,
y: 8.0,
width: 16.0,
height: 16.0,
};
const RED: ShaderPlaceholder = ShaderPlaceholder {
color: Color(1.0, 0.0, 0.0, 1.0),
shape: None,
};
fn shader(tag: &str, placeholder: Option<ShaderPlaceholder>) -> RuntimeShader {
let mut shader = RuntimeShader::new(&format!(
"{RUNTIME_SHADER_PRELUDE_WGSL}
// {tag} {}
@fragment fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {{
return vec4<f32>(0.0, 0.0, 1.0, 1.0);
}}",
std::process::id()
));
shader.set_placeholder(placeholder);
shader
}
fn page(layer: GraphicsLayer, content: Vec<RenderNode>) -> RenderGraph {
let bounds = Rect {
x: 0.0,
y: 0.0,
width: PANE.width,
height: PANE.height,
};
page_with(
bounds,
ProjectiveTransform::translation(PANE.x, PANE.y),
layer,
content,
)
}
fn page_with(
bounds: Rect,
transform: ProjectiveTransform,
layer: GraphicsLayer,
content: Vec<RenderNode>,
) -> RenderGraph {
let full = Rect {
x: 0.0,
y: 0.0,
width: SIZE as f32,
height: SIZE as f32,
};
let pane = shared_test_support::layer_node(bounds, transform, layer, content);
support::page_graph(
SIZE,
SIZE,
vec![
support::solid_rect(full, Color::WHITE),
RenderNode::Layer(Box::new(pane)),
],
)
}
fn backdrop_page(shader: RuntimeShader) -> RenderGraph {
page(
GraphicsLayer {
backdrop_effect: Some(RenderEffect::runtime_shader(shader)),
..GraphicsLayer::default()
},
Vec::new(),
)
}
fn effect_page(effect: Option<RenderEffect>) -> RenderGraph {
effect_page_with(GraphicsLayer::default(), effect)
}
fn effect_page_with(layer: GraphicsLayer, effect: Option<RenderEffect>) -> RenderGraph {
let square = support::solid_rect(
Rect {
x: 0.0,
y: 0.0,
width: PANE.width,
height: PANE.height,
},
Color(0.0, 1.0, 0.0, 1.0),
);
page(
GraphicsLayer {
render_effect: effect,
..layer
},
vec![square],
)
}
fn settled(graph: RenderGraph) -> CapturedFrame {
let mut reference = support::LockedRenderer::beside_locked().expect("reference renderer");
support::capture_graph(&mut reference, graph, SIZE, SIZE)
}
fn first_frame(graph: &RenderGraph) -> (support::LockedRenderer, CapturedFrame) {
let mut renderer = support::headless_renderer_compiling_in_background().expect("GPU required");
let frame = support::capture_graph(&mut renderer, graph.clone(), SIZE, SIZE);
let stats = renderer.last_frame_stats().expect("frame statistics");
assert_eq!(
stats.placeholder_draws, 1,
"the effect must wait for its pipeline"
);
assert!(
renderer.needs_frame_warmup(),
"a frame drawn without an effect asks for the frame that replaces it"
);
(renderer, frame)
}
fn assert_lands(renderer: &mut support::LockedRenderer, graph: RenderGraph) {
let (drawn, _) = support::capture_drawn(renderer, &graph, SIZE, SIZE);
support::assert_same_bytes(
"the frame after the pipeline lands",
SIZE,
&drawn.pixels,
&settled(graph.clone()).pixels,
);
for _ in 0..4 {
if !renderer.needs_frame_warmup() {
return;
}
support::capture_graph(renderer, graph.clone(), SIZE, SIZE);
}
panic!("the renderer kept asking for frames after the effect drew");
}
fn pane_colors(frame: &CapturedFrame) -> Vec<[u8; 4]> {
let mut colors = support::region_pixels(frame, PANE)
.as_chunks::<4>()
.0
.to_vec();
colors.sort_unstable();
colors.dedup();
colors
}
#[test]
fn a_backdrop_fills_its_shape_with_its_placeholder_until_its_pipeline_lands() {
let graph = backdrop_page(shader("placeholder backdrop", Some(RED)));
let (mut renderer, first) = first_frame(&graph);
assert_eq!(pane_colors(&first), [[255, 0, 0, 255]]);
let without_pane = settled(support::page_graph(
SIZE,
SIZE,
vec![support::solid_rect(
Rect {
x: 0.0,
y: 0.0,
width: SIZE as f32,
height: SIZE as f32,
},
Color::WHITE,
)],
));
let outside = |frame: &CapturedFrame| {
let mut pixels = frame.pixels.clone();
for y in PANE.y as u32..(PANE.y + PANE.height) as u32 {
let start = ((y * SIZE + PANE.x as u32) * 4) as usize;
pixels[start..start + (PANE.width as usize) * 4].fill(0);
}
pixels
};
assert_eq!(
outside(&first),
outside(&without_pane),
"the placeholder stays inside its shape"
);
assert_lands(&mut renderer, graph);
}
#[test]
fn a_backdrop_without_a_placeholder_leaves_its_shape_to_what_is_beneath() {
let graph = backdrop_page(shader("bare backdrop", None));
let (mut renderer, first) = first_frame(&graph);
assert_eq!(pane_colors(&first), [[255, 255, 255, 255]]);
assert_lands(&mut renderer, graph);
}
#[test]
fn a_layer_effect_leaves_the_content_as_it_is_until_its_pipeline_lands() {
let effect = RenderEffect::runtime_shader(shader("layer effect", Some(RED)));
let graph = effect_page(Some(effect));
let (mut renderer, first) = first_frame(&graph);
support::assert_same_bytes(
"the content without its effect",
SIZE,
&first.pixels,
&settled(effect_page(None)).pixels,
);
assert_lands(&mut renderer, graph);
}
fn shader_only(tag: &str) -> GraphicsLayer {
let mut only = shader(tag, Some(RED));
only.set_position_independent(true);
GraphicsLayer {
render_effect: Some(RenderEffect::runtime_shader(only)),
..GraphicsLayer::default()
}
}
fn assert_placeholder_then_lands(graph: RenderGraph) {
let (mut renderer, first) = first_frame(&graph);
assert_eq!(pane_colors(&first), [[255, 0, 0, 255]]);
assert_lands(&mut renderer, graph);
}
#[test]
fn a_layer_drawn_only_by_its_shader_shows_the_placeholder_until_its_pipeline_lands() {
assert_placeholder_then_lands(page(shader_only("shader-only layer"), Vec::new()));
}
const MIDDLE: ShaderPlaceholder = ShaderPlaceholder {
shape: Some(PlaceholderShape {
bounds: Rect {
x: 0.25,
y: 0.25,
width: 0.5,
height: 0.5,
},
corner_radius: PANE.width * 0.25,
}),
..RED
};
fn assert_in_middle(frame: &CapturedFrame, inside: [u8; 4]) {
let pixel = |x: f32, y: f32| {
let start = (((PANE.y + y) as u32 * SIZE + (PANE.x + x) as u32) * 4) as usize;
[0, 1, 2, 3].map(|channel| frame.pixels[start + channel])
};
assert_eq!(pixel(8.0, 8.0), inside, "the shape's middle");
assert_eq!(pixel(2.0, 2.0), [255; 4], "outside the shape");
assert_eq!(pixel(4.0, 4.0), [255; 4], "the shape's rounded corner");
}
#[test]
fn a_placeholder_with_a_shape_fills_only_that_rounded_rectangle() {
let graph = backdrop_page(shader("shaped placeholder", Some(MIDDLE)));
let (mut renderer, first) = first_frame(&graph);
assert_in_middle(&first, [255, 0, 0, 255]);
assert_lands(&mut renderer, graph);
}
#[test]
fn a_layer_effect_with_a_shaped_placeholder_keeps_the_content_in_that_shape() {
let effect = RenderEffect::runtime_shader(shader("shaped layer effect", Some(MIDDLE)));
let graph = effect_page(Some(effect));
let (mut renderer, first) = first_frame(&graph);
assert_in_middle(&first, [0, 255, 0, 255]);
assert_lands(&mut renderer, graph);
}
#[test]
fn a_shadow_is_left_out_until_its_blur_lands() {
let square = || {
support::solid_rect(
Rect {
x: 0.0,
y: 0.0,
width: PANE.width,
height: PANE.height,
},
Color(0.0, 1.0, 0.0, 1.0),
)
};
let shadow = support::drop_shadow(
Rect {
x: 6.0,
y: 6.0,
width: PANE.width,
height: PANE.height,
},
Color::BLACK,
6.0,
);
let graph = page(GraphicsLayer::default(), vec![shadow, square()]);
let mut renderer = support::headless_renderer_compiling_in_background().expect("GPU required");
let first = support::capture_graph(&mut renderer, graph.clone(), SIZE, SIZE);
let stats = renderer.last_frame_stats().expect("frame statistics");
assert!(
stats.placeholder_draws > 0,
"the shadow must wait for its blur"
);
assert!(renderer.needs_frame_warmup());
support::assert_same_bytes(
"the page without its shadow",
SIZE,
&first.pixels,
&settled(page(GraphicsLayer::default(), vec![square()])).pixels,
);
assert_lands(&mut renderer, graph);
}
#[test]
fn a_frame_with_placeholders_awaits_their_pipelines_until_they_land() {
let graph = backdrop_page(shader("awaited backdrop", Some(RED)));
let (mut renderer, _) = first_frame(&graph);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while renderer.awaits_pipelines() {
assert!(
std::time::Instant::now() < deadline,
"the pipelines never landed"
);
std::thread::sleep(std::time::Duration::from_millis(2));
}
support::capture_graph(&mut renderer, graph, SIZE, SIZE);
let stats = renderer.last_frame_stats().expect("frame statistics");
assert_eq!(stats.placeholder_draws, 0);
assert!(!renderer.awaits_pipelines());
}
#[test]
fn a_scaled_layer_drawn_only_by_its_shader_shows_the_placeholder() {
assert_placeholder_then_lands(page_with(
Rect {
x: 0.0,
y: 0.0,
width: PANE.width * 0.5,
height: PANE.height * 0.5,
},
ProjectiveTransform::uniform_scale(2.0)
.then(ProjectiveTransform::translation(PANE.x, PANE.y)),
shader_only("scaled shader-only layer"),
Vec::new(),
));
}