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

nordisk/facett

facett-map3d/benches/heavy_render.rs

//! **HEAVY render / throughput benchmark** — a large synthetic OSM tile driven
//! through the whole CPU render pipeline, benched **per part-step** (the Bencher LAW
//! in `.nornir/render-engine.md`: bench the PARTS, not just "draw map") plus the
//! end-to-end rollup.
//!
//! The tile: an `N×N` town grid (thousands of building footprints) + a road network +
//! landuse + a lake — a real city-scale mesh (hundreds of thousands of triangles).
//! Set `MAP3D_BENCH_TOWN=<n>` to change the grid side (default 64 → 4096 buildings).
//!
//! Steps benched, each with a triangle/element throughput:
//! - `extrude`         — ways → [`Mesh3`] (the tessellation + z-layering)
//! - `classify`        — the korp-seam layer classification
//! - `shade`           — project + frustum-clip + cull + light + depth-sort (the kernel)
//! - `scissor`         — the rect-scissor bug-oracle over every face
//! - `backend.analyze` — the full [`RenderBackend`] conformance oracle (rollup)
//!
//! Run: `CARGO_TARGET_DIR=… cargo bench -p facett-map3d`

use std::hint::black_box;

use criterion::{Criterion, Throughput, criterion_group, criterion_main};

use facett_core::Theme;
use facett_map::Way;
use facett_map3d::extrude::{build, build_with_water_terrain_styled, GroundTerrain, RoofStyle, WaterMesh};
use facett_map3d::{
    load_layers, scissor_report, shade_mesh_faces, CpuPainterBackend, Map3D, OrbitCamera, RenderBackend, RenderScene,
    SceneFlags, Viewport,
};

/// The town-grid side length (`n×n` buildings). `MAP3D_BENCH_TOWN` overrides it.
fn town_side() -> usize {
    std::env::var("MAP3D_BENCH_TOWN").ok().and_then(|v| v.parse().ok()).unwrap_or(64)
}

/// Build a large synthetic OSM tile: an `n×n` grid of building footprints, a road
/// running along every grid row + column (the street network), a landuse block, and
/// one lake — a realistic dense-city way set.
fn heavy_tile(n: usize) -> Vec<Way> {
    let th = Theme::default();
    let (lon0, lat0) = (17.70_f64, 59.61_f64);
    let step = 0.0016;
    let bsz = 0.0010;
    let mut ways = Vec::with_capacity(n * n + 2 * n + 2);
    let mut id = 1i64;
    // Buildings.
    for r in 0..n {
        for c in 0..n {
            let bx = lon0 + c as f64 * step;
            let by = lat0 + r as f64 * step;
            let ring = [(bx, by), (bx + bsz, by), (bx + bsz, by + bsz), (bx, by + bsz), (bx, by)];
            let tags = vec![("building".to_string(), "yes".to_string())];
            ways.push(Way::from_lonlat(id, &ring, &tags, &th).unwrap());
            id += 1;
        }
    }
    // Roads: one horizontal + one vertical centreline per grid line.
    for r in 0..n {
        let by = lat0 + r as f64 * step - step * 0.4;
        let line: Vec<(f64, f64)> = (0..=n).map(|c| (lon0 + c as f64 * step, by)).collect();
        ways.push(Way::line(id, "road", line, egui::Color32::GRAY, 2.0));
        id += 1;
    }
    for c in 0..n {
        let bx = lon0 + c as f64 * step - step * 0.4;
        let line: Vec<(f64, f64)> = (0..=n).map(|r| (bx, lat0 + r as f64 * step)).collect();
        ways.push(Way::line(id, "road", line, egui::Color32::GRAY, 2.0));
        id += 1;
    }
    // A landuse block + a lake.
    let big = n as f64 * step;
    ways.push(Way::from_lonlat(
        id,
        &[(lon0, lat0), (lon0 + big, lat0), (lon0 + big, lat0 + big), (lon0, lat0 + big), (lon0, lat0)],
        &[("landuse".to_string(), "residential".to_string())],
        &th,
    ).unwrap());
    id += 1;
    ways.push(Way::from_lonlat(
        id,
        &[(lon0, lat0), (lon0 + big * 0.3, lat0), (lon0 + big * 0.3, lat0 + big * 0.3), (lon0, lat0 + big * 0.3), (lon0, lat0)],
        &[("natural".to_string(), "water".to_string())],
        &th,
    ).unwrap());
    ways
}

fn bench(c: &mut Criterion) {
    let n = town_side();
    let ways = heavy_tile(n);
    let (mesh, tiles) = build(&ways, 0.22);
    let tris = mesh.triangle_count() as u64;
    eprintln!("[heavy_render] town {n}x{n}: {} ways → {tiles} footprints, {tris} triangles", ways.len());

    let mut cam = OrbitCamera::default();
    cam.settle();
    let center = egui::pos2(600.0, 600.0);
    let scale = 600.0;
    let bg = egui::Color32::from_rgb(20, 24, 40);
    let rect = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1200.0, 1200.0));
    let water = WaterMesh::default();

    let mut g = c.benchmark_group("map3d.heavy");

    // ── part-step: extrude (ways → mesh) ──
    g.throughput(Throughput::Elements(ways.len() as u64));
    g.bench_function("extrude", |b| b.iter(|| black_box(build(black_box(&ways), 0.22))));

    // ── part-step: extrude WITH pitched roofs + OSM colours (round-2 cost) ──
    // The same extrude, but growing real gabled/hipped roof geometry + colours — so
    // the Loki bencher can price the roof geometry against the flat-cap `extrude` arm.
    g.throughput(Throughput::Elements(ways.len() as u64));
    g.bench_function("extrude_roofed", |b| {
        b.iter(|| {
            black_box(build_with_water_terrain_styled(
                black_box(&ways),
                0.22,
                GroundTerrain::Flat,
                RoofStyle::Pitched,
            ))
        })
    });

    // ── part-step: korp-seam classify ──
    g.throughput(Throughput::Elements(ways.len() as u64));
    g.bench_function("classify", |b| b.iter(|| black_box(load_layers(black_box(&ways)))));

    // ── part-step: shade (project + clip + cull + light + sort) ──
    g.throughput(Throughput::Elements(tris));
    g.bench_function("shade", |b| {
        b.iter(|| black_box(shade_mesh_faces(black_box(&mesh), &cam, center, scale, bg)))
    });

    // ── part-step: scissor over every projected face ──
    let faces = shade_mesh_faces(&mesh, &cam, center, scale, bg);
    g.throughput(Throughput::Elements(faces.len() as u64));
    g.bench_function("scissor", |b| b.iter(|| black_box(scissor_report(black_box(&faces), rect))));

    // ── rollup: the full RenderBackend analyze (the conformance oracle) ──
    let m = Map3D::from_ways("bench", &ways, 0.22);
    let scene = RenderScene {
        mesh: m.mesh(),
        water: &water,
        camera: m.render_camera(),
        sun: m.sun(),
        viewport: Viewport::from_rect(rect),
        bg,
        flags: SceneFlags::default(),
        sky_lut: None,
        clock_s: 0.0,
        // The scene's world scale, off the SAME `Map3D` the mesh and camera come
        // from — not `None`. `None` is the synthetic-fixture signal that puts haze,
        // SSAO and the water plane on their legacy absolutes, so a bench that hands
        // it would measure a different backend path than the demo runs.
        du_per_m: m.design_units_per_metre(),
                overlay: None,
                overlay_emissive: &[],
    };
    g.throughput(Throughput::Elements(m.mesh().triangle_count() as u64));
    g.bench_function("backend_analyze", |b| b.iter(|| black_box(CpuPainterBackend.analyze(black_box(&scene)))));

    g.finish();

    // ── the SIGTUNA TOUR per-frame cost (the demo's opening pose) ────────────────
    // The cinematic tour opens on a HIGH air-circle framing the whole extruded town —
    // the heaviest view (every prism in frame). Bench the real per-frame render cost
    // at that pose (pose derivation + project + clip + cull + light + sort), so the
    // Loki bencher tracks the demo's opening frame time as it grows. Host-stamped,
    // plain numbers, criterion (light-verify locally; HEAVY defers to Loki).
    let mut tour_map = Map3D::from_geometry("tour bench", &ways).with_tour(true);
    tour_map.set_clock(4.0); // mid air-circle (whole skyline in frame)
    let circle_cam = tour_map.render_camera();
    let tour_mesh = tour_map.mesh().clone();
    let circle_tris = tour_mesh.triangle_count() as u64;
    eprintln!(
        "[heavy_render] tour opening frame: {circle_tris} tris, {} houses in frame",
        tour_map.buildings_visible()
    );
    let mut gt = c.benchmark_group("map3d.tour");
    gt.throughput(Throughput::Elements(circle_tris));
    gt.bench_function("frame_shade_circle", |b| {
        b.iter(|| black_box(shade_mesh_faces(black_box(&tour_mesh), &circle_cam, center, scale, bg)))
    });

    // ── the SAME opening-frame cost, but with PITCHED ROOFS + OSM colours ─────────
    // Roofs ON vs OFF at the identical HIGH air-circle pose: `frame_shade_circle`
    // (flat) vs `frame_shade_circle_roofed` (pitched) is the roof-geometry frame-time
    // delta the round-2 feature must justify. The roofed mesh carries more triangles
    // (the ridges/slopes), so throughput is stamped on ITS own triangle count.
    let mut roofed_map = Map3D::from_geometry_roofed("tour bench roofed", &ways).with_tour(true);
    roofed_map.set_clock(4.0);
    let roofed_cam = roofed_map.render_camera();
    let roofed_mesh = roofed_map.mesh().clone();
    let roofed_tris = roofed_mesh.triangle_count() as u64;
    eprintln!(
        "[heavy_render] tour opening frame (roofed): {roofed_tris} tris, {} houses, {} pitched, {} coloured",
        roofed_map.buildings_visible(),
        roofed_map.pitched_roofs(),
        roofed_map.coloured_buildings()
    );
    gt.throughput(Throughput::Elements(roofed_tris));
    gt.bench_function("frame_shade_circle_roofed", |b| {
        b.iter(|| black_box(shade_mesh_faces(black_box(&roofed_mesh), &roofed_cam, center, scale, bg)))
    });
    gt.finish();
}

criterion_group!(benches, bench);
criterion_main!(benches);