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_scissor.rs

//! **GPU-path scissor PROOF** (the OPEN bug: 3D geometry escaping the widget rect
//! in the LIVE wgpu build).
//!
//! M1 proved the **CPU painter** confines its ink to the widget rect
//! (`clip_poly_to_rect` drives `ink_outside_rect = 0`). It did NOT verify the
//! **GPU lane** (`draw_map_gpu` + `map3d.wgsl`). This test drives the *real* wgpu
//! depth path (`with_gpu(true)` + `install_renderer`) headlessly through kittest,
//! at the worst-case street-level dolled-in pose (distance ≈ 0.42, Front preset,
//! low elevation), and asserts NO ink lands outside the widget's allocated rect.
//!
//! The fix it pins (FIX (a)): `Map3dPaintCallback::paint` now sets the wgpu
//! `set_scissor_rect` to the widget viewport ∩ clip_rect (see `gpu_scissor_px`),
//! the GPU twin of the CPU painter's `clip_poly_to_rect`. Before the fix the GPU
//! pass was confined only by egui's courtesy viewport / NDC clip; a panel-wide
//! clip rect (the callback is added through the panel painter) left no tight
//! scissor pinned to THIS widget.
//!
//! Method (renderer- / window-padding-independent): render the SAME canvas twice —
//! once with the 3D GPU view drawn into an inset sub-rect, once without — and diff.
//! Any pixel outside the widget rect that changed = ink the GPU pass leaked.
//!
//! **Sensitivity note (honest):** with egui_wgpu's *stock* renderer the blit is
//! already double-confined — egui sets the render-pass scissor from the callback's
//! `clip_rect` (renderer.rs:530) AND a courtesy viewport from the callback rect
//! (renderer.rs:588). So this end-to-end render asserts the GPU lane DOES confine
//! ink (0 px outside the rect) and that the LIVE wgpu path ran, but it cannot, on
//! the stock renderer, distinguish "our scissor on" from "our scissor off". The
//! rect MATH of FIX (a) — that our scissor is viewport ∩ clip_rect and therefore
//! never wider than the widget even when the surrounding clip_rect is panel-wide —
//! is the fail-on-bug / pass-on-fix oracle, unit-tested in `gpu.rs`
//! (`scissor_is_pinned_to_widget_not_panel_clip`, `gpu_scissor_px_from_callback_info`).
//! Our scissor is load-bearing whenever the viewport courtesy is bypassed (a host
//! overriding the viewport in an overlapping callback, batched passes, or a
//! non-stock backend) — it makes the GPU lane self-confining regardless.
//!
//! Run: `cargo test -p facett-map3d --features wgpu --test gpu_scissor -- --nocapture`

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

use egui::Color32;
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, V3, ViewPreset};

/// A closed building ring → a real OSM-style filled `Way` (the demo's prism path).
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 spread across the design box (the `from_ways`
/// path the demo's OSM 3D tab drives), no logo overlay.
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
}

/// The worst-case low, dollied-in street-level pose — the angle that flung off-axis
/// near-foreground geometry far off-centre. Distance ≈ 0.42 (just above the camera
/// min), Front preset, low elevation.
fn street_level(m: &mut Map3D) {
    m.spin = false;
    m.set_clock(0.0);
    m.cam.target = V3::new(0.0, 0.0, 0.0);
    m.cam.goal_target = m.cam.target;
    m.cam.distance = 0.42;
    m.cam.goal_distance = 0.42;
    m.view_preset(ViewPreset::Front);
    m.cam.elevation = 0.02;
    m.cam.goal_elevation = 0.02;
    m.cam.settle();
}

/// Render the canvas through kittest's **wgpu** backend. When `draw_view`, the
/// GPU-opted 3D map is drawn into an inset sub-rect of a sentinel-filled canvas;
/// otherwise only the sentinel fill is drawn (the baseline). Returns the frame.
fn render(draw_view: bool, canvas: f32, margin: f32, sentinel: Color32) -> (image::RgbaImage, bool) {
    let rs = create_render_state(default_wgpu_setup(), Default::default());
    // Install the real depth-tested renderer — the LIVE GPU lane.
    facett_map3d::gpu::install_renderer(&rs);

    let mut view = Map3D::from_ways("gpu-scissor", &town(), 0.22).with_gpu(true);
    street_level(&mut view);

    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(canvas, canvas))
        .renderer(WgpuTestRenderer::from_render_state(rs))
        .build_ui(move |ui| {
            facett_core::set_theme(ui.ctx(), Theme::sci_fi());
            let full = ui.max_rect();
            let inner = full.shrink(margin);
            // Sentinel-fill the widget's would-be rect so the baseline + the drawn
            // frame share an identical background everywhere outside it.
            ui.painter().rect_filled(inner, egui::CornerRadius::ZERO, sentinel);
            if draw_view {
                let mut child = ui.new_child(egui::UiBuilder::new().max_rect(inner));
                facett_core::Facet::ui(&mut view, &mut child);
                cell.set(view.used_gpu());
            }
        });
    harness.run_steps(4);
    let img = harness.render().expect("headless wgpu render produced an image");
    (img, used_gpu_cell.get())
}

/// **No ink outside the rect on the GPU lane** (fail-on-bug / pass-on-fix). Drive
/// the real wgpu depth path at the street-level pose; diff the drawn frame against
/// a no-view baseline; assert the margin band around the widget is untouched.
#[test]
fn gpu_path_paints_no_ink_outside_its_rect() {
    const CANVAS: f32 = 600.0;
    const MARGIN: f32 = 90.0; // band around the widget that must stay untouched
    let sentinel = Color32::from_rgb(255, 0, 255); // magenta — the map never paints this

    let (drawn, used_gpu) = render(true, CANVAS, MARGIN, sentinel);
    let (baseline, _) = render(false, CANVAS, MARGIN, sentinel);

    // The final-picture BLOB the spec wants emitted so an agent can Read the actual
    // rendered image (turning "looks wrong" into a pixel it inspects itself).
    image::DynamicImage::ImageRgba8(drawn.clone()).save("/tmp/map3d_gpu_scissor.png").ok();

    assert!(used_gpu, "the GPU depth path actually ran (used_gpu=true) — this proves the LIVE wgpu lane");
    assert_eq!(drawn.dimensions(), baseline.dimensions(), "same framebuffer size");

    let ppp = 1.0_f32; // CANVAS == framebuffer in the headless harness
    let (w, h) = drawn.dimensions();
    let m_px = (MARGIN * ppp).round() as u32;

    let mut leaked = 0u64;
    let mut band = 0u64;
    for y in 0..h {
        for x in 0..w {
            let outside = x < m_px || x >= w.saturating_sub(m_px) || y < m_px || y >= h.saturating_sub(m_px);
            if !outside {
                continue;
            }
            band += 1;
            if drawn.get_pixel(x, y) != baseline.get_pixel(x, y) {
                leaked += 1;
            }
        }
    }
    assert!(band > 0, "there is a margin band to check");
    let frac = leaked as f64 / band as f64;
    eprintln!(
        "[gpu-scissor] used_gpu={used_gpu} margin band {band} px, changed-vs-baseline {leaked} ({:.4}%) — blob /tmp/map3d_gpu_scissor.png",
        frac * 100.0
    );
    // A perfect scissor changes ZERO pixels outside the rect; allow only a hair of
    // anti-aliased feathering on the clip boundary (< 0.05% of the band).
    assert!(frac < 0.0005, "the GPU 3D pass drew {leaked} px ({:.3}%) OUTSIDE its rect", frac * 100.0);

    #[cfg(feature = "testmatrix")]
    facett_core::testmatrix::emit(
        "facett-map3d",
        "gpu_scissor_no_ink_outside_rect",
        used_gpu && frac < 0.0005,
        &format!("lane=gpu used_gpu={used_gpu} leaked_px={leaked} band_px={band} frac={frac:.6}"),
    );
}