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

nordisk/facett

facett-map3d/tests/ssao_proof.rs

//! **SSAO / GTAO render PROOF (T3.1, LAW #4).**
//!
//! A normal (G-buffer) target is added to the map3d offscreen; a screen-space
//! ambient-occlusion pass reads it and the depth, and the main pass modulates the
//! ambient term by the result. Creases + wall/ground contacts come back darker.
//!
//! This renders a tight **courtyard** of blocks (an inner concave well whose floor
//! and inner walls are heavily self-occluded) through egui_kittest's wgpu backend,
//! AO ON vs OFF, at a fixed clock + sun. Shadows are turned OFF so the ONLY variable
//! is the ambient occlusion (otherwise cast shadows would confound the luma). It then
//! asserts, as DATA:
//!
//!   1. **the concave well darkens** — mean luma of the courtyard region drops with
//!      AO on (the contact occlusion the SSAO pass computed); AO-off is the control;
//!   2. **AO only subtracts** — the whole geometry's mean luma is <= AO-off (AO never
//!      brightens) and the well drops more than the open scene (it is the discriminator);
//!   3. **deterministic** — two independent AO-on renders are byte-identical (FC-7).
//!
//! Run: `cargo test -p facett-map3d --features wgpu --test ssao_proof -- --nocapture`

#![cfg(feature = "wgpu")]

use egui_kittest::wgpu::{create_render_state, default_wgpu_setup, WgpuTestRenderer};
use egui_kittest::Harness;
use facett_core::Facet as _;
use facett_map::Way;
use facett_map3d::{Map3D, SunSky, ViewPreset};

/// A tight ring of tall blocks around a small open centre — a courtyard whose floor
/// + inner faces are deeply ambient-occluded (the SSAO target's job).
fn courtyard() -> Vec<Way> {
    let (lon0, lat0) = (9.50_f64, 47.10_f64);
    let step = 0.0016; // tight spacing → a small well
    let bsz = 0.0012;
    let mut ways = Vec::new();
    let mut id = 1i64;
    for r in 0..3 {
        for c in 0..3 {
            if r == 1 && c == 1 {
                continue; // leave the centre open → the courtyard well
            }
            let bx = lon0 + c as f64 * step;
            let by = lat0 + r as f64 * step;
            let ring = [(bx, by), (bx + bsz, by), (bx + bsz, by + bsz), (bx, by + bsz), (bx, by)];
            let tags = vec![
                ("building".to_string(), "yes".to_string()),
                ("height".to_string(), "120".to_string()),
            ];
            ways.push(Way::from_lonlat(id, &ring, &tags, &facett_core::Theme::default()).unwrap());
            id += 1;
        }
    }
    ways
}

fn scene(ssao: bool) -> Map3D {
    // An AMBIENT-DOMINATED environment: a weak sun + a strong sky fill, so the
    // courtyard is lit mostly by the hemisphere ambient the SSAO pass modulates
    // (a strong direct sun would swamp the AO term). Shadows OFF so AO is the only
    // variable between the two renders. A near-top-down camera looks into the well.
    let sun = SunSky {
        dir: facett_map3d::SunSky::from_angles(0.4, 0.9).dir,
        sun_color: [1.0, 0.98, 0.92],
        sun_intensity: 0.35,
        sky_color: [0.55, 0.62, 0.80],
        ambient: 0.95,
    };
    let mut m = Map3D::from_ways("ssao", &courtyard(), 0.4)
        .with_gpu(true)
        .with_sun(sun)
        .with_shadows(0.0)
        .with_ssao(ssao);
    m.spin = false;
    m.set_clock(0.0);
    m.view_preset(ViewPreset::Top);
    m.cam.goal_distance = 2.4;
    m.cam.goal_target = facett_map3d::V3::new(0.0, 0.05, 0.0);
    m.cam.settle();
    m
}

fn render(ssao: bool) -> image::RgbaImage {
    // THE GPU TURN (`facett_core::render::gputurn`) — held for the whole render, not just
    // the bring-up. This file brings a device up from TWO `#[test]`s and took no turn:
    // MEASURED 2026-08-31, it died with `signal: 11, SIGSEGV` in a whole-workspace
    // `--release` run. That is not an assertion failure, it is a hard crash that fails the
    // FILE for a reason that has nothing to do with SSAO. facett-map has enforced this rule
    // since 2026-08-27 (`tests/one_gpu_turn_is_the_only_one.rs`); the lock lived in that
    // crate's test directory, so this one could not reach it. It lives in facett-core now.
    let _gpu = facett_core::render::gputurn::hold();

    let rs = create_render_state(
        facett_map3d::gpu::proof_wgpu_setup().unwrap_or_else(default_wgpu_setup),
        Default::default(),
    );
    facett_map3d::gpu::install_renderer(&rs);
    let mut view = scene(ssao);
    let mut harness = Harness::builder()
        .with_size(egui::vec2(640.0, 640.0))
        .renderer(WgpuTestRenderer::from_render_state(rs))
        .build_ui(move |ui| {
            facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
            view.ui(ui);
        });
    harness.run_steps(4);
    harness.render().expect("wgpu render")
}

fn luma(p: &image::Rgba<u8>) -> f64 {
    0.2126 * p.0[0] as f64 + 0.7152 * p.0[1] as f64 + 0.0722 * p.0[2] as f64
}

/// Mean luma of GEOMETRY pixels (not the dark background) across the whole frame.
fn geom_mean(img: &image::RgbaImage) -> (f64, usize) {
    let mut sum = 0.0;
    let mut n = 0usize;
    for p in img.pixels() {
        let l = luma(p);
        if l > 10.0 {
            sum += l;
            n += 1;
        }
    }
    (if n > 0 { sum / n as f64 } else { 0.0 }, n)
}

/// Mean luma of geometry pixels in the **central courtyard region** (the inner well)
/// — a centred box where the floor + inner faces are most occluded.
fn well_mean(img: &image::RgbaImage) -> (f64, usize) {
    let (w, h) = (img.width(), img.height());
    let (x0, x1) = (w * 36 / 100, w * 64 / 100);
    let (y0, y1) = (h * 40 / 100, h * 72 / 100);
    let mut sum = 0.0;
    let mut n = 0usize;
    for y in y0..y1 {
        for x in x0..x1 {
            let p = img.get_pixel(x, y);
            let l = luma(p);
            if l > 10.0 {
                sum += l;
                n += 1;
            }
        }
    }
    (if n > 0 { sum / n as f64 } else { 0.0 }, n)
}

#[test]
fn ssao_darkens_the_concave_courtyard_and_is_deterministic() {
    let on = render(true);
    let off = render(false);
    image::DynamicImage::ImageRgba8(on.clone()).save("/tmp/map3d_ssao_on.png").ok();
    image::DynamicImage::ImageRgba8(off.clone()).save("/tmp/map3d_ssao_off.png").ok();

    let (on_well, on_wn) = well_mean(&on);
    let (off_well, off_wn) = well_mean(&off);
    let (on_all, _) = geom_mean(&on);
    let (off_all, _) = geom_mean(&off);
    let well_drop = off_well - on_well;
    let all_drop = off_all - on_all;
    eprintln!(
        "EMIT-SSAO well_on={on_well:.2} well_off={off_well:.2} well_drop={well_drop:.2} all_on={on_all:.2} all_off={off_all:.2} all_drop={all_drop:.2} well_n_on={on_wn} well_n_off={off_wn}"
    );

    // The courtyard region is real geometry in both frames.
    assert!(on_wn > 2_000 && off_wn > 2_000, "the courtyard well is visible in both frames");
    // (1) AO darkens the concave well (the contact/crease occlusion subtracts ambient).
    assert!(
        well_drop > 1.5,
        "SSAO ON darkens the concave courtyard ({on_well:.2} < {off_well:.2}, drop {well_drop:.2})"
    );
    // (2) AO only subtracts (never brightens) the whole geometry, and the WELL drops
    //     more than the frame-wide average — the occlusion concentrates in the concavity.
    assert!(all_drop >= -0.05, "SSAO never brightens the geometry on average ({all_drop:.2})");
    assert!(
        well_drop > all_drop + 0.5,
        "the AO darkening concentrates in the concave well ({well_drop:.2} > {all_drop:.2})"
    );

    // (3) Determinism (FC-7): an independent AO-on re-render is byte-identical.
    let on2 = render(true);
    assert_eq!(on.as_raw(), on2.as_raw(), "the SSAO render is deterministic (two renders byte-identical)");

    #[cfg(feature = "testmatrix")]
    {
        let ok = well_drop > 1.5 && all_drop >= -0.05 && well_drop > all_drop + 0.5 && on.as_raw() == on2.as_raw();
        facett_core::testmatrix::emit(
            "facett-map3d",
            "ssao_darkens_concave_courtyard",
            ok,
            &format!("blob=/tmp/map3d_ssao_on.png well_drop={well_drop:.2} all_drop={all_drop:.2} well_on={on_well:.2} well_off={off_well:.2}"),
        );
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// GFX_V2 item 7 — THE MISSING CONTROL: does SSAO leave a FLAT PLANE alone?
//
// The test above proves AO darkens a concave well and that the darkening
// CONCENTRATES there. What it does not prove is the other half of the definition:
// that an unoccluded flat surface is left alone. Its frame-wide `all_drop` was 1.16
// luma — small, but not zero, and "small on a frame that is mostly walls and
// creases" cannot distinguish "AO is correctly local" from "AO darkens everything a
// little". A pass returning a constant 0.9 occlusion would satisfy every assertion
// up there.
//
// So this renders ONE frame containing both: a wide flat-roofed slab whose roof
// centre sits many AO radii from any edge, and the same concave courtyard beside it.
// AO must darken the well and NOT the roof, in the same frame, under the same light.
// ─────────────────────────────────────────────────────────────────────────────

/// A tight courtyard cluster (the concave well) beside a WIDE flat-roofed slab (the
/// flat plane). Same block height and tags, so the two regions differ only in shape.
///
/// The courtyard ring is the same construction as [`courtyard`], offset — it is the
/// shape whose concavity is already proven, reused rather than re-invented.
fn slab_and_courtyard() -> Vec<Way> {
    let (lon0, lat0) = (9.50_f64, 47.10_f64);
    let step = 0.0016;
    let bsz = 0.0012;
    let theme = facett_core::Theme::default();
    let tags = vec![
        ("building".to_string(), "yes".to_string()),
        ("height".to_string(), "120".to_string()),
    ];
    let mut ways = Vec::new();
    let mut id = 1i64;

    // ── RIGHT: the concave courtyard (3x3 minus the centre). ──
    for r in 0..3 {
        for c in 0..3 {
            if r == 1 && c == 1 {
                continue;
            }
            let bx = lon0 + c as f64 * step;
            let by = lat0 + r as f64 * step;
            let ring = [(bx, by), (bx + bsz, by), (bx + bsz, by + bsz), (bx, by + bsz), (bx, by)];
            ways.push(Way::from_lonlat(id, &ring, &tags, &theme).unwrap());
            id += 1;
        }
    }

    // ── LEFT: one wide slab. Its footprint spans the same extent as the whole 3x3
    // cluster, so its roof CENTRE is ~1.5 block-widths from the nearest edge in every
    // direction — far enough that no edge can occlude it at the SSAO radius.
    let span = 2.0 * step + bsz;
    let sx = lon0 - span - 2.0 * step; // a clear gap between the two structures
    let ring = [
        (sx, lat0),
        (sx + span, lat0),
        (sx + span, lat0 + span),
        (sx, lat0 + span),
        (sx, lat0),
    ];
    ways.push(Way::from_lonlat(id, &ring, &tags, &theme).unwrap());
    ways
}

fn slab_scene(ssao: bool) -> Map3D {
    // Identical lighting to `scene()`: ambient-dominated, shadows OFF, so AO is the
    // only variable between the two renders.
    let sun = SunSky {
        dir: facett_map3d::SunSky::from_angles(0.4, 0.9).dir,
        sun_color: [1.0, 0.98, 0.92],
        sun_intensity: 0.35,
        sky_color: [0.55, 0.62, 0.80],
        ambient: 0.95,
    };
    let mut m = Map3D::from_ways("ssao_flat", &slab_and_courtyard(), 0.4)
        .with_gpu(true)
        .with_sun(sun)
        .with_shadows(0.0)
        .with_ssao(ssao);
    m.spin = false;
    m.set_clock(0.0);
    m.view_preset(ViewPreset::Top);
    m.cam.goal_distance = 3.4;
    m.cam.goal_target = facett_map3d::V3::new(0.0, 0.05, 0.0);
    m.cam.settle();
    m
}

fn render_slab(ssao: bool) -> image::RgbaImage {
    // THE GPU TURN (`facett_core::render::gputurn`) — held for the whole render, not just
    // the bring-up. This file brings a device up from TWO `#[test]`s and took no turn:
    // MEASURED 2026-08-31, it died with `signal: 11, SIGSEGV` in a whole-workspace
    // `--release` run. That is not an assertion failure, it is a hard crash that fails the
    // FILE for a reason that has nothing to do with SSAO. facett-map has enforced this rule
    // since 2026-08-27 (`tests/one_gpu_turn_is_the_only_one.rs`); the lock lived in that
    // crate's test directory, so this one could not reach it. It lives in facett-core now.
    let _gpu = facett_core::render::gputurn::hold();

    let rs = create_render_state(
        facett_map3d::gpu::proof_wgpu_setup().unwrap_or_else(default_wgpu_setup),
        Default::default(),
    );
    {
        // ATTRIBUTION. `egui_kittest`'s `default_wgpu_setup()` does its OWN adapter
        // selection — it is NOT facett's `request_best_adapter` — so which device
        // answered has to be read, not assumed. Printed because it changes what the
        // measured luma numbers are evidence about.
        let i = rs.adapter.get_info();
        eprintln!("[ssao_proof] device: {} ({:?}, {:?})", i.name, i.backend, i.device_type);
    }
    facett_map3d::gpu::install_renderer(&rs);
    let mut view = slab_scene(ssao);
    let mut harness = Harness::builder()
        .with_size(egui::vec2(640.0, 640.0))
        .renderer(WgpuTestRenderer::from_render_state(rs))
        .build_ui(move |ui| {
            facett_core::set_theme(ui.ctx(), facett_core::Theme::sci_fi());
            view.ui(ui);
        });
    harness.run_steps(4);
    harness.render().expect("wgpu render")
}

/// Mean luma of geometry pixels in a fractional box of the frame, with the count.
fn box_mean(img: &image::RgbaImage, fx0: f64, fy0: f64, fx1: f64, fy1: f64) -> (f64, usize) {
    let (w, h) = (img.width() as f64, img.height() as f64);
    let (x0, x1) = ((fx0 * w) as u32, (fx1 * w) as u32);
    let (y0, y1) = ((fy0 * h) as u32, (fy1 * h) as u32);
    let mut sum = 0.0;
    let mut n = 0usize;
    for y in y0..y1.min(img.height()) {
        for x in x0..x1.min(img.width()) {
            let l = luma(img.get_pixel(x, y));
            if l > 10.0 {
                sum += l;
                n += 1;
            }
        }
    }
    (if n > 0 { sum / n as f64 } else { 0.0 }, n)
}

#[test]
fn ssao_darkens_the_concave_well_but_leaves_the_flat_roof_alone() {
    let on = render_slab(true);
    let off = render_slab(false);
    image::DynamicImage::ImageRgba8(on.clone()).save("/tmp/map3d_ssao_flat_on.png").ok();
    image::DynamicImage::ImageRgba8(off.clone()).save("/tmp/map3d_ssao_flat_off.png").ok();

    // A 2-D survey, printed, so the region choice is evidence rather than a guess.
    // Measured on an RTX 4090: the AO drop is ~6.5 in the courtyard well, ~0.3 on the
    // slab roof and ~0.2 on the open ground.
    for j in 0..8 {
        let fy0 = 0.12 + j as f64 * 0.095;
        let mut row = String::new();
        for i in 0..10 {
            let fx0 = i as f64 / 10.0;
            let (a, _) = box_mean(&on, fx0, fy0, fx0 + 0.1, fy0 + 0.095);
            let (b, n) = box_mean(&off, fx0, fy0, fx0 + 0.1, fy0 + 0.095);
            let d = if n > 200 { b - a } else { f64::NAN };
            row.push_str(&format!("{d:7.2}"));
        }
        eprintln!("EMIT-SSAO-GRID y={fy0:.3} drops:{row}");
    }

    // ── The three regions. ──
    // CONCAVE: the courtyard well — floor and inner faces, deeply self-occluded.
    let (well_on, well_n) = box_mean(&on, 0.60, 0.42, 0.70, 0.58);
    let (well_off, _) = box_mean(&off, 0.60, 0.42, 0.70, 0.58);
    // FLAT #1: the centre of the wide slab roof. A plane, ~1.5 block-widths from its
    // nearest edge in every direction, so nothing can occlude it at the AO radius.
    let (roof_on, roof_n) = box_mean(&on, 0.30, 0.42, 0.40, 0.58);
    let (roof_off, _) = box_mean(&off, 0.30, 0.42, 0.40, 0.58);
    // FLAT #2: open ground, well clear of both structures. A second, independent flat
    // plane — at a different height and orientation in the depth buffer, so a bug that
    // happened to spare one has to spare the other too.
    let (grd_on, grd_n) = box_mean(&on, 0.25, 0.19, 0.75, 0.25);
    let (grd_off, _) = box_mean(&off, 0.25, 0.19, 0.75, 0.25);

    let well_drop = well_off - well_on;
    let roof_drop = roof_off - roof_on;
    let grd_drop = grd_off - grd_on;
    eprintln!(
        "EMIT-SSAO-FLAT well {well_off:.2}->{well_on:.2} drop={well_drop:.2} n={well_n} | \
         roof {roof_off:.2}->{roof_on:.2} drop={roof_drop:.2} n={roof_n} | \
         ground {grd_off:.2}->{grd_on:.2} drop={grd_drop:.2} n={grd_n} | \
         well/roof={:.1}x well/ground={:.1}x",
        well_drop / roof_drop.abs().max(0.01),
        well_drop / grd_drop.abs().max(0.01)
    );

    // ── NON-VACUITY: all three regions are real lit surfaces in both frames. ──
    // Without this, a region that fell off the geometry would have drop 0 and would
    // "prove" the flat plane is untouched by containing nothing at all.
    assert!(well_n > 5_000, "the courtyard well is real geometry ({well_n} px)");
    assert!(roof_n > 5_000, "the slab roof is real geometry ({roof_n} px)");
    assert!(grd_n > 5_000, "the open ground is real geometry ({grd_n} px)");
    for (name, l) in [("well", well_off), ("roof", roof_off), ("ground", grd_off)] {
        assert!(l > 40.0, "the {name} region is a LIT surface, not near-black ({l:.2})");
    }

    // ── (1) AO DARKENS THE CONCAVITY — AGAINST THIS FRAME'S OWN FLAT CONTROLS. ──
    //
    // This clause used to read `well_drop > 4.0`, an absolute luma calibrated when the
    // SSAO sampling radius was the flat `0.16` design units this crate shipped until
    // 2026-08-04. `b7b75fc5` re-derived it in METRES — `SSAO_RADIUS_METRES = 12.0`
    // through the scene's own `du_per_m`, because 0.16 du on a metric scene is a **300 m
    // contact-shadow radius**, three football pitches of "contact". Ambient occlusion
    // integrates over that radius, so shrinking it 25x necessarily shrinks the luma drop:
    // measured 2026-08-22, `well_drop` 1.27 on both lavapipe and an RTX 4090. The pass is
    // not weaker than it should be; the radius is right and the constant was calibrated
    // against the wrong one.
    //
    // So the discriminator is the RATIO the two flat controls below already establish,
    // which is what "AO is a CONCAVITY term" actually claims and is independent of the
    // radius, the exposure and the tonemap: the well must darken by a real amount AND by
    // an order of magnitude more than a plane does. Measured: well 1.27 against roof 0.08
    // and ground 0.00 — 16x and 127x.
    let flat_noise = roof_drop.abs().max(grd_drop.abs());
    assert!(
        well_drop > 0.5,
        "SSAO must darken the concave courtyard well ({well_drop:.2} luma) — a pass that darkens \
         NOTHING fails here"
    );
    assert!(
        well_drop > 8.0 * flat_noise.max(0.02),
        "…and it must darken the CONCAVITY, not the frame: well {well_drop:.2} against a flat \
         control of {flat_noise:.2} luma is only {:.1}x, and AO that darkens a plane as much as \
         a well is a constant, not an occlusion term",
        well_drop / flat_noise.max(0.02)
    );

    // ── (2) AO LEAVES A FLAT PLANE ALONE. THIS IS THE CONTROL THAT WAS MISSING. ──
    // A pass returning a constant occlusion — the most plausible way for this to be
    // quietly broken, and one that satisfies every assertion in the courtyard test
    // above — darkens these two regions just as much as the well.
    assert!(
        roof_drop < 1.0,
        "SSAO must NOT darken the flat slab roof ({roof_drop:.2} luma) — a pass that darkens \
         EVERYTHING fails here"
    );
    assert!(
        grd_drop < 1.0,
        "…nor the open flat ground ({grd_drop:.2} luma)"
    );

    // ── (3) AND IT ONLY SUBTRACTS. ──
    for (name, d) in [("well", well_drop), ("roof", roof_drop), ("ground", grd_drop)] {
        assert!(d >= -0.05, "AO never BRIGHTENS the {name} region ({d:.2})");
    }

    // ── (4) THE RATIO — the assertion that survives a change of scene scale. ──
    // Absolute luma thresholds move with exposure and albedo; the ratio between a
    // concavity and a plane in the SAME frame does not.
    assert!(
        well_drop > roof_drop.abs() * 5.0,
        "the concave well must darken at least 5x more than the flat roof \
         ({well_drop:.2} vs {roof_drop:.2})"
    );
    assert!(
        well_drop > grd_drop.abs() * 5.0,
        "…and at least 5x more than the flat ground ({well_drop:.2} vs {grd_drop:.2})"
    );

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map3d",
        "ssao_spares_the_flat_plane",
        well_drop > 4.0 && roof_drop < 1.0 && grd_drop < 1.0,
        &format!(
            "blob=/tmp/map3d_ssao_flat_on.png well_drop={well_drop:.2} roof_drop={roof_drop:.2} ground_drop={grd_drop:.2}"
        ),
    );
}