facett-map3d/tests/fog_ramp.rs
//! **The fog band is ONE contract, and both lanes are held to it** — the guard behind
//! `facett_map3d::fog`, closing GFX_V2 §V2.9b's *"`FOG_MAX 0.55` exists in THREE
//! copies"*.
//!
//! What was actually triplicated was not a constant but the whole band —
//! `near = distance·0.6`, `far = distance·2.2 + 0.5`, `max = 0.55` — written longhand
//! at `lib.rs` twice (CPU painter) and `backend.rs` once (GPU uniform). Three copies
//! of a number the two render lanes must agree on, agreeing only by coincidence.
//!
//! [`FogRamp`] made that a single writer. This file is what keeps it one, and it holds
//! the parts a compiler cannot:
//!
//! 1. **No literal may come back.** The band's coefficients must not appear as bare
//! numbers anywhere in `src/` outside `fog.rs`. This is the assertion that actually
//! fails when somebody re-inlines `cam.distance * 0.6` in a new painter.
//! 2. **The slot contract holds against the SHADER TEXT.** `as_uniform()` writes
//! `[near, far, max, _]`; `gpu.rs` splits that into `fog_range.xy` and `fog.w`; and
//! `map3d.wgsl` reads `U.fog_range.x`, `U.fog_range.y` and `U.fog.w`. A rename or a
//! slot swap on either side silently re-points the band at the wrong number — a
//! defect no Rust type can catch, because every slot is an `f32`.
//! 3. **The divergence is RECORDED, not hidden.** The CPU ramp is linear and the GPU's
//! is exponential extinction (the 2026-08-04 aerial-perspective reform, which the
//! CPU painter never got). Asserting "the lanes agree" would be false. What IS
//! asserted is what is true: same band edges, same cap, both monotone, both zero
//! before `near` and both saturating at `max`.
//!
//! Pure — no GPU, no feature gate. Runs on any box.
use facett_map3d::fog::{FogRamp, FOG_MAX};
/// Read a crate source file relative to the manifest dir.
fn src(rel: &str) -> String {
let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(rel);
std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
}
/// ── 1. THE LITERALS MAY NOT COME BACK ────────────────────────────────────────────
///
/// The failure this prevents is not hypothetical — it is exactly the state the crate
/// was in until 2026-08-24. A new CPU painter, a new debug view, a "just this once"
/// tune: each one re-inlines the band, and from then on the 4090 and the software
/// painter fog differently with every existing test still green, because each lane is
/// internally consistent.
#[test]
fn the_fog_band_appears_as_a_literal_in_exactly_one_file() {
// The three signatures of an inlined band. `0.55` alone is too common a float to
// scan for (it is a colour, an alpha, a blend weight); the band is recognisable by
// its coefficients against a camera distance, which nothing else in the crate uses.
let signatures = ["distance * 0.6", "distance * 2.2", "FOG_MAX"];
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut offenders: Vec<String> = Vec::new();
let mut scanned = 0usize;
for entry in std::fs::read_dir(&dir).expect("src/ is readable") {
let path = entry.expect("dir entry").path();
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let name = path.file_name().unwrap().to_string_lossy().into_owned();
scanned += 1;
if name == "fog.rs" {
continue; // THE one writer.
}
let text = std::fs::read_to_string(&path).expect("read src file");
for (n, line) in text.lines().enumerate() {
// Doc comments may name the band — that is how the module explains itself.
let code = line.trim_start();
if code.starts_with("//") {
continue;
}
for sig in signatures {
if code.contains(sig) {
offenders.push(format!("{name}:{}: {}", n + 1, line.trim()));
}
}
}
}
// NON-VACUITY: a scan that walked nothing would pass this file forever.
assert!(scanned >= 8, "the src/ scan found {scanned} .rs files — it is not looking at the crate");
assert!(
std::fs::metadata(dir.join("fog.rs")).is_ok(),
"fog.rs — the one writer this test exempts — must exist, or the exemption is a hole"
);
assert!(
offenders.is_empty(),
"the fog band is inlined outside `fog.rs` again ({} site(s)); route them through \
`FogRamp::from_camera_distance` — that is the whole point of GFX_V2 §V2.9b's \
three-copies finding:\n {}",
offenders.len(),
offenders.join("\n ")
);
}
/// ── 2. THE SLOT CONTRACT, AGAINST THE SHADER TEXT ────────────────────────────────
///
/// `as_uniform()` → `backend.rs` `fog` → `gpu.rs` `fog_range.xy` + `fog.w` →
/// `map3d.wgsl`. Four hops, every one of them an untyped `f32` slot.
#[test]
fn the_uniform_slots_are_the_ones_the_shader_reads() {
let wgsl = src("src/map3d.wgsl");
let gpu = src("src/gpu.rs");
// The shader's own reads. If someone re-points `near` at `.y`, this fails.
assert!(
wgsl.contains("let near = U.fog_range.x;"),
"map3d.wgsl reads the band's NEAR from fog_range.x"
);
assert!(
wgsl.contains("let span = max(U.fog_range.y - near, 1e-3);"),
"…and its FAR from fog_range.y, with the same 1e-3 span floor `FogRamp::span` uses"
);
assert!(
wgsl.contains("dist_t * U.fog.w"),
"…and the CAP from fog.w — the slot `as_uniform()[2]` travels in"
);
// The gpu.rs split that puts them there: fog_range.xy = fog[0..2], fog.w = fog[2].
assert!(
gpu.contains("fog_range: [self.fog[0], self.fog[1],"),
"gpu.rs routes as_uniform()[0] and [1] into fog_range.x and .y"
);
assert!(
gpu.contains("self.fog[2]"),
"gpu.rs routes as_uniform()[2] (the cap) into the fog colour's w"
);
// And the Rust side agrees about which index is which.
let r = FogRamp::from_camera_distance(4.0);
let u = r.as_uniform();
assert_eq!(u[0], r.near, "slot 0 is near");
assert_eq!(u[1], r.far, "slot 1 is far");
assert_eq!(u[2], r.max, "slot 2 is the cap");
assert_eq!(u[2], FOG_MAX, "…which is FOG_MAX unless a caller dimmed it");
}
/// ── 3. THE DIVERGENCE, RECORDED HONESTLY ─────────────────────────────────────────
///
/// Both lanes are held to the band's THREE guarantees. The curve between the edges is
/// asserted to be different, because it is — and a test that claimed otherwise would
/// be the false green this suite keeps finding.
#[test]
fn both_lanes_share_the_band_and_differ_only_in_curve() {
let d = 10.0_f32;
let r = FogRamp::from_camera_distance(d);
// The GPU's curve, transcribed from map3d.wgsl `fs_main` at a fragment on the
// ground plane with no height haze (k = 0 ⇒ ground_air = 1), which is the case
// where the two lanes are directly comparable at all.
let gpu_t = |depth: f32| -> f32 {
let dist = (depth - r.near).max(0.0);
let density = 2.5 / r.span();
(1.0 - (-dist * density).exp()).clamp(0.0, 1.0) * r.max
};
// (a) both are exactly zero in the foreground — the property the 2026-08-04 reform
// was written to restore ("a zero-length path is never hazed, at any altitude").
for probe in [0.0_f32, r.near * 0.5, r.near] {
assert_eq!(r.t(probe), 0.0, "CPU: no haze at {probe} (band starts at {})", r.near);
assert!(gpu_t(probe).abs() < 1e-6, "GPU: no haze at {probe}");
}
// (b) neither may exceed the cap, at any depth — the "distant shapes recede, they
// do not vanish" guarantee, which is what FOG_MAX < 1.0 buys.
for probe in [r.far, r.far * 10.0, 1e6] {
assert!(r.t(probe) <= FOG_MAX + 1e-6, "CPU cap holds at {probe}");
assert!(gpu_t(probe) <= FOG_MAX + 1e-6, "GPU cap holds at {probe}");
}
// (c) both monotone.
let (mut pc, mut pg) = (-1.0_f32, -1.0_f32);
for i in 0..=60 {
let depth = i as f32 * (r.far / 30.0);
let (c, g) = (r.t(depth), gpu_t(depth));
assert!(c >= pc - 1e-6, "CPU ramp is monotone at {depth}");
assert!(g >= pg - 1e-6, "GPU ramp is monotone at {depth}");
pc = c;
pg = g;
}
// (d) AND THEY GENUINELY DIFFER IN SHAPE. Recorded as a number so the day someone
// ports the exponential reform to the CPU painter, this test says so out loud
// instead of quietly continuing to pass.
let mid = (r.near + r.far) * 0.5;
let (cm, gm) = (r.t(mid), gpu_t(mid));
eprintln!("EMIT-FOG band=[{:.3},{:.3}] cap={FOG_MAX} mid_depth={mid:.3} cpu_t={cm:.4} gpu_t={gm:.4}", r.near, r.far);
assert!(
(gm - cm).abs() > 0.05,
"the CPU (linear, {cm:.4}) and GPU (exponential extinction, {gm:.4}) ramps still \
differ mid-band. If this now fails, the lanes were UNIFIED — good news, but the \
module doc in fog.rs says they are not, and it must be corrected in the same commit"
);
assert!(gm > cm, "exponential extinction hazes the mid-band harder than a linear ramp");
}