Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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