1a0ccd8b17
Splits RenderBackend's per-voxel face-visibility extraction and tile shading out as GPU-offloadable steps (wgpu compute shaders), while keeping greedy-mesh merge/compaction CPU-only per the plan's "partial GPU rendering" design. RENDER_BACKEND=cpu|gpu|hybrid selects the strategy, falling back to cpu automatically (logged) if no compatible GPU adapter is found. Verified against a real GPU: all tests pass, including ones asserting byte-identical output between the cpu and gpu backends; a new benchmark example honestly shows cpu currently outperforming gpu/hybrid at realistic batch sizes since each call is its own dispatch/readback round trip rather than batched across a whole render batch (documented as a follow-up optimization). Also fixes two real, pre-existing gaps found while validating the worker's actual `docker build`: a missing .dockerignore was sending the local multi-GB target/ dir into the build context, and the Dockerfile's rust:1.80 pin was already too old for current transitive dependency MSRVs (bumped to rust:1.97). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
104 lines
4.1 KiB
Rust
104 lines
4.1 KiB
Rust
//! `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<ColumnPixel> {
|
|
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)"),
|
|
}
|
|
}
|