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