use cranpose_core::NodeId;
use cranpose_render_common::{
Renderer,
graph::{CachePolicy, LayerNode, ProjectiveTransform, RenderGraph, RenderNode},
};
use cranpose_render_wgpu::{CancelReason, PresentOutcome};
use cranpose_ui_graphics::{Color, Rect};
use crate::support;
const WIDTH: u32 = 128;
const HEIGHT: u32 = 96;
fn test_layer(node_id: Option<NodeId>, children: Vec<RenderNode>) -> LayerNode {
support::contract_layer(
node_id,
CachePolicy::None,
Rect {
x: 0.0,
y: 0.0,
width: WIDTH as f32,
height: HEIGHT as f32,
},
ProjectiveTransform::identity(),
children,
)
}
fn direct_graph() -> RenderGraph {
RenderGraph::new(test_layer(
Some(7_100),
vec![support::solid_rect(
Rect {
x: 16.0,
y: 12.0,
width: 64.0,
height: 48.0,
},
Color(0.2, 0.7, 0.3, 1.0),
)],
))
}
fn nested_isolated_graph() -> RenderGraph {
let mut child = support::contract_layer(
Some(7_101),
CachePolicy::None,
Rect {
x: 0.0,
y: 0.0,
width: 96.0,
height: 72.0,
},
ProjectiveTransform::identity(),
vec![support::solid_rect(
Rect {
x: 12.0,
y: 10.0,
width: 64.0,
height: 48.0,
},
Color(0.8, 0.2, 0.2, 1.0),
)],
);
child.graphics_layer.alpha = 0.5;
RenderGraph::new(test_layer(
Some(7_100),
vec![RenderNode::Layer(Box::new(child))],
))
}
fn target_view(
renderer: &support::LockedRenderer,
width: u32,
height: u32,
) -> (wgpu::Texture, wgpu::TextureView) {
let device = renderer
.try_device()
.expect("renderer GPU device was not initialized");
support::render_target(
device,
width,
height,
wgpu::TextureFormat::Bgra8UnormSrgb,
wgpu::TextureUsages::RENDER_ATTACHMENT,
)
}
#[test]
fn renderer_replacement_cancels_in_flight_packet() {
let mut renderer = match support::headless_renderer() {
Ok(renderer) => renderer,
Err(err) => {
eprintln!("skipping renderer-epoch cancel: headless WGPU init failed: {err}");
return;
}
};
renderer.scene_mut().graph = Some(direct_graph());
let packet = renderer
.build_frame_packet_for_tests(WIDTH, HEIGHT)
.expect("direct graph must lower into a packet");
support::reinit_gpu(&mut renderer).expect("GPU reinit failed");
let (texture, view) = target_view(&renderer, WIDTH, HEIGHT);
let outcome = renderer
.render_held_packet_for_tests(&texture, &view, WIDTH, HEIGHT, packet)
.expect("a cancel is a protocol outcome, not a draw error");
assert_eq!(
outcome,
PresentOutcome::Cancelled(CancelReason::RendererEpoch),
"a packet built against the dead renderer must cancel, not draw"
);
let packet = renderer
.build_frame_packet_for_tests(WIDTH, HEIGHT)
.expect("the next build must lower normally");
let outcome = renderer
.render_held_packet_for_tests(&texture, &view, WIDTH, HEIGHT, packet)
.expect("the post-replacement packet must draw");
assert_eq!(
outcome,
PresentOutcome::Presented,
"a packet built against the NEW renderer must present"
);
}
#[test]
fn surface_reconfigure_cancels_waiting_packet() {
let mut renderer = match support::headless_renderer() {
Ok(renderer) => renderer,
Err(err) => {
eprintln!("skipping surface-epoch cancel: headless WGPU init failed: {err}");
return;
}
};
renderer.scene_mut().graph = Some(direct_graph());
let packet = renderer
.build_frame_packet_for_tests(WIDTH, HEIGHT)
.expect("direct graph must lower into a packet");
renderer.note_surface_reconfigured();
let (texture, view) = target_view(&renderer, WIDTH, HEIGHT);
let outcome = renderer
.render_held_packet_for_tests(&texture, &view, WIDTH, HEIGHT, packet)
.expect("a cancel is a protocol outcome, not a draw error");
assert_eq!(
outcome,
PresentOutcome::Cancelled(CancelReason::SurfaceEpoch),
"a packet straddling a surface reconfigure must cancel, not draw"
);
let packet = renderer
.build_frame_packet_for_tests(WIDTH, HEIGHT)
.expect("the next build must lower normally");
let outcome = renderer
.render_held_packet_for_tests(&texture, &view, WIDTH, HEIGHT, packet)
.expect("the post-reconfigure packet must draw");
assert_eq!(outcome, PresentOutcome::Presented);
}
fn cancel_at_different_viewport(renderer: &mut support::LockedRenderer, graph: RenderGraph) {
renderer.scene_mut().graph = Some(graph);
let packet = renderer
.build_frame_packet_for_tests(WIDTH, HEIGHT)
.expect("graph must lower into a packet");
let (texture, view) = target_view(renderer, WIDTH / 2, HEIGHT / 2);
let outcome = renderer
.render_held_packet_for_tests(&texture, &view, WIDTH / 2, HEIGHT / 2, packet)
.expect("a cancel is a protocol outcome, not a draw error");
assert_eq!(outcome, PresentOutcome::Cancelled(CancelReason::Viewport));
}
#[test]
fn viewport_mismatch_cancels_packet() {
let mut renderer = match support::headless_renderer() {
Ok(renderer) => renderer,
Err(err) => {
eprintln!("skipping viewport cancel: headless WGPU init failed: {err}");
return;
}
};
cancel_at_different_viewport(&mut renderer, direct_graph());
}
#[test]
fn cancelled_nested_scene_renders_the_same_on_the_next_frame() {
let mut renderer = match support::headless_renderer() {
Ok(renderer) => renderer,
Err(err) => {
eprintln!("skipping nested-scene cancel: headless WGPU init failed: {err}");
return;
}
};
let reference =
support::present_and_read(&mut renderer, WIDTH, HEIGHT, nested_isolated_graph());
cancel_at_different_viewport(&mut renderer, nested_isolated_graph());
let after_cancel =
support::present_and_read(&mut renderer, WIDTH, HEIGHT, nested_isolated_graph());
support::assert_same_bytes(
"nested isolated scene after a cancelled packet",
WIDTH,
&after_cancel,
&reference,
);
}
fn stored_run_graph(count: usize, color: Color) -> RenderGraph {
support::stored_run_graph(WIDTH, HEIGHT, &vec![color; count])
}
#[test]
fn a_cancelled_packet_leaves_the_run_store_at_its_last_upload() {
let mut renderer = match support::headless_renderer() {
Ok(renderer) => renderer,
Err(err) => {
eprintln!("skipping run store cancel: headless WGPU init failed: {err}");
return;
}
};
let green = Color(0.2, 0.7, 0.3, 1.0);
let red = Color(0.8, 0.2, 0.2, 1.0);
let first =
support::present_and_read(&mut renderer, WIDTH, HEIGHT, stored_run_graph(96, green));
cancel_at_different_viewport(&mut renderer, stored_run_graph(96, red));
let after_cancel =
support::present_and_read(&mut renderer, WIDTH, HEIGHT, stored_run_graph(96, red));
let reference =
support::present_and_read(&mut renderer, WIDTH, HEIGHT, stored_run_graph(96, red));
assert_ne!(
first, after_cancel,
"the red command must draw red, not the green tables the store retained"
);
support::assert_same_bytes("after a cancelled packet", WIDTH, &after_cancel, &reference);
}