Skip to content

Commit 40c6b3b

Browse files
authored
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.
1 parent 02b520b commit 40c6b3b

File tree

2 files changed

+148
-38
lines changed

2 files changed

+148
-38
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ bytemuck = "1.7"
261261
# Needed to poll Task examples
262262
futures-lite = "1.11.3"
263263
crossbeam-channel = "0.5.0"
264+
argh = "0.1.12"
264265

265266
[[example]]
266267
name = "hello_world"

examples/stress_tests/many_cubes.rs

Lines changed: 147 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,68 @@
33
//! To measure performance realistically, be sure to run this in release mode.
44
//! `cargo run --example many_cubes --release`
55
//!
6-
//! By default, this arranges the meshes in a cubical pattern, where the number of visible meshes
7-
//! varies with the viewing angle. You can choose to run the demo with a spherical pattern that
6+
//! By default, this arranges the meshes in a spherical pattern that
87
//! distributes the meshes evenly.
98
//!
10-
//! To start the demo using the spherical layout run
11-
//! `cargo run --example many_cubes --release sphere`
9+
//! See `cargo run --example many_cubes --release -- --help` for more options.
1210
13-
use std::f64::consts::PI;
11+
use std::{f64::consts::PI, str::FromStr};
1412

13+
use argh::FromArgs;
1514
use bevy::{
1615
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
1716
math::{DVec2, DVec3},
1817
prelude::*,
18+
render::render_resource::{Extent3d, TextureDimension, TextureFormat},
1919
window::{PresentMode, WindowPlugin},
2020
};
21+
use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng};
22+
23+
#[derive(FromArgs, Resource)]
24+
/// `many_cubes` stress test
25+
struct Args {
26+
/// how the cube instances should be positioned.
27+
#[argh(option, default = "Layout::Sphere")]
28+
layout: Layout,
29+
30+
/// whether to step the camera animation by a fixed amount such that each frame is the same across runs.
31+
#[argh(switch)]
32+
benchmark: bool,
33+
34+
/// whether to vary the material data in each instance.
35+
#[argh(switch)]
36+
vary_material_data: bool,
37+
38+
/// the number of different textures from which to randomly select the material base color. 0 means no textures.
39+
#[argh(option, default = "0")]
40+
material_texture_count: usize,
41+
}
42+
43+
#[derive(Default, Clone)]
44+
enum Layout {
45+
Cube,
46+
#[default]
47+
Sphere,
48+
}
49+
50+
impl FromStr for Layout {
51+
type Err = String;
52+
53+
fn from_str(s: &str) -> Result<Self, Self::Err> {
54+
match s {
55+
"cube" => Ok(Self::Cube),
56+
"sphere" => Ok(Self::Sphere),
57+
_ => Err(format!(
58+
"Unknown layout value: '{}', valid options: 'cube', 'sphere'",
59+
s
60+
)),
61+
}
62+
}
63+
}
2164

2265
fn main() {
66+
let args: Args = argh::from_env();
67+
2368
App::new()
2469
.add_plugins((
2570
DefaultPlugins.set(WindowPlugin {
@@ -32,28 +77,36 @@ fn main() {
3277
FrameTimeDiagnosticsPlugin,
3378
LogDiagnosticsPlugin::default(),
3479
))
80+
.insert_resource(args)
3581
.add_systems(Startup, setup)
3682
.add_systems(Update, (move_camera, print_mesh_count))
3783
.run();
3884
}
3985

86+
const WIDTH: usize = 200;
87+
const HEIGHT: usize = 200;
88+
4089
fn setup(
4190
mut commands: Commands,
91+
args: Res<Args>,
4292
mut meshes: ResMut<Assets<Mesh>>,
43-
mut materials: ResMut<Assets<StandardMaterial>>,
93+
material_assets: ResMut<Assets<StandardMaterial>>,
94+
images: ResMut<Assets<Image>>,
4495
) {
4596
warn!(include_str!("warning_string.txt"));
4697

47-
const WIDTH: usize = 200;
48-
const HEIGHT: usize = 200;
98+
let args = args.into_inner();
99+
let images = images.into_inner();
100+
let material_assets = material_assets.into_inner();
101+
49102
let mesh = meshes.add(Mesh::from(shape::Cube { size: 1.0 }));
50-
let material = materials.add(StandardMaterial {
51-
base_color: Color::PINK,
52-
..default()
53-
});
54103

55-
match std::env::args().nth(1).as_deref() {
56-
Some("sphere") => {
104+
let material_textures = init_textures(args, images);
105+
let materials = init_materials(args, &material_textures, material_assets);
106+
107+
let mut material_rng = StdRng::seed_from_u64(42);
108+
match args.layout {
109+
Layout::Sphere => {
57110
// NOTE: This pattern is good for testing performance of culling as it provides roughly
58111
// the same number of visible meshes regardless of the viewing angle.
59112
const N_POINTS: usize = WIDTH * HEIGHT * 4;
@@ -65,8 +118,8 @@ fn setup(
65118
fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS);
66119
let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
67120
commands.spawn(PbrBundle {
68-
mesh: mesh.clone_weak(),
69-
material: material.clone_weak(),
121+
mesh: mesh.clone(),
122+
material: materials.choose(&mut material_rng).unwrap().clone(),
70123
transform: Transform::from_translation((radius * unit_sphere_p).as_vec3()),
71124
..default()
72125
});
@@ -86,14 +139,14 @@ fn setup(
86139
}
87140
// cube
88141
commands.spawn(PbrBundle {
89-
mesh: mesh.clone_weak(),
90-
material: material.clone_weak(),
142+
mesh: mesh.clone(),
143+
material: materials.choose(&mut material_rng).unwrap().clone(),
91144
transform: Transform::from_xyz((x as f32) * 2.5, (y as f32) * 2.5, 0.0),
92145
..default()
93146
});
94147
commands.spawn(PbrBundle {
95-
mesh: mesh.clone_weak(),
96-
material: material.clone_weak(),
148+
mesh: mesh.clone(),
149+
material: materials.choose(&mut material_rng).unwrap().clone(),
97150
transform: Transform::from_xyz(
98151
(x as f32) * 2.5,
99152
HEIGHT as f32 * 2.5,
@@ -102,14 +155,14 @@ fn setup(
102155
..default()
103156
});
104157
commands.spawn(PbrBundle {
105-
mesh: mesh.clone_weak(),
106-
material: material.clone_weak(),
158+
mesh: mesh.clone(),
159+
material: materials.choose(&mut material_rng).unwrap().clone(),
107160
transform: Transform::from_xyz((x as f32) * 2.5, 0.0, (y as f32) * 2.5),
108161
..default()
109162
});
110163
commands.spawn(PbrBundle {
111-
mesh: mesh.clone_weak(),
112-
material: material.clone_weak(),
164+
mesh: mesh.clone(),
165+
material: materials.choose(&mut material_rng).unwrap().clone(),
113166
transform: Transform::from_xyz(0.0, (x as f32) * 2.5, (y as f32) * 2.5),
114167
..default()
115168
});
@@ -123,20 +176,67 @@ fn setup(
123176
}
124177
}
125178

126-
// add one cube, the only one with strong handles
127-
// also serves as a reference point during rotation
128-
commands.spawn(PbrBundle {
129-
mesh,
130-
material,
131-
transform: Transform {
132-
translation: Vec3::new(0.0, HEIGHT as f32 * 2.5, 0.0),
133-
scale: Vec3::splat(5.0),
134-
..default()
135-
},
179+
commands.spawn(DirectionalLightBundle { ..default() });
180+
}
181+
182+
fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
183+
let mut color_rng = StdRng::seed_from_u64(42);
184+
let color_bytes: Vec<u8> = (0..(args.material_texture_count * 4))
185+
.map(|i| if (i % 4) == 3 { 255 } else { color_rng.gen() })
186+
.collect();
187+
color_bytes
188+
.chunks(4)
189+
.map(|pixel| {
190+
images.add(Image::new_fill(
191+
Extent3d {
192+
width: 1,
193+
height: 1,
194+
depth_or_array_layers: 1,
195+
},
196+
TextureDimension::D2,
197+
pixel,
198+
TextureFormat::Rgba8UnormSrgb,
199+
))
200+
})
201+
.collect()
202+
}
203+
204+
fn init_materials(
205+
args: &Args,
206+
textures: &[Handle<Image>],
207+
assets: &mut Assets<StandardMaterial>,
208+
) -> Vec<Handle<StandardMaterial>> {
209+
let capacity = if args.vary_material_data {
210+
match args.layout {
211+
Layout::Cube => (WIDTH - WIDTH / 10) * (HEIGHT - HEIGHT / 10),
212+
Layout::Sphere => WIDTH * HEIGHT * 4,
213+
}
214+
} else {
215+
args.material_texture_count
216+
}
217+
.max(1);
218+
219+
let mut materials = Vec::with_capacity(capacity);
220+
materials.push(assets.add(StandardMaterial {
221+
base_color: Color::WHITE,
222+
base_color_texture: textures.get(0).cloned(),
136223
..default()
137-
});
224+
}));
138225

139-
commands.spawn(DirectionalLightBundle { ..default() });
226+
let mut color_rng = StdRng::seed_from_u64(42);
227+
let mut texture_rng = StdRng::seed_from_u64(42);
228+
materials.extend(
229+
std::iter::repeat_with(|| {
230+
assets.add(StandardMaterial {
231+
base_color: Color::rgb_u8(color_rng.gen(), color_rng.gen(), color_rng.gen()),
232+
base_color_texture: textures.choose(&mut texture_rng).cloned(),
233+
..default()
234+
})
235+
})
236+
.take(capacity - materials.len()),
237+
);
238+
239+
materials
140240
}
141241

142242
// NOTE: This epsilon value is apparently optimal for optimizing for the average
@@ -159,9 +259,18 @@ fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
159259
}
160260

161261
// System for rotating the camera
162-
fn move_camera(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera>>) {
262+
fn move_camera(
263+
time: Res<Time>,
264+
args: Res<Args>,
265+
mut camera_query: Query<&mut Transform, With<Camera>>,
266+
) {
163267
let mut camera_transform = camera_query.single_mut();
164-
let delta = time.delta_seconds() * 0.15;
268+
let delta = 0.15
269+
* if args.benchmark {
270+
1.0 / 60.0
271+
} else {
272+
time.delta_seconds()
273+
};
165274
camera_transform.rotate_z(delta);
166275
camera_transform.rotate_x(delta);
167276
}

0 commit comments

Comments
 (0)