Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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");
}