facett-map3d/tests/sky_lut_proof.rs
//! **PHYSICAL-SKY TRANSMITTANCE-LUT on-screen PROOF** (FLIGHT-SIM #7 render wiring).
//!
//! The GPU scattering-LUT compute pass (landed in `facett-map3d-flight`, oracle-proven
//! vs the CPU `TransmittanceLut`) precomputes atmospheric transmittance-to-sun. This
//! test proves that LUT is now **wired into the live 21-pass render pipeline's sky
//! pass and changes the pixels on screen**: the sky fragment multiplies the sun
//! disc/glow by the sampled transmittance, so a reddening LUT physically reddens the
//! sun. It drives the REAL wgpu lane (`with_gpu(true)` + `install_renderer`) headlessly
//! through `egui_kittest`, exactly as `gpu_scissor` / `golden_248` do.
//!
//! Method (renderer-independent, self-verifying):
//! * render the SAME sun-facing town three times through the live GPU sky pass —
//! (a) NO LUT (the analytic sun tint, the pre-wiring baseline),
//! (b) an all-ones LUT (transmittance = 1 → the wiring is transparent),
//! (c) a strongly-reddening LUT (blue crushed);
//! * TRANSPARENCY control: (b) ≈ (a) — binding a unit LUT must not perturb the
//! analytic result (proves the fallback is byte-faithful, L2 no-regression);
//! * VISIBLE effect: (c) differs from (a), and the changed pixels shift RED-ward
//! (blue drops relative to red) — the physical sunset reddening drawn on screen;
//! * SENSITIVITY: the reddening frame must differ on a non-trivial pixel count, so
//! the proof is a real tripwire, not a rubber stamp.
//!
//! **Adapter-gated:** with no GPU adapter the test self-skips with a recorded
//! `testmatrix` sentinel (Loki HAS a real GPU, so it runs there). The GPU-less
//! RED-when-broken half of the contract (uniform sized + bindings declared + sampling
//! gated) lives in `gpu.rs::sky_lut_wiring_registered_and_uniform_sized`.
//!
//! Run: `cargo test -p facett-map3d --features wgpu,testmatrix --test sky_lut_proof -- --nocapture`
#![cfg(feature = "wgpu")]
use egui_kittest::Harness;
use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state, default_wgpu_setup};
use facett_core::Theme;
use facett_map::Way;
use facett_map3d::{Map3D, SkyTransmittanceLut, SunSky, V3, ViewPreset};
const CANVAS: f32 = 400.0;
/// A closed building ring → a real OSM-style filled `Way`.
fn building(id: i64, ring: &[(f64, f64)]) -> Way {
let tags = vec![("building".to_string(), "yes".to_string())];
Way::from_lonlat(id, ring, &tags, &Theme::default()).expect("a building way")
}
/// A small town of real buildings (the `from_ways` extrude path).
fn town() -> Vec<Way> {
let mut ways = Vec::new();
let mut id = 1;
for r in 0..4 {
for c in 0..4 {
let bx = 9.50 + c as f64 * 0.002;
let by = 47.10 + r as f64 * 0.002;
ways.push(building(
id,
&[(bx, by), (bx + 0.0008, by), (bx + 0.0008, by + 0.0008), (bx, by + 0.0008), (bx, by)],
));
id += 1;
}
}
ways
}
/// Is a GPU adapter present at all? (The wgpu test-renderer needs one.) Mirrors the
/// flight crate's `scatter_gpu::compute_device` guard so the proof self-skips cleanly.
fn have_adapter() -> bool {
let instance = wgpu::Instance::default();
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: None,
apply_limit_buckets: false,
}))
.is_ok()
}
/// A uniform LUT of one transmittance value (fills every μ / altitude texel), so the
/// sampled sun transmittance is deterministic regardless of the sun's elevation.
fn uniform_lut(rgb: [f32; 3]) -> SkyTransmittanceLut {
let (mu, h) = (16u32, 8u32);
let texels = vec![[rgb[0], rgb[1], rgb[2], 1.0]; (mu * h) as usize];
SkyTransmittanceLut { mu_size: mu, h_size: h, texels, alt_norm: 0.0 }
}
/// Render the sun-facing town through the LIVE GPU sky pass, with an optional
/// physical-sky transmittance LUT wired in. Front elevation looks along −Z with the
/// sun placed up the −Z hemisphere, so the sun disc + glow fill the upper sky.
fn render(lut: Option<SkyTransmittanceLut>) -> image::RgbaImage {
let rs = create_render_state(default_wgpu_setup(), Default::default());
facett_map3d::gpu::install_renderer(&rs);
// Sun up the −Z hemisphere (matches Front's −Z look) so the disc/glow is in frame.
let sun = SunSky::from_angles(std::f32::consts::PI, 0.32);
let mut view = Map3D::from_ways("sky-lut", &town(), 0.22).with_gpu(true).with_sun(sun);
view.spin = false;
view.set_clock(0.0);
view.cam.target = V3::new(0.0, 0.35, 0.0);
view.cam.goal_target = view.cam.target;
view.cam.distance = 0.9;
view.cam.goal_distance = 0.9;
view.view_preset(ViewPreset::Front);
view.cam.elevation = 0.06;
view.cam.goal_elevation = 0.06;
view.cam.settle();
if let Some(l) = lut {
view.set_sky_transmittance_lut(l);
}
let mut harness = Harness::builder()
.with_size(egui::vec2(CANVAS, CANVAS))
.renderer(WgpuTestRenderer::from_render_state(rs))
.build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), Theme::sci_fi());
facett_core::Facet::ui(&mut view, ui);
});
harness.run_steps(4);
harness.render().expect("headless wgpu render produced an image")
}
/// `(changed_px, mean_dblue, mean_dred)` between two frames over pixels whose max
/// channel delta exceeds `tol`. `dblue`/`dred` are `b - a` (negative = the channel
/// dropped from frame `a` to frame `b`).
fn diff(a: &image::RgbaImage, b: &image::RgbaImage, tol: i32) -> (u64, f64, f64) {
let (mut changed, mut sblue, mut sred) = (0u64, 0i64, 0i64);
for (pa, pb) in a.pixels().zip(b.pixels()) {
let dr = pb.0[0] as i32 - pa.0[0] as i32;
let dg = pb.0[1] as i32 - pa.0[1] as i32;
let db = pb.0[2] as i32 - pa.0[2] as i32;
if dr.abs().max(dg.abs()).max(db.abs()) > tol {
changed += 1;
sblue += db as i64;
sred += dr as i64;
}
}
let n = changed.max(1) as f64;
(changed, sblue as f64 / n, sred as f64 / n)
}
#[test]
fn physical_sky_lut_reddens_the_sun_on_screen() {
if !have_adapter() {
// It said `ok = TRUE` for an arm that did not run: a GREEN matrix row whose own
// detail string reads `skipped=true`. A row that records the skip and calls it a
// pass is worse than no row — it is the skip wearing the proof's badge.
facett_core::testmatrix::gpu_skip(
"facett-map3d",
"sky_lut_onscreen",
"no GPU adapter",
"the physical sky LUT reddens the sun ON SCREEN (the GPU-less wiring is a \
separate unit test and does not stand in for this)",
);
return;
}
let base = render(None); // analytic sun tint (pre-wiring baseline)
let ones = render(Some(uniform_lut([1.0, 1.0, 1.0]))); // transmittance = 1 → transparent
let red = render(Some(uniform_lut([1.0, 0.22, 0.05]))); // strong sunset reddening
assert_eq!(base.dimensions(), (CANVAS as u32, CANVAS as u32));
// A final-picture BLOB an agent can Read to inspect the actual reddening.
image::DynamicImage::ImageRgba8(red.clone()).save("/tmp/map3d_sky_lut_red.png").ok();
image::DynamicImage::ImageRgba8(base.clone()).save("/tmp/map3d_sky_lut_base.png").ok();
let total = (CANVAS * CANVAS) as u64;
// ── TRANSPARENCY control: a unit LUT must not perturb the analytic sky (L2). ──
let (ones_changed, _, _) = diff(&base, &ones, 6);
let ones_frac = ones_changed as f64 / total as f64;
eprintln!("[sky_lut_proof] unit-LUT changed_px={ones_changed} ({:.4}% of frame)", ones_frac * 100.0);
assert!(ones_frac < 0.01, "a unit (transmittance=1) LUT is transparent: {:.4}% changed", ones_frac * 100.0);
// ── VISIBLE effect: the reddening LUT changes real pixels, RED-ward. ──
let (red_changed, mean_dblue, mean_dred) = diff(&base, &red, 6);
let red_frac = red_changed as f64 / total as f64;
eprintln!(
"[sky_lut_proof] red-LUT changed_px={red_changed} ({:.3}% of frame) mean_dblue={mean_dblue:.2} mean_dred={mean_dred:.2}",
red_frac * 100.0
);
// Read only by the testmatrix row below.
#[cfg(feature = "testmatrix")]
let ok = red_frac > 0.02 && mean_dblue < -2.0 && mean_dblue < mean_dred;
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-map3d",
"sky_lut_onscreen",
ok,
&format!(
"gpu_present changed_frac={red_frac:.4} mean_dblue={mean_dblue:.2} mean_dred={mean_dred:.2} unit_lut_frac={ones_frac:.4}"
),
);
// RED-when-broken: if the LUT never reached the shader (binding dropped, sampling
// removed, or lut_on never set) the reddening frame == the baseline → red_frac ≈ 0
// and this fails. If it reached but shifted the wrong way, the blue-drop fails.
assert!(red_frac > 0.02, "the reddening LUT visibly changed the sky (got {:.3}% of frame)", red_frac * 100.0);
assert!(mean_dblue < -2.0, "the sun tint lost blue (reddened): mean Δblue = {mean_dblue:.2}");
assert!(mean_dblue < mean_dred, "blue dropped more than red (a red-ward shift): Δblue {mean_dblue:.2} < Δred {mean_dred:.2}");
}