From 40c6b3b91e9ae8136259d705a483e833315a42d5 Mon Sep 17 00:00:00 2001 From: Robert Swain Date: Sat, 2 Sep 2023 16:49:32 +0200 Subject: [PATCH] Enhance many_cubes stress test use cases (#9596) # Objective - Make `many_cubes` suitable for testing various parts of the upcoming batching work. ## Solution - Use `argh` for CLI. - Default to the sphere layout as it is more useful for benchmarking. - Add a benchmark mode that advances the camera by a fixed step to render the same frames across runs. - Add an option to vary the material data per-instance. The color is randomized. - Add an option to generate a number of textures and randomly choose one per instance. - Use seeded `StdRng` for deterministic random numbers. --- Cargo.toml | 1 + examples/stress_tests/many_cubes.rs | 185 ++++++++++++++++++++++------ 2 files changed, 148 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 35a675b62f244..9030800ffd39e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -261,6 +261,7 @@ bytemuck = "1.7" # Needed to poll Task examples futures-lite = "1.11.3" crossbeam-channel = "0.5.0" +argh = "0.1.12" [[example]] name = "hello_world" diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index f8194589b9328..29efbfda2c2f8 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -3,23 +3,68 @@ //! To measure performance realistically, be sure to run this in release mode. //! `cargo run --example many_cubes --release` //! -//! By default, this arranges the meshes in a cubical pattern, where the number of visible meshes -//! varies with the viewing angle. You can choose to run the demo with a spherical pattern that +//! By default, this arranges the meshes in a spherical pattern that //! distributes the meshes evenly. //! -//! To start the demo using the spherical layout run -//! `cargo run --example many_cubes --release sphere` +//! See `cargo run --example many_cubes --release -- --help` for more options. -use std::f64::consts::PI; +use std::{f64::consts::PI, str::FromStr}; +use argh::FromArgs; use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, math::{DVec2, DVec3}, prelude::*, + render::render_resource::{Extent3d, TextureDimension, TextureFormat}, window::{PresentMode, WindowPlugin}, }; +use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng}; + +#[derive(FromArgs, Resource)] +/// `many_cubes` stress test +struct Args { + /// how the cube instances should be positioned. + #[argh(option, default = "Layout::Sphere")] + layout: Layout, + + /// whether to step the camera animation by a fixed amount such that each frame is the same across runs. + #[argh(switch)] + benchmark: bool, + + /// whether to vary the material data in each instance. + #[argh(switch)] + vary_material_data: bool, + + /// the number of different textures from which to randomly select the material base color. 0 means no textures. + #[argh(option, default = "0")] + material_texture_count: usize, +} + +#[derive(Default, Clone)] +enum Layout { + Cube, + #[default] + Sphere, +} + +impl FromStr for Layout { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "cube" => Ok(Self::Cube), + "sphere" => Ok(Self::Sphere), + _ => Err(format!( + "Unknown layout value: '{}', valid options: 'cube', 'sphere'", + s + )), + } + } +} fn main() { + let args: Args = argh::from_env(); + App::new() .add_plugins(( DefaultPlugins.set(WindowPlugin { @@ -32,28 +77,36 @@ fn main() { FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin::default(), )) + .insert_resource(args) .add_systems(Startup, setup) .add_systems(Update, (move_camera, print_mesh_count)) .run(); } +const WIDTH: usize = 200; +const HEIGHT: usize = 200; + fn setup( mut commands: Commands, + args: Res, mut meshes: ResMut>, - mut materials: ResMut>, + material_assets: ResMut>, + images: ResMut>, ) { warn!(include_str!("warning_string.txt")); - const WIDTH: usize = 200; - const HEIGHT: usize = 200; + let args = args.into_inner(); + let images = images.into_inner(); + let material_assets = material_assets.into_inner(); + let mesh = meshes.add(Mesh::from(shape::Cube { size: 1.0 })); - let material = materials.add(StandardMaterial { - base_color: Color::PINK, - ..default() - }); - match std::env::args().nth(1).as_deref() { - Some("sphere") => { + let material_textures = init_textures(args, images); + let materials = init_materials(args, &material_textures, material_assets); + + let mut material_rng = StdRng::seed_from_u64(42); + match args.layout { + Layout::Sphere => { // NOTE: This pattern is good for testing performance of culling as it provides roughly // the same number of visible meshes regardless of the viewing angle. const N_POINTS: usize = WIDTH * HEIGHT * 4; @@ -65,8 +118,8 @@ fn setup( fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS); let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi); commands.spawn(PbrBundle { - mesh: mesh.clone_weak(), - material: material.clone_weak(), + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), transform: Transform::from_translation((radius * unit_sphere_p).as_vec3()), ..default() }); @@ -86,14 +139,14 @@ fn setup( } // cube commands.spawn(PbrBundle { - mesh: mesh.clone_weak(), - material: material.clone_weak(), + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), transform: Transform::from_xyz((x as f32) * 2.5, (y as f32) * 2.5, 0.0), ..default() }); commands.spawn(PbrBundle { - mesh: mesh.clone_weak(), - material: material.clone_weak(), + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), transform: Transform::from_xyz( (x as f32) * 2.5, HEIGHT as f32 * 2.5, @@ -102,14 +155,14 @@ fn setup( ..default() }); commands.spawn(PbrBundle { - mesh: mesh.clone_weak(), - material: material.clone_weak(), + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), transform: Transform::from_xyz((x as f32) * 2.5, 0.0, (y as f32) * 2.5), ..default() }); commands.spawn(PbrBundle { - mesh: mesh.clone_weak(), - material: material.clone_weak(), + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), transform: Transform::from_xyz(0.0, (x as f32) * 2.5, (y as f32) * 2.5), ..default() }); @@ -123,20 +176,67 @@ fn setup( } } - // add one cube, the only one with strong handles - // also serves as a reference point during rotation - commands.spawn(PbrBundle { - mesh, - material, - transform: Transform { - translation: Vec3::new(0.0, HEIGHT as f32 * 2.5, 0.0), - scale: Vec3::splat(5.0), - ..default() - }, + commands.spawn(DirectionalLightBundle { ..default() }); +} + +fn init_textures(args: &Args, images: &mut Assets) -> Vec> { + let mut color_rng = StdRng::seed_from_u64(42); + let color_bytes: Vec = (0..(args.material_texture_count * 4)) + .map(|i| if (i % 4) == 3 { 255 } else { color_rng.gen() }) + .collect(); + color_bytes + .chunks(4) + .map(|pixel| { + images.add(Image::new_fill( + Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + pixel, + TextureFormat::Rgba8UnormSrgb, + )) + }) + .collect() +} + +fn init_materials( + args: &Args, + textures: &[Handle], + assets: &mut Assets, +) -> Vec> { + let capacity = if args.vary_material_data { + match args.layout { + Layout::Cube => (WIDTH - WIDTH / 10) * (HEIGHT - HEIGHT / 10), + Layout::Sphere => WIDTH * HEIGHT * 4, + } + } else { + args.material_texture_count + } + .max(1); + + let mut materials = Vec::with_capacity(capacity); + materials.push(assets.add(StandardMaterial { + base_color: Color::WHITE, + base_color_texture: textures.get(0).cloned(), ..default() - }); + })); - commands.spawn(DirectionalLightBundle { ..default() }); + let mut color_rng = StdRng::seed_from_u64(42); + let mut texture_rng = StdRng::seed_from_u64(42); + materials.extend( + std::iter::repeat_with(|| { + assets.add(StandardMaterial { + base_color: Color::rgb_u8(color_rng.gen(), color_rng.gen(), color_rng.gen()), + base_color_texture: textures.choose(&mut texture_rng).cloned(), + ..default() + }) + }) + .take(capacity - materials.len()), + ); + + materials } // NOTE: This epsilon value is apparently optimal for optimizing for the average @@ -159,9 +259,18 @@ fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 { } // System for rotating the camera -fn move_camera(time: Res