Add GPU-accelerated rendering: cpu/gpu/hybrid RenderBackend (Phase 8)
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
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
//! `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)"),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user