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