facett-core/tests/cloud_parity.rs
//! **The cloud lane must draw the picture the instance lane draws.**
//!
//! The instance lane projects every node on the CPU and re-sends a screen-space
//! instance array every frame. The cloud lane uploads model-space points once and
//! sends 48 bytes of camera. They share the fragment shaders, the additive blend and
//! the whole bloom chain — the only difference is *where* the projection happens, so
//! the two frames must agree.
//!
//! This is the guard that makes the cloud lane trustworthy, so it is written to be
//! able to FAIL: it compares real pixels, it asserts the frames are non-blank first
//! (two black images agree perfectly and prove nothing), and it includes a control
//! that changes the camera and requires the comparison to notice.
//!
//! Skips when no GPU turn is available (`render_*_offscreen` return `None`) — the same
//! contract every other GPU pixel test in this crate follows.
#![cfg(feature = "wgpu")]
use facett_core::render::gpu::{
render_cloud_offscreen, render_graph_offscreen, BloomParams, CloudCamera, CloudEdge,
CloudPoint, EdgeInstance, GraphScene, NodeInstance,
};
/// A deterministic little cloud: points on a unit sphere, coloured by index, wired in a
/// ring so there are real edges to draw.
fn model(n: usize) -> (Vec<CloudPoint>, Vec<CloudEdge>) {
let golden = 2.399_963_2_f32;
let pts: Vec<CloudPoint> = (0..n)
.map(|i| {
let m = n as f32;
let y = 1.0 - 2.0 * ((i as f32) + 0.5) / m;
let r = (1.0 - y * y).max(0.0).sqrt();
let th = golden * i as f32;
let c = egui::Color32::from_rgb((40 + i * 7 % 200) as u8, (90 + i * 13 % 150) as u8, 220);
CloudPoint::new([r * th.cos(), y, r * th.sin()], c)
})
.collect();
let edges: Vec<CloudEdge> = (0..n).map(|i| [i as u32, ((i + 1) % n) as u32]).collect();
(pts, edges)
}
/// The CPU-side build the instance lane does — the exact arithmetic in
/// `facett_graph3d::Graph3D::gpu_scene`, restated here because facett-core cannot depend
/// on facett-graph3d. If this drifts from that, the parity claim is about the wrong
/// thing, so it is written from the same constants: radius `3 + (z+1)*2.5`, depth
/// brightness `clamp(0.55 + (depth+1)*0.35, 0.4, 1.3)`, edges at 0.6x and 1.5 px.
fn instance_scene(
pts: &[CloudPoint],
edges: &[CloudEdge],
cam: &CloudCamera,
background: [f32; 4],
) -> GraphScene {
let (yaw, scale, emissive, shrink) = (cam.cam[0], cam.cam[1], cam.cam[2], cam.cam[3]);
let (cx, cy) = (cam.viewport[2], cam.viewport[3]);
let (s, c) = yaw.sin_cos();
let proj: Vec<([f32; 2], f32)> = pts
.iter()
.map(|p| {
let [px, py, pz] = p.pos;
let x = px * c + pz * s;
let d = -px * s + pz * c;
([cx + x * scale, cy - py * scale], d)
})
.collect();
let radiance = |packed: u32, depth: f32, mul: f32| -> [f32; 4] {
let ch = |sh: u32| {
let v = ((packed >> sh) & 0xff) as f32 / 255.0;
v * v
};
let dbright = (0.55 + (depth + 1.0) * 0.35).clamp(0.4, 1.3);
let k = emissive * dbright * mul;
[ch(0) * k, ch(8) * k, ch(16) * k, 1.0]
};
GraphScene {
background,
nodes: pts
.iter()
.zip(&proj)
.map(|(p, &(sp, z))| NodeInstance {
center: sp,
radius: (3.0 + (z + 1.0) * 2.5) * shrink,
_pad: 0.0,
color: radiance(p.color, z, 1.0),
})
.collect(),
edges: edges
.iter()
.map(|&[a, b]| {
let (pa, pb) = (proj[a as usize], proj[b as usize]);
EdgeInstance {
a: pa.0,
b: pb.0,
width: 1.5,
_pad: 0.0,
color: radiance(pts[a as usize].color, (pa.1 + pb.1) * 0.5, 0.6),
}
})
.collect(),
}
}
/// Mean absolute per-channel difference between two RGBA frames, 0..255.
fn mean_abs_diff(a: &[u8], b: &[u8]) -> f64 {
assert_eq!(a.len(), b.len(), "frames differ in size");
let sum: u64 = a.iter().zip(b).map(|(x, y)| u64::from(x.abs_diff(*y))).sum();
sum as f64 / a.len() as f64
}
/// Fraction of bytes that are not zero — the non-blank oracle. Two black frames match
/// perfectly, so without this the parity assert below could pass on nothing at all.
fn ink(a: &[u8]) -> f64 {
a.iter().filter(|&&v| v > 8).count() as f64 / a.len() as f64
}
#[test]
fn the_cloud_lane_draws_the_instance_lane_picture() {
const W: u32 = 256;
const H: u32 = 192;
let bg = [0.01, 0.01, 0.02, 1.0];
let bloom = BloomParams::default();
let (pts, edges) = model(64);
let cam = CloudCamera::frame(W as f32, H as f32, 0.6, 3.0, 1.0);
let Some(cloud) = render_cloud_offscreen(W, H, &pts, &edges, &cam, bg, bloom) else {
eprintln!("no GPU turn — skipping cloud parity");
return;
};
let scene = instance_scene(&pts, &edges, &cam, bg);
let inst = render_graph_offscreen(W, H, &scene, bloom).expect("the instance lane rendered");
// Both lanes agree on WHAT they drew.
assert_eq!(cloud.node_instances, 64, "cloud drew every node");
assert_eq!(cloud.node_instances, inst.node_instances, "same node count");
assert_eq!(cloud.edge_instances, inst.edge_instances, "same edge count");
// Neither frame is blank. Without this the pixel compare below is satisfied by two
// black images — the identity value LAW 2 warns about.
let (ic, ii) = (ink(&cloud.rgba), ink(&inst.rgba));
assert!(ic > 0.02, "the cloud frame actually drew something (ink {ic:.4})");
assert!(ii > 0.02, "the instance frame actually drew something (ink {ii:.4})");
// And they agree on the PIXELS. Not bit-exact: the two lanes compute the same
// arithmetic in different places (CPU f32 vs the shader's), so a tolerance on the
// mean absolute difference is the honest form. A whole-frame mean of ~1/255 is far
// tighter than any visible difference and far looser than fp noise.
let d = mean_abs_diff(&cloud.rgba, &inst.rgba);
assert!(d < 1.5, "cloud vs instance lane mean |Δ| = {d:.4}/255 — the pictures differ");
eprintln!("cloud parity: mean |Δ| = {d:.4}/255, ink {ic:.4} vs {ii:.4}");
}
/// **The control**: the comparison above must be able to SEE a difference. Render the
/// cloud at a different yaw and require the same metric to reject it. Without this,
/// `mean_abs_diff` returning a small number proves nothing — a comparison that cannot
/// fail is not a comparison.
#[test]
fn the_parity_comparison_can_actually_fail() {
const W: u32 = 256;
const H: u32 = 192;
let bg = [0.01, 0.01, 0.02, 1.0];
let bloom = BloomParams::default();
let (pts, edges) = model(64);
let a = CloudCamera::frame(W as f32, H as f32, 0.6, 3.0, 1.0);
let b = CloudCamera::frame(W as f32, H as f32, 1.9, 3.0, 1.0);
let Some(fa) = render_cloud_offscreen(W, H, &pts, &edges, &a, bg, bloom) else {
eprintln!("no GPU turn — skipping parity control");
return;
};
let fb = render_cloud_offscreen(W, H, &pts, &edges, &b, bg, bloom).expect("second render");
let d = mean_abs_diff(&fa.rgba, &fb.rgba);
assert!(
d > 1.5,
"a 1.3 rad camera change must move the picture past the parity tolerance, got {d:.4}/255"
);
eprintln!("parity control: a yaw change moves the metric to {d:.4}/255");
}