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