facett-map3d/tests/route_reaches_the_gpu.rs
//! **A route set after the first paint must reach the DEVICE** — the mesh-mutation cache
//! invalidation (`Map3D::mesh_mut`).
//!
//! # The defect this pins
//!
//! `WgpuVelloBackend::paint` builds its GPU payload behind `if self.cache.borrow().is_none()`
//! (backend.rs:683) — once, and again only after `RenderBackend::reset_cache`. Three
//! writers touched `Map3D::mesh`; ONE invalidated that cache (`set_mesh_for_test`) and two
//! did not:
//!
//! * `Map3D::set_route` — the route ribbon, the stop markers, the chevron train;
//! * `Map3D::apply_roof_accent` — the roof highlight.
//!
//! So a route set after the pane's first paint was **built, counted, published in
//! `state_json`, and never drawn on the GPU lane**. Every cheaper signal was green:
//! `triangle_count()` grew, `state_json.triangles` grew, `RenderStats::visible_triangles`
//! grew — because all three read the CPU-side mesh. Only the bytes on the device knew.
//!
//! # Why the oracle is the device buffer and nothing else
//!
//! This is the exact reason `visible_triangles()` cannot be the oracle here: on the GPU
//! backend it comes from `analyze_scene`, which projects `scene.mesh` on the CPU. It is a
//! measurement of the mesh, not of the upload, so it would have been GREEN throughout the
//! defect. The assertion is therefore `Map3dRenderer::vbuf_for_test().1` — the vertex count
//! the next `draw` will use, read out of the live renderer in the harness's own
//! `callback_resources`.
//!
//! Distinct from `tests/gpu_geometry_swap.rs`, which pins the seam one layer down: that
//! file proves the RENDERER replaces a payload it is handed with a new build stamp. This
//! one proves a new payload is ever BUILT AND HANDED OVER at all.
//!
//! Run (needs the real device, opens NO window):
//! ```text
//! env -u WAYLAND_DISPLAY -u DISPLAY \
//! cargo test -p facett-map3d --features wgpu --test route_reaches_the_gpu -- --nocapture
//! ```
#![cfg(feature = "wgpu")]
use std::cell::RefCell;
use std::rc::Rc;
use facett_core::Theme;
use facett_map::Way;
use facett_map3d::gpu::Map3dRenderer;
use facett_map3d::Map3D;
const CANVAS: egui::Vec2 = egui::vec2(640.0, 480.0);
/// The scene's ways — held separately because `set_route` projects its waypoints against
/// the SAME footprint bbox the mesh was extruded from (`extrude::project_route`), so the
/// route and the buildings have to come from one set of ways or the ribbon lands nowhere.
fn ways() -> Vec<Way> {
let theme = Theme::default();
let ring = |lon: f64, lat: f64, h: f64| {
vec![(lon - h, lat - h), (lon + h, lat - h), (lon + h, lat + h), (lon - h, lat + h), (lon - h, lat - h)]
};
(0..6_i64)
.flat_map(|i| {
let d = i as f64 * 0.0004;
Way::from_lonlat(100 + i, &ring(18.0686 + d, 59.3293 + d, 0.00008), &[("building", "yes")], &theme)
})
.collect()
}
/// The vertex count the next device draw will use, read out of the live renderer.
fn device_draw_count(renderer: &egui_wgpu::RenderState) -> u32 {
let g = renderer.renderer.read();
let r = g.callback_resources.get::<Map3dRenderer>().expect("the map3d renderer is installed");
r.vbuf_for_test().map_or(0, |(_, n)| n)
}
/// **THE RED LINE.** Paint, then set a route, then paint again: the device's draw count
/// must GROW, because the ribbon is real triangles.
///
/// RED: put `&mut self.mesh` back in `Map3D::set_route` (in place of `self.mesh_mut()`) —
/// the backend's payload cache is then never invalidated, the second paint hands the device
/// the first frame's payload again, and `after == before` while every CPU-side count has
/// moved.
#[test]
fn a_route_set_after_the_first_paint_reaches_the_device() {
let setup = facett_core::render::gpu::adapter_wgpu::facett_wgpu_options().wgpu_setup;
let rs = egui_kittest::wgpu::create_render_state(setup, Default::default());
let info = rs.adapter.get_info();
eprintln!("LOOK: adapter {} [{:?}/{:?}]", info.name, info.device_type, info.backend);
facett_map3d::gpu::install_renderer(&rs);
// Kept so the test can read the renderer the harness is about to own.
let probe = rs.clone();
let ways = ways();
let map = Rc::new(RefCell::new(Map3D::from_geometry("route probe", &ways).with_gpu(true)));
assert!(map.borrow().gpu_lane_selected(), "fixture: the wgpu lane is selected");
let drive = map.clone();
let mut harness = egui_kittest::Harness::builder()
.with_size(CANVAS)
.renderer(egui_kittest::wgpu::WgpuTestRenderer::from_render_state(rs))
.build_ui(move |ui| {
facett_core::set_theme(ui.ctx(), Theme::default());
facett_core::Facet::ui(&mut *drive.borrow_mut(), ui);
});
// ── FRAME(S) 1: the payload is built by the layout pass and uploaded by the render.
for _ in 0..3 {
harness.step();
harness.render().expect("the wgpu backend produced a frame");
}
let before = device_draw_count(&probe);
let tris_before = map.borrow().triangle_count();
eprintln!("LOOK: before the route — device draw count {before}, mesh triangles {tris_before}");
// NON-VACUITY, three ways. Without all three, `after > before` could be true of
// nothing, or of a pane the GPU never touched.
assert!(before > 0, "nothing was uploaded at all, so there is no baseline to grow from");
assert_eq!(
map.borrow().lane_flag(),
facett_core::render::lane::LaneFlag::Gpu,
"the GPU lane must actually be the one painting, or the device payload is irrelevant"
);
// ── THE ROUTE, set AFTER the first paint — the whole point.
let bb_lon = 18.0686;
let bb_lat = 59.3293;
map.borrow_mut().set_route(
&ways,
&[
(bb_lon as f32, bb_lat as f32),
((bb_lon + 0.0010) as f32, (bb_lat + 0.0010) as f32),
((bb_lon + 0.0020) as f32, (bb_lat + 0.0006) as f32),
],
);
let tris_after_mesh = map.borrow().triangle_count();
assert!(
tris_after_mesh > tris_before,
"fixture: the route must add real triangles to the mesh ({tris_before} -> \
{tris_after_mesh}); otherwise this test would pass on a route that was never built"
);
// ── FRAME(S) 2.
for _ in 0..3 {
harness.step();
harness.render().expect("the wgpu backend produced a frame");
}
let after = device_draw_count(&probe);
eprintln!("LOOK: after the route — device draw count {after}, mesh triangles {tris_after_mesh}");
assert!(
after > before,
"the route reached the MESH ({tris_before} -> {tris_after_mesh} triangles) and not \
the DEVICE: the draw count is still {after} against {before}. The backend caches \
its GPU payload behind `cache.borrow().is_none()` (backend.rs:683), so a mesh \
mutation that skips `Map3D::mesh_mut`'s `reset_cache` is invisible on the GPU lane \
forever — every count green, no pixel."
);
}