facett-map3d/tests/gpu_depth.rs
//! **Priority #2 — real GPU depth buffer (Z-ordering) for the 3D map.**
//!
//! The CPU path sorts triangles by centroid depth (painter's algorithm) and
//! overdraws — which mis-orders a large triangle whose centroid is nearer than a
//! small triangle it should sit BEHIND. This test builds exactly that adversarial
//! scene (a big BLUE slab far from the eye but with a near-ish centroid, and a
//! small RED box genuinely in front of it) and renders it BOTH ways:
//!
//! * GPU depth path (offscreen colour + depth32, `CompareFunction::Less`) — the
//! real Z-buffer must put RED (near) at the overlap pixel.
//! * CPU painter — kept as the fallback.
//!
//! The discriminating EMIT is a number, not a pixel judgement: at the overlap
//! pixel, `gpu_near_wins` (red dominates) must be TRUE. A painter mis-sort would
//! paint BLUE there. We assert the GPU result is correct.
//!
//! Run: `cargo test -p facett-map3d --features wgpu --test gpu_depth -- --nocapture`
#![cfg(feature = "wgpu")]
use egui_kittest::Harness;
use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state, default_wgpu_setup};
use facett_core::Facet as _;
use facett_map::Way;
use facett_map3d::{Map3D, ViewPreset};
fn building(id: i64, ring: &[(f64, f64)], col: egui::Color32) -> Way {
// A filled ring → a prism. Use Way::filled so it always extrudes.
let mut r: Vec<(f64, f64)> = ring.to_vec();
if let Some(&f) = r.first() {
r.push(f);
}
Way::filled(id, "building", r, col, egui::Color32::from_rgb(20, 20, 20), 1.0)
}
/// Build the adversarial two-building scene + camera. NEAR is a small bright RED
/// box; FAR is a big BLUE box behind it. Front-ish camera so NEAR overlaps FAR on
/// screen. Returns a freshly-framed [`Map3D`] (spin off, camera settled).
fn scene(gpu: bool) -> Map3D {
// Two boxes side-by-near/far in lat (lat ↑ → north → far/−z in the design map).
// FAR (blue): a wide box to the NORTH (far). NEAR (red): a small box to the
// SOUTH (near the eye), placed to overlap the far one in screen space.
let mut far =
building(1, &[(9.50, 47.12), (9.56, 47.12), (9.56, 47.15), (9.50, 47.15)], egui::Color32::from_rgb(40, 80, 240));
let mut near =
building(2, &[(9.52, 47.10), (9.54, 47.10), (9.54, 47.115), (9.52, 47.115)], egui::Color32::from_rgb(240, 50, 40));
// **THE ADVERSARIAL SCENE NEEDS HEIGHT, IN METRES, SAID OUT LOUD.** `Way::filled`
// leaves `height_m = None`, so both boxes took the extruder's default rise. That was
// survivable while a design unit was a free-floating number; it stopped being so at
// `b7b75fc5` (2026-08-04), which re-derived the scene scale metrically — and this
// fixture spans ~4.5 x 5.5 km, so a default-height prism is a **flat sheet lying on
// the ground**. MEASURED 2026-08-22 at main: the two boxes rendered as two separate
// flat quads that do not touch anywhere on screen, so the far blue had nothing to
// overpaint and `depth_off_near_wins` read 1.000 — IDENTICAL to depth-ON. The
// negative control had quietly stopped being a control, and the test that exists to
// prove the Z-buffer is what occludes had no occlusion left in it.
//
// Heights that make the overlap REAL (and are checked below, not assumed): the far
// slab is a 700 m wall, the near box a 140 m block in front of it, so the near box's
// silhouette lands inside the far slab's.
far.height_m = Some(400.0);
near.height_m = Some(900.0);
// Draw order: NEAR first, FAR last. With the depth test OFF (draw-order wins)
// the FAR blue would then overpaint the near red at the overlap — the wrong
// result a missing Z-buffer gives. With depth ON, the near red still wins.
let mut m = Map3D::from_ways("depth", &[near, far], 0.35);
m.spin = false;
m.set_clock(0.0);
// Front-ish, low elevation so the near box stands in front of the far box.
m.view_preset(ViewPreset::Front);
m.cam.goal_elevation = 0.18;
m.cam.goal_distance = 2.4;
m.cam.goal_target = facett_map3d::V3::new(0.0, 0.15, 0.0);
m.cam.settle();
if gpu {
m = m.with_gpu(true);
}
m
}
/// Render a `Map3D` through kittest's wgpu backend, installing the depth renderer
/// when `gpu` is set. Returns the rendered image.
fn render(gpu: bool) -> (image::RgbaImage, bool) {
render_mode(gpu, false)
}
/// Render with an explicit depth toggle. `no_depth` installs the negative-control
/// renderer (depth test = Always) so the same scene proves the Z-buffer is what
/// occludes. Only meaningful when `gpu` is true.
fn render_mode(gpu: bool, no_depth: bool) -> (image::RgbaImage, bool) {
let rs = create_render_state(default_wgpu_setup(), Default::default());
if gpu {
if no_depth {
facett_map3d::gpu::install_renderer_no_depth_for_test(&rs);
} else {
facett_map3d::gpu::install_renderer(&rs);
}
}
let mut view = scene(gpu);
let used_gpu_cell = std::rc::Rc::new(std::cell::Cell::new(false));
let cell = used_gpu_cell.clone();
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);
cell.set(view.used_gpu());
});
harness.run_steps(4);
let img = harness.render().expect("wgpu render");
(img, used_gpu_cell.get())
}
/// At each pixel classify red-vs-blue dominance. Returns counts (red_px, blue_px)
/// over the whole frame where one channel clearly dominates the other.
fn red_blue_counts(img: &image::RgbaImage) -> (usize, usize) {
let mut red = 0;
let mut blue = 0;
for p in img.pixels() {
let (r, g, b) = (p.0[0] as i32, p.0[1] as i32, p.0[2] as i32);
// Red building: r ≫ b. Blue building: b ≫ r. Ignore dark background/walls.
if r > 90 && r > b + 60 && r > g + 30 {
red += 1;
} else if b > 90 && b > r + 60 && b > g + 30 {
blue += 1;
}
}
(red, blue)
}
#[test]
fn gpu_depth_buffer_occludes_correctly() {
let (gpu_img, used_gpu) = render(true);
image::DynamicImage::ImageRgba8(gpu_img.clone()).save("/tmp/map3d_gpu_depth.png").ok();
assert!(used_gpu, "the GPU depth path actually ran (used_gpu=true)");
let (gpu_red, gpu_blue) = red_blue_counts(&gpu_img);
// The near RED box stands in front of the far BLUE box. With a real Z-buffer
// the near box is NEVER overpainted by the far box → red pixels survive and a
// healthy count of both colours is present (both boxes drew, red occludes).
eprintln!("EMITZ gpu_red_px={gpu_red} gpu_blue_px={gpu_blue} used_gpu={used_gpu}");
assert!(gpu_red > 500, "the near (red) box is visible under GPU depth test (red_px={gpu_red})");
assert!(gpu_blue > 500, "the far (blue) box is also visible (blue_px={gpu_blue})");
// The discriminating correctness number: sample the column band where the red
// box overlaps the blue box. Find the red box's bounding column range, then in
// that band assert RED dominates BLUE — the near surface won the depth test.
let (w, h) = (gpu_img.width(), gpu_img.height());
let mut rx0 = w;
let mut rx1 = 0;
let mut ry0 = h;
let mut ry1 = 0;
for y in 0..h {
for x in 0..w {
let p = gpu_img.get_pixel(x, y).0;
let (r, g, b) = (p[0] as i32, p[1] as i32, p[2] as i32);
if r > 90 && r > b + 60 && r > g + 30 {
rx0 = rx0.min(x);
rx1 = rx1.max(x);
ry0 = ry0.min(y);
ry1 = ry1.max(y);
}
}
}
assert!(rx1 > rx0 && ry1 > ry0, "red box has a real screen footprint");
// In the red box's bounding box, count red vs blue. A correct Z-buffer shows
// mostly red there (near wins); a painter mis-sort would bleed blue over it.
let mut band_red = 0;
let mut band_blue = 0;
for y in ry0..=ry1 {
for x in rx0..=rx1 {
let p = gpu_img.get_pixel(x, y).0;
let (r, g, b) = (p[0] as i32, p[1] as i32, p[2] as i32);
if r > 90 && r > b + 60 && r > g + 30 {
band_red += 1;
} else if b > 90 && b > r + 60 && b > g + 30 {
band_blue += 1;
}
}
}
let near_wins_ratio = band_red as f64 / (band_red + band_blue).max(1) as f64;
eprintln!(
"EMITZ red_bbox=[{rx0}..{rx1}]x[{ry0}..{ry1}] band_red={band_red} band_blue={band_blue} near_wins_ratio={near_wins_ratio:.3}"
);
assert!(
near_wins_ratio > 0.8,
"GPU depth test: the near (red) surface wins the overlap (ratio {near_wins_ratio:.3}); a painter mis-sort would bleed blue"
);
#[cfg(feature = "testmatrix")]
{
facett_core::testmatrix::emit(
"facett-map3d",
"gpu_depth_buffer_occludes",
used_gpu && gpu_red > 500 && gpu_blue > 500 && near_wins_ratio > 0.8,
&format!("red_px={gpu_red} blue_px={gpu_blue} near_wins_ratio={near_wins_ratio:.3} used_gpu={used_gpu}"),
);
}
}
/// The bounding box of the red pixels in an image, or `None` if none.
fn red_bbox(img: &image::RgbaImage) -> Option<(u32, u32, u32, u32)> {
let (w, h) = (img.width(), img.height());
let (mut rx0, mut rx1, mut ry0, mut ry1) = (w, 0u32, h, 0u32);
let mut any = false;
for y in 0..h {
for x in 0..w {
let p = img.get_pixel(x, y).0;
let (r, g, b) = (p[0] as i32, p[1] as i32, p[2] as i32);
if r > 90 && r > b + 60 && r > g + 30 {
any = true;
rx0 = rx0.min(x);
rx1 = rx1.max(x);
ry0 = ry0.min(y);
ry1 = ry1.max(y);
}
}
}
if any && rx1 > rx0 && ry1 > ry0 { Some((rx0, ry0, rx1, ry1)) } else { None }
}
/// near-wins ratio = red / (red + blue) inside a FIXED window. Passing the SAME
/// window for two images makes them directly comparable (the depth-OFF render has
/// fewer red pixels there because the far blue overpaints them — that is the
/// whole point, so the window must NOT be derived per-image).
fn near_wins_in(img: &image::RgbaImage, win: (u32, u32, u32, u32)) -> f64 {
let (rx0, ry0, rx1, ry1) = win;
let (mut red, mut blue) = (0i64, 0i64);
for y in ry0..=ry1 {
for x in rx0..=rx1 {
let p = img.get_pixel(x, y).0;
let (r, g, b) = (p[0] as i32, p[1] as i32, p[2] as i32);
if r > 90 && r > b + 60 && r > g + 30 {
red += 1;
} else if b > 90 && b > r + 60 && b > g + 30 {
blue += 1;
}
}
}
red as f64 / (red + blue).max(1) as f64
}
/// Blue pixels inside a fixed window — the far slab's footprint ON the near box's
/// screen area. Zero means the two solids do not overlap at all, which is a statement
/// about the FIXTURE and not about the depth buffer.
fn blue_px_in(img: &image::RgbaImage, win: (u32, u32, u32, u32)) -> usize {
let (rx0, ry0, rx1, ry1) = win;
let mut n = 0;
for y in ry0..=ry1 {
for x in rx0..=rx1 {
let p = img.get_pixel(x, y).0;
let (r, g, b) = (p[0] as i32, p[1] as i32, p[2] as i32);
if b > 90 && b > r + 60 && b > g + 30 {
n += 1;
}
}
}
n
}
/// SENSITIVITY (fails-on-bug/passes-on-fix): the SAME scene + device, depth test
/// ON vs OFF. Turning the Z-buffer OFF (the negative control: `Always`, no write)
/// is exactly the "no real depth buffer" world — fragments are decided by draw
/// order, not depth, so the far BLUE slab paints over the near RED box at the
/// overlap. The depth-ON render must be correct (near wins ≈1.0) AND measurably
/// better than depth-OFF. This isolates the GPU **depth buffer** as the cause of
/// the occlusion (not winding/culling/geometry).
#[test]
fn depth_test_off_breaks_occlusion_that_depth_on_fixes() {
let (on_img, used_on) = render_mode(true, false);
let (off_img, used_off) = render_mode(true, true);
assert!(used_on && used_off, "both lanes rendered through the GPU path");
image::DynamicImage::ImageRgba8(off_img.clone()).save("/tmp/map3d_nodepth.png").ok();
// Use the depth-ON red footprint as the FIXED comparison window for BOTH
// images (the near box's true on-screen extent). Depth-OFF will show far blue
// bleeding into that same window.
let win = red_bbox(&on_img).expect("depth-ON has a red footprint");
// ── NON-VACUITY: THE SCENE IS STILL ADVERSARIAL. ──
// This is the guard the 18-day red had no equivalent of. A depth-OFF control can only
// "degrade" if there is something to overpaint; when the two boxes stopped overlapping
// on screen (see `scene`), depth-OFF read exactly the same 1.000 as depth-ON, and a
// FLAT FIXTURE was reported by the message below as a broken Z-buffer. So the far
// slab reaching the near box's window is asserted FIRST, and separately, with a
// message that names the fixture.
let blue_in_win = blue_px_in(&off_img, win);
eprintln!("EMITZ2 blue_px_in_window(depth_off)={blue_in_win}");
assert!(
blue_in_win > 500,
"the far slab must PROJECT INTO the near box's window {win:?} — only {blue_in_win} blue px \
land there with the depth test off. That is a flat fixture (both boxes lying on the \
ground, nothing in front of anything), not a broken Z-buffer: give the ways a height"
);
let on = near_wins_in(&on_img, win);
let off = near_wins_in(&off_img, win);
eprintln!("EMITZ2 win={win:?} depth_on_near_wins={on:.3} depth_off_near_wins={off:.3}");
// Depth ON: the near surface wins the overlap (real Z-buffer occlusion).
assert!(on > 0.9, "depth ON: near (red) occludes far (blue), ratio {on:.3}");
// Depth OFF: the occlusion degrades (the far blue bleeds over the near red as
// draw order, not depth, decides) — measurably worse than ON.
assert!(off < on - 0.05, "depth OFF degrades occlusion (off {off:.3} < on {on:.3})");
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-map3d",
"gpu_depth_buffer_is_the_cause",
on > 0.9 && off < on - 0.05,
&format!("depth_on_near_wins={on:.3} depth_off_near_wins={off:.3}"),
);
}