use cranpose_render_common::{
Renderer,
graph::{CachePolicy, DrawRunNode, LayerNode, PrimitivePhase, RenderGraph, RenderNode},
};
use cranpose_render_wgpu::RenderStatsSnapshot;
use cranpose_ui_graphics::{
Brush, Color, DrawScope, DrawScopeDefault, Point, Rect, Size, Stroke, StrokeCap, StrokeJoin,
};
use support::SIZE;
use crate::support;
const ROOT_SCALE: f32 = 1.25;
fn graph(record: impl FnOnce(&mut DrawScopeDefault)) -> RenderGraph {
let mut scope = DrawScopeDefault::new(Size::new(SIZE as f32, SIZE as f32));
record(&mut scope);
RenderGraph::new(LayerNode {
cache_policy: CachePolicy::None,
local_bounds: Rect {
x: 11.25,
y: 7.5,
width: 183.0,
height: 171.0,
},
clip_to_bounds: true,
children: vec![RenderNode::DrawRun(DrawRunNode::new(
PrimitivePhase::BeforeChildren,
scope.into_primitives(),
))],
..LayerNode::default()
})
}
fn solid_shapes(scope: &mut DrawScopeDefault) {
support::record_solid_scene(scope);
let brush = Brush::solid(Color(0.95, 0.6, 0.2, 1.0));
for (index, cap) in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square]
.into_iter()
.enumerate()
{
let x = 70.0 + index as f32 * 17.0;
scope.draw_line(
brush.clone(),
Point::new(x, 60.0),
Point::new(x + 30.0, 150.0),
Stroke {
width: 1.0 + index as f32 * 2.5,
cap,
join: StrokeJoin::Miter,
},
);
}
let ring = Stroke {
width: 2.5,
..Stroke::default()
};
scope.draw_circle_stroked(brush.clone(), Point::new(170.0, 160.0), 30.0, ring);
scope.draw_arc(brush, Point::new(60.0, 175.0), 24.0, 0.4, 3.9, ring);
}
fn first_and_landed(graph: &RenderGraph) -> (Vec<u8>, RenderStatsSnapshot, Vec<u8>) {
let mut renderer = support::headless_renderer_compiling_in_background().expect("GPU required");
support::wait_for_background_compiler_idle();
let first = capture(&mut renderer, graph);
let stats = renderer.last_frame_stats().expect("frame statistics");
let landed = support::capture_settled(&mut renderer, |renderer| {
renderer.scene_mut().graph = Some(graph.clone());
renderer
.capture_frame_with_scale(SIZE, SIZE, ROOT_SCALE)
.expect("capture")
});
(first, stats, landed.pixels)
}
fn capture(renderer: &mut support::LockedRenderer, graph: &RenderGraph) -> Vec<u8> {
renderer.scene_mut().graph = Some(graph.clone());
renderer
.capture_frame_with_scale(SIZE, SIZE, ROOT_SCALE)
.expect("capture")
.pixels
}
const INSIDE: u8 = 32;
const EDGE: u8 = 160;
fn differing_inside(first: &[u8], landed: &[u8]) -> Vec<(usize, usize)> {
let side = SIZE as usize;
let pixel = |pixels: &[u8], x: usize, y: usize| {
let at = (y * side + x) * 4;
[pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]]
};
let apart = |a: [u8; 4], b: [u8; 4]| a.iter().zip(b).any(|(a, b)| a.abs_diff(b) > INSIDE);
let mut inside = Vec::new();
for y in 0..side {
for x in 0..side {
let drawn = pixel(landed, x, y);
if !apart(pixel(first, x, y), drawn) {
continue;
}
let on_edge = (y.saturating_sub(1)..(y + 2).min(side)).any(|ny| {
(x.saturating_sub(1)..(x + 2).min(side))
.any(|nx| apart(pixel(landed, nx, ny), drawn))
});
if !on_edge {
inside.push((x, y));
}
}
}
inside
}
fn assert_fallback_matches(record: impl FnOnce(&mut DrawScopeDefault)) {
let (first, stats, landed) = first_and_landed(&graph(record));
assert!(
stats.shape_pipeline_fallback_draws > 0,
"the first frame must draw shapes while their pipelines compile: {stats:?}"
);
assert_eq!(stats.placeholder_draws, 0, "no shape is left out");
let inside = differing_inside(&first, &landed);
assert!(
inside.is_empty(),
"the fallback differs away from the edges at {inside:?}"
);
let moved = support::pixels_differing_beyond(SIZE, &first, &landed, EDGE);
assert!(
moved.is_empty(),
"edges moved: {}",
support::describe_differing(&moved)
);
}
#[test]
fn a_first_frame_draws_solid_shapes_as_their_pipelines_do() {
assert_fallback_matches(solid_shapes);
}
#[test]
fn a_first_frame_draws_gradients_with_their_colours() {
assert_fallback_matches(support::record_mixed_scene);
}