facett-map/tests/draw_count.rs
//! **GG audit row 21, second half: how many draws a frame costs, counted where they are
//! recorded.**
//!
//! # The defect
//!
//! The tile lane walks its hand-over band-major and, per `(band, tile)` group, submits
//! one `draw_indirect` per chunk per stream — except the ROAD stream, which submitted
//! TWO per chunk: the dark casing and the bright fill, as two draws off the same count
//! with a different uniform bound between them. So a frame of `G` groups cost more draws
//! than it had `(group, stream, chunk)` units, and the extra ones were not carrying any
//! extra geometry.
//!
//! # The oracle
//!
//! [`facett_map::gpu::draw_calls_total`] is bumped by every `draw_indexed_indirect` the
//! map records (process-wide, like `offscreen_target_allocs` and for the same reason —
//! the renderer lives in egui's `CallbackResources`); [`facett_map::gpu::draw_units_total`]
//! counts the `(group, stream, chunk)` units the same walk visited, and
//! [`facett_map::gpu::draw_band_runs_total`] the band runs. Per frame, the claim is
//! **draws ≤ units**: one draw per chunk per stream kind, the casing and its fill as two
//! INSTANCES of one draw rather than two draws.
//!
//! # What the row asks for, and what this does not claim
//!
//! The row's ideal is *"draws per frame ≤ bands"*. That needs every tile in a band run to
//! be drawable by ONE command, i.e. one vertex buffer and one bind group across tiles —
//! a slab arena for the tile cache plus per-instance tile transforms in the pinned
//! `draw.wgsl`. `multi_draw_indirect` alone cannot do it: it cannot change the vertex
//! buffer or the bind group between the draws it issues, and every tile has its own of
//! both. The ratio draws/bands is therefore MEASURED and filed as a matrix row
//! (`gg21_draws_le_bands`, honest red until the arena lands), not asserted here.
//!
//! Run (needs a real device and the GPU opt-in):
//! ```text
//! FACETT_TEST_GPU=integrated cargo test -p facett-map --features wgpu --test draw_count -- \
//! --test-threads=1 --nocapture
//! ```
#![cfg(feature = "wgpu")]
use facett_core::render::lane;
use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
use facett_map::Way;
const CANVAS: egui::Vec2 = egui::vec2(520.0, 400.0);
#[path = "common/one_gpu.rs"]
mod one_gpu;
#[path = "common/gpu_gate.rs"]
mod gpu_gate;
fn ring(id: i64, lon: f64, lat: f64, w: f64, tags: &[(&str, &str)]) -> Way {
let t: Vec<(String, String)> = tags.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
let r = vec![(lon - w, lat - w), (lon + w, lat - w), (lon + w, lat + w), (lon - w, lat + w), (lon - w, lat - w)];
Way::from_lonlat_with(id, &r, &t, &facett_map::MapPalette::cartographic(false)).expect("a closed ring is a way")
}
fn line(id: i64, lon0: f64, lon1: f64, lat: f64, tags: &[(&str, &str)]) -> Way {
let t: Vec<(String, String)> = tags.iter().map(|(k, v)| ((*k).to_string(), (*v).to_string())).collect();
Way::from_lonlat_with(id, &[(lon0, lat), (lon1, lat)], &t, &facett_map::MapPalette::cartographic(false))
.expect("a 2-vertex line is a way")
}
fn fresh_render_state() -> egui_wgpu::RenderState {
let setup = facett_core::render::gpu::adapter_wgpu::facett_wgpu_options().wgpu_setup;
let rs = egui_kittest::wgpu::create_render_state(setup, Default::default());
facett_map::gpu::install_renderer(&rs);
rs
}
/// **The N-tile viewport**: `TILES` tiles, each carrying one way of every class the
/// style ladder bands — the same fixture `gpu_lane_paint_order.rs` bounds its group
/// census with, so `groups` here is the number that test already guards.
const TILES: u64 = 8;
const KINDS: &[(&[(&str, &str)], bool)] = &[
(&[("natural", "water")], true),
(&[("leisure", "park")], true),
(&[("building", "yes")], true),
(&[("landuse", "industrial")], true),
(&[("natural", "coastline")], false),
(&[("waterway", "river")], false),
(&[("highway", "footway")], false),
(&[("highway", "motorway")], false),
(&[("highway", "residential")], false),
(&[("highway", "motorway"), ("bridge", "yes")], false),
(&[("highway", "motorway"), ("tunnel", "yes")], false),
];
fn n_tile_facet() -> MapFacet {
let mut ways = Vec::new();
let mut id = 1i64;
for tile in 1..=TILES {
for (k, closed) in KINDS {
let lon = 18.0 + (tile as f64) * 0.01;
let mut w = if *closed { ring(id, lon, 59.33, 0.002, k) } else { line(id, lon, lon + 0.002, 59.33, k) };
w.tile_key = tile;
ways.push(w);
id += 1;
}
}
let view = MapView::new(LatLon::new(59.33, 18.045), 12.0, (CANVAS.x, CANVAS.y));
MapFacet::new("gg21-draws", view, MapTheme::default()).with_layer(Box::new(WaysLayer::new("osm-ways", ways)))
}
/// Per-frame figures over `steps` rendered frames: `(lane frames, draws, units, band
/// runs, median frame ms)`. Every step is rendered — the upload-once latch puts the
/// payload on the first frame's callback (`gpu_lane_paint_order::drive`).
fn drive(mut f: MapFacet, steps: u32) -> (u64, u64, u64, u64, f64) {
use egui_kittest::wgpu::WgpuTestRenderer;
let _turn = one_gpu::hold();
let frames0 = lane::lane_use().frames();
let draws0 = facett_map::gpu::draw_calls_total();
let units0 = facett_map::gpu::draw_units_total();
let bands0 = facett_map::gpu::draw_band_runs_total();
let mut h = egui_kittest::Harness::builder()
.with_size(CANVAS)
.renderer(WgpuTestRenderer::from_render_state(fresh_render_state()))
.build_ui(move |ui| f.ui(ui));
let mut ms = Vec::with_capacity(steps as usize);
for _ in 0..steps {
let t0 = std::time::Instant::now();
h.render().expect("the wgpu backend produced a frame");
ms.push(t0.elapsed().as_secs_f64() * 1e3);
h.step();
}
ms.sort_by(f64::total_cmp);
(
lane::lane_use().frames() - frames0,
facett_map::gpu::draw_calls_total() - draws0,
facett_map::gpu::draw_units_total() - units0,
facett_map::gpu::draw_band_runs_total() - bands0,
ms[ms.len() / 2],
)
}
#[test]
fn a_frame_records_one_draw_per_chunk_per_stream_and_the_bands_ratio_is_measured() {
if !gpu_gate::gated("facett-map", "a_frame_records_one_draw_per_chunk_per_stream_and_the_bands_ratio_is_measured") {
return;
}
let f = n_tile_facet();
let (groups, tiles) = f.gpu_group_census_for_test(&[0]);
assert_eq!(tiles as u64, TILES, "the fixture must really span {TILES} tiles");
const STEPS: u32 = 12;
let (frames, draws, units, band_runs, median_ms) = drive(f, STEPS);
assert!(frames > 0, "the GPU lane encoded no frames — this measured the CPU painter");
assert!(units > 0 && band_runs > 0, "the frame walked no draw units at all; the fixture drew nothing");
let per = |n: u64| n as f64 / frames as f64;
let (d, u, b) = (per(draws), per(units), per(band_runs));
eprintln!(
"LOOK: [gg21 draws] {frames} lane frames over {STEPS} steps, {groups} groups / {tiles} tiles: \
{d:.1} draws/frame, {u:.1} (group,stream,chunk) units/frame, {b:.1} band runs/frame, \
draws/bands = {:.2}, draws/units = {:.3}, median frame {median_ms:.2} ms",
d / b,
d / u,
);
// THE CLAIM: one draw per unit. The road stream's casing+fill pair is two instances
// of one draw, not two draws.
assert!(
draws <= units,
"{d:.1} draws for {u:.1} units per frame — some stream is submitting more than one draw per chunk"
);
nornir_testmatrix::functional_status(
"facett-map",
"gg21_one_draw_per_chunk_per_stream",
draws <= units,
&format!("{d:.1} draws / {u:.1} units per frame over {groups} groups"),
);
// THE ROW'S IDEAL, measured and filed, not asserted — see the module doc.
nornir_testmatrix::functional_status(
"facett-map",
"gg21_draws_le_bands",
draws <= band_runs,
&format!("{d:.1} draws vs {b:.1} band runs per frame ({tiles} tiles) — needs the tile slab arena"),
);
}
/// **What this box's adapters can do about it** — printed, so the report and the arena
/// follow-up start from measured feature bits rather than a guess. wgpu 29 has no gate
/// on `multi_draw_indirect` itself (it is emulated as a loop where the backend lacks
/// it); `MULTI_DRAW_INDIRECT_COUNT` and `INDIRECT_FIRST_INSTANCE` are the two that
/// decide whether a GPU-side draw count and per-draw tile slots are available.
#[test]
fn the_adapters_report_their_indirect_draw_features() {
let instance = wgpu::Instance::default();
let adapters = pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()));
assert!(!adapters.is_empty(), "no wgpu adapter on this host");
for a in &adapters {
let f = a.features();
let i = a.get_info();
eprintln!(
"LOOK: [gg21 features] {} ({:?}, {:?}): MULTI_DRAW_INDIRECT_COUNT={} INDIRECT_FIRST_INSTANCE={} \
max_buffer_size={} max_storage_buffer_binding_size={}",
i.name,
i.backend,
i.device_type,
f.contains(wgpu::Features::MULTI_DRAW_INDIRECT_COUNT),
f.contains(wgpu::Features::INDIRECT_FIRST_INSTANCE),
a.limits().max_buffer_size,
a.limits().max_storage_buffer_binding_size,
);
}
}