Skip to content

Commit

Permalink
Enhance many_cubes stress test use cases (#9596)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
superdump authored Sep 2, 2023
1 parent 02b520b commit 40c6b3b
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 38 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
185 changes: 147 additions & 38 deletions examples/stress_tests/many_cubes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, Self::Err> {
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 {
Expand All @@ -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<Args>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
material_assets: ResMut<Assets<StandardMaterial>>,
images: ResMut<Assets<Image>>,
) {
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;
Expand All @@ -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()
});
Expand All @@ -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,
Expand All @@ -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()
});
Expand All @@ -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<Image>) -> Vec<Handle<Image>> {
let mut color_rng = StdRng::seed_from_u64(42);
let color_bytes: Vec<u8> = (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<Image>],
assets: &mut Assets<StandardMaterial>,
) -> Vec<Handle<StandardMaterial>> {
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
Expand All @@ -159,9 +259,18 @@ fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
}

// System for rotating the camera
fn move_camera(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera>>) {
fn move_camera(
time: Res<Time>,
args: Res<Args>,
mut camera_query: Query<&mut Transform, With<Camera>>,
) {
let mut camera_transform = camera_query.single_mut();
let delta = time.delta_seconds() * 0.15;
let delta = 0.15
* if args.benchmark {
1.0 / 60.0
} else {
time.delta_seconds()
};
camera_transform.rotate_z(delta);
camera_transform.rotate_x(delta);
}
Expand Down

0 comments on commit 40c6b3b

Please sign in to comment.