//! `cargo run --release --example benchmark` — compares tile-rasterization and section-meshing //! throughput across the `cpu`/`gpu`/`hybrid` `RenderBackend`s on a synthetic workload sized to //! match `main.rs`'s real batching (one `RENDER_PROFILE=server` xread batch's worth of chunks — //! see `config::batch_size_for`), run through the same rayon-parallel-batch shape `main.rs` uses. //! This is the Phase 8 plan's "measure throughput difference" verification step; `render::gpu`'s //! own tests already cover byte-for-byte output equivalence, so this only measures timing. use std::time::{Duration, Instant}; use mcmapper_worker::config::{batch_size_for, Profile}; use mcmapper_worker::mesh; use mcmapper_worker::render::{ ColumnPixel, CpuRenderBackend, GpuRenderBackend, HybridRenderBackend, RenderBackend, }; use rayon::prelude::*; fn synthetic_columns() -> Vec { let mut columns = Vec::with_capacity(256); for x in 0..16u8 { for z in 0..16u8 { let block_id = ((x as u16 + z as u16) % 20) + 1; columns.push(ColumnPixel { local_x: x, local_z: z, block_id, block_meta: 0 }); } } columns } /// A wavy, partially-solid section — not fully solid (which greedy-merges to almost nothing) or /// fully empty (no work at all), so meshing does a realistic amount of face-mask/merge work. fn synthetic_section(seed: i32) -> [u16; 4096] { let mut blocks = [0u16; 4096]; for y in 0..16i32 { for z in 0..16i32 { for x in 0..16i32 { let height = 6 + (x + z + seed) % 6; if y <= height { let block_id = 1 + ((x * 3 + z * 7 + y * 11 + seed) % 12) as u16; blocks[((y as usize) * 16 + z as usize) * 16 + x as usize] = block_id << 4; } } } } blocks } fn bench_tiles(backend: &dyn RenderBackend, columns: &[ColumnPixel], batch: usize) -> Duration { let start = Instant::now(); (0..batch).into_par_iter().for_each(|_| { backend.rasterize_tile(columns).expect("rasterize_tile must not fail on synthetic input"); }); start.elapsed() } fn bench_meshing(backend: &dyn RenderBackend, sections: &[[u16; 4096]]) -> Duration { let start = Instant::now(); sections.par_iter().for_each(|blocks| { let _ = mesh::mesh_section(blocks, backend); }); start.elapsed() } fn run_backend(name: &str, backend: &dyn RenderBackend, columns: &[ColumnPixel], sections: &[[u16; 4096]], batch: usize) { // One warmup pass per stage so pipeline/shader compilation and first-dispatch driver // overhead (real on GPU, one-time) doesn't get counted as steady-state throughput. backend.rasterize_tile(columns).unwrap(); let _ = mesh::mesh_section(§ions[0], backend); let tiles = bench_tiles(backend, columns, batch); let meshes = bench_meshing(backend, sections); println!( "{name:<8} tiles: {batch} in {tiles:>9.2?} ({:>8.1}/s) sections: {} in {meshes:>9.2?} ({:>8.1}/s)", batch as f64 / tiles.as_secs_f64(), sections.len(), sections.len() as f64 / meshes.as_secs_f64(), ); } #[tokio::main] async fn main() { let batch = batch_size_for(Profile::Server); let columns = synthetic_columns(); let sections: Vec<[u16; 4096]> = (0..batch as i32).map(synthetic_section).collect(); println!("Synthetic workload: {batch} tiles + {} sections per backend (RENDER_PROFILE=server batch size)\n", sections.len()); let cpu = CpuRenderBackend::new(); run_backend("cpu", &cpu, &columns, §ions, batch); match GpuRenderBackend::try_new().await { Some(gpu) => { println!("GPU adapter: {}", gpu.adapter_name()); run_backend("gpu", &gpu, &columns, §ions, batch); } None => println!("gpu (skipped — no compatible GPU adapter found)"), } match GpuRenderBackend::try_new().await { Some(gpu) => { let hybrid = HybridRenderBackend::new(gpu); run_backend("hybrid", &hybrid, &columns, §ions, batch); } None => println!("hybrid (skipped — no compatible GPU adapter found)"), } }