rusting_engine 2.6.0

Vulkan 3D game engine with GPU-accelerated physics for massive physics-heavy scenes
//! Fixed render benchmark with stored baselines.
//!
//! ```sh
//! cargo run --release --example render_bench -- eco benchmarks/my-gpu-eco.json
//! cargo run --release --example render_bench -- eco --screens 6 --bears 5000 --bear-updates 500
//! ```
//!
//! `--screens N` adds N camera screens at `--screen-size WxH` (320x180 by
//! default); `--bears N` adds N static spheres sharing one mesh and
//! material, `--bear-updates N` moves that many of them every frame, and
//! `--screen-every N` draws each feed every N frames, `--no-bodies` drops
//! the 1,000 GPU physics bodies, `--lights N` adds a night street of N point
//! and spot lights, `--city N` adds a city of N buildings walked at street
//! level, `--effects` adds 50K GPU particles, 2K see-through spheres and
//! 300 skinned characters, `--probes` adds a 64-probe light probe grid over
//! the scene, `--rain` adds 50K CPU raindrops stopped by a heightfield and
//! roofs, and `--extent WxH` changes the frame size.
//! `--repeat N` runs N times, prints the minimum and median mean frame
//! time, warns when they spread over 20%, and keeps the median run.
//!
//! Renders the benchmark scene along its camera path (600 frames at
//! 1920x1080, offscreen) and prints the report as JSON. With a baseline path:
//! a missing file is written from this run; an existing one is compared, and
//! any material regression is printed and exits with status 1.

use rusting_engine::rendering::benchmark::{
    gpu_busy_percent, repeat_spread, run_render_benchmark_with,
    RenderBenchmarkReport,
};
use rusting_engine::rendering::init_vulkan_headless;
use rusting_engine::runtime::{
    QualityProfile, RenderBenchmarkExtras, RENDER_BENCHMARK_FRAMES,
};

fn main() {
    let mut extras = RenderBenchmarkExtras {
        screen_size: [320, 180],
        ..RenderBenchmarkExtras::default()
    };
    let mut extent = [1920, 1080];
    let mut repeats = 1;
    let mut positional = Vec::new();
    let mut arguments = std::env::args().skip(1);
    while let Some(argument) = arguments.next() {
        let mut value = || {
            arguments
                .next()
                .unwrap_or_else(|| panic!("{argument} needs a value"))
        };
        let size = |text: String| -> [u32; 2] {
            let (w, h) = text.split_once('x').expect("size is WxH");
            [w.parse().expect("width"), h.parse().expect("height")]
        };
        match argument.as_str() {
            "--screens" => extras.screens = value().parse().expect("count"),
            "--screen-size" => extras.screen_size = size(value()),
            "--bears" => extras.bears = value().parse().expect("count"),
            "--bear-updates" => {
                extras.bear_updates = value().parse().expect("count");
            }
            "--screen-every" => {
                extras.screen_every = value().parse().expect("frames");
            }
            "--no-bodies" => extras.without_bodies = true,
            "--lights" => extras.lights = value().parse().expect("count"),
            "--city" => extras.city = value().parse().expect("count"),
            "--effects" => extras.effects = true,
            "--probes" => extras.probes = true,
            "--rain" => extras.rain = true,
            "--extent" => extent = size(value()),
            "--repeat" => repeats = value().parse().expect("count"),
            _ => positional.push(argument),
        }
    }
    let mut arguments = positional.into_iter();
    let quality = match arguments.next().as_deref().unwrap_or("auto") {
        "auto" => QualityProfile::Auto,
        "eco" => QualityProfile::Eco,
        "balanced" => QualityProfile::Balanced,
        "high" => QualityProfile::High,
        other => {
            panic!("unknown quality `{other}`: auto, eco, balanced or high")
        }
    };
    let baseline = arguments.next();

    let base = init_vulkan_headless();
    let mut runs: Vec<RenderBenchmarkReport> = (0..repeats.max(1))
        .map(|_| {
            let busy = gpu_busy_percent();
            let mut report = run_render_benchmark_with(
                base.queue.clone(),
                extent,
                quality,
                RENDER_BENCHMARK_FRAMES,
                extras,
            )
            .unwrap_or_else(|error| panic!("benchmark failed: {error}"));
            report.gpu_busy_percent =
                busy.zip(gpu_busy_percent()).map(Into::into);
            if let Some(before) = busy.filter(|busy| *busy > 10) {
                eprintln!("warning: the GPU was {before}% busy before the run");
            }
            report
        })
        .collect();
    let means: Vec<f64> =
        runs.iter().map(|run| run.frame_time.mean_ms).collect();
    let (min, median, spread) = repeat_spread(&means);
    let mut report = runs
        .swap_remove(means.iter().position(|mean| *mean == median).unwrap());
    if means.len() > 1 {
        eprintln!(
            "{} runs: mean frame min {min:.2} ms, median {median:.2} ms, spread {:.0}%",
            means.len(),
            spread * 100.0
        );
        if spread > 0.2 {
            eprintln!("warning: the runs spread over 20%; the GPU or CPU was not steady");
        }
        report.repeat_frame_means_ms = means;
    }
    let json = serde_json::to_string_pretty(&report).unwrap();
    println!("{json}");

    let Some(path) = baseline else { return };
    match std::fs::read_to_string(&path) {
        Ok(stored) => {
            let stored: RenderBenchmarkReport = serde_json::from_str(&stored)
                .unwrap_or_else(|error| panic!("{path}: {error}"));
            if !report.same_machine(&stored) {
                eprintln!(
                    "times not compared: {} {} against baseline {} {}",
                    report.device, report.driver, stored.device, stored.driver
                );
                for drift in report.time_growth(&stored) {
                    eprintln!("warning: {drift}");
                }
            }
            let regressions = report.regressions(&stored);
            if regressions.is_empty() {
                eprintln!("no regressions against {path}");
            } else {
                for regression in &regressions {
                    eprintln!("regression: {regression}");
                }
                std::process::exit(1);
            }
        }
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
            std::fs::write(&path, json + "\n")
                .unwrap_or_else(|error| panic!("{path}: {error}"));
            eprintln!("wrote baseline {path}");
        }
        Err(error) => panic!("{path}: {error}"),
    }
}