Skip to content

Commit 047518b

Browse files
committed
Enhance many_cubes stress test use cases
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 c440de0 commit 047518b

File tree

2 files changed

+176
-18
lines changed

2 files changed

+176
-18
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+
clap = { version = "4.4.0", features = ["derive"] }
264265

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

examples/stress_tests/many_cubes.rs

Lines changed: 175 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
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
1311
use std::f64::consts::PI;
1412

@@ -18,8 +16,40 @@ use bevy::{
1816
prelude::*,
1917
window::{PresentMode, WindowPlugin},
2018
};
19+
use bevy_internal::render::render_resource::{Extent3d, TextureDimension, TextureFormat};
20+
use clap::{Parser, ValueEnum};
21+
use rand::{rngs::StdRng, seq::SliceRandom, Rng, SeedableRng};
22+
23+
#[derive(Parser, Resource)]
24+
#[command(author, version, about, long_about = None)]
25+
struct Args {
26+
/// How the cube instances should be positioned.
27+
#[arg(short, long, value_enum, default_value_t=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+
#[arg(short, long, default_value_t = false)]
32+
benchmark: bool,
33+
34+
/// Whether to vary the material data in each instance.
35+
#[arg(short, long, default_value_t = false)]
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+
#[arg(short, long, default_value_t = 0)]
40+
material_texture_count: usize,
41+
}
42+
43+
#[derive(Default, Clone, ValueEnum)]
44+
enum Layout {
45+
Cube,
46+
#[default]
47+
Sphere,
48+
}
2149

2250
fn main() {
51+
let args = Args::parse();
52+
2353
App::new()
2454
.add_plugins((
2555
DefaultPlugins.set(WindowPlugin {
@@ -32,28 +62,47 @@ fn main() {
3262
FrameTimeDiagnosticsPlugin,
3363
LogDiagnosticsPlugin::default(),
3464
))
65+
.insert_resource(args)
3566
.add_systems(Startup, setup)
3667
.add_systems(Update, (move_camera, print_mesh_count))
3768
.run();
3869
}
3970

4071
fn setup(
4172
mut commands: Commands,
73+
args: Res<Args>,
4274
mut meshes: ResMut<Assets<Mesh>>,
43-
mut materials: ResMut<Assets<StandardMaterial>>,
75+
materials: ResMut<Assets<StandardMaterial>>,
76+
images: ResMut<Assets<Image>>,
4477
) {
4578
warn!(include_str!("warning_string.txt"));
4679

80+
let args = args.into_inner();
81+
let images = images.into_inner();
82+
let materials = materials.into_inner();
83+
4784
const WIDTH: usize = 200;
4885
const HEIGHT: usize = 200;
86+
4987
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-
});
5488

55-
match std::env::args().nth(1).as_deref() {
56-
Some("sphere") => {
89+
let material_textures = init_textures(args, images);
90+
91+
let material = materials.add(Color::PINK.into());
92+
let mut texture_rng = StdRng::seed_from_u64(42);
93+
let texture_material = if args.material_texture_count == 1 {
94+
Some(materials.add(StandardMaterial {
95+
base_color: Color::WHITE,
96+
base_color_texture: get_texture(args, &mut texture_rng, &material_textures),
97+
..default()
98+
}))
99+
} else {
100+
None
101+
};
102+
103+
let mut color_rng = StdRng::seed_from_u64(42);
104+
match args.layout {
105+
Layout::Sphere => {
57106
// NOTE: This pattern is good for testing performance of culling as it provides roughly
58107
// the same number of visible meshes regardless of the viewing angle.
59108
const N_POINTS: usize = WIDTH * HEIGHT * 4;
@@ -66,7 +115,15 @@ fn setup(
66115
let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
67116
commands.spawn(PbrBundle {
68117
mesh: mesh.clone_weak(),
69-
material: material.clone_weak(),
118+
material: get_material(
119+
args,
120+
&material,
121+
texture_material.as_ref(),
122+
materials,
123+
&mut color_rng,
124+
&mut texture_rng,
125+
&material_textures,
126+
),
70127
transform: Transform::from_translation((radius * unit_sphere_p).as_vec3()),
71128
..default()
72129
});
@@ -87,13 +144,29 @@ fn setup(
87144
// cube
88145
commands.spawn(PbrBundle {
89146
mesh: mesh.clone_weak(),
90-
material: material.clone_weak(),
147+
material: get_material(
148+
args,
149+
&material,
150+
texture_material.as_ref(),
151+
materials,
152+
&mut color_rng,
153+
&mut texture_rng,
154+
&material_textures,
155+
),
91156
transform: Transform::from_xyz((x as f32) * 2.5, (y as f32) * 2.5, 0.0),
92157
..default()
93158
});
94159
commands.spawn(PbrBundle {
95160
mesh: mesh.clone_weak(),
96-
material: material.clone_weak(),
161+
material: get_material(
162+
args,
163+
&material,
164+
texture_material.as_ref(),
165+
materials,
166+
&mut color_rng,
167+
&mut texture_rng,
168+
&material_textures,
169+
),
97170
transform: Transform::from_xyz(
98171
(x as f32) * 2.5,
99172
HEIGHT as f32 * 2.5,
@@ -103,13 +176,29 @@ fn setup(
103176
});
104177
commands.spawn(PbrBundle {
105178
mesh: mesh.clone_weak(),
106-
material: material.clone_weak(),
179+
material: get_material(
180+
args,
181+
&material,
182+
texture_material.as_ref(),
183+
materials,
184+
&mut color_rng,
185+
&mut texture_rng,
186+
&material_textures,
187+
),
107188
transform: Transform::from_xyz((x as f32) * 2.5, 0.0, (y as f32) * 2.5),
108189
..default()
109190
});
110191
commands.spawn(PbrBundle {
111192
mesh: mesh.clone_weak(),
112-
material: material.clone_weak(),
193+
material: get_material(
194+
args,
195+
&material,
196+
texture_material.as_ref(),
197+
materials,
198+
&mut color_rng,
199+
&mut texture_rng,
200+
&material_textures,
201+
),
113202
transform: Transform::from_xyz(0.0, (x as f32) * 2.5, (y as f32) * 2.5),
114203
..default()
115204
});
@@ -139,6 +228,65 @@ fn setup(
139228
commands.spawn(DirectionalLightBundle { ..default() });
140229
}
141230

231+
fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
232+
let mut color_rng = StdRng::seed_from_u64(42);
233+
let color_bytes: Vec<u8> = (0..(args.material_texture_count * 4))
234+
.map(|i| if (i % 4) == 3 { 255 } else { color_rng.gen() })
235+
.collect();
236+
color_bytes
237+
.chunks(4)
238+
.map(|pixel| {
239+
images.add(Image::new_fill(
240+
Extent3d {
241+
width: 1,
242+
height: 1,
243+
depth_or_array_layers: 1,
244+
},
245+
TextureDimension::D2,
246+
pixel,
247+
TextureFormat::Rgba8UnormSrgb,
248+
))
249+
})
250+
.collect()
251+
}
252+
253+
fn get_texture<R: Rng>(
254+
args: &Args,
255+
texture_rng: &mut R,
256+
material_textures: &[Handle<Image>],
257+
) -> Option<Handle<Image>> {
258+
match args.material_texture_count {
259+
c if c == 0 => None,
260+
c if c == 1 => Some(material_textures[0].clone_weak()),
261+
_ => Some(material_textures.choose(texture_rng).unwrap().clone_weak()),
262+
}
263+
}
264+
265+
fn get_material<R: Rng>(
266+
args: &Args,
267+
material: &Handle<StandardMaterial>,
268+
texture_material: Option<&Handle<StandardMaterial>>,
269+
materials: &mut Assets<StandardMaterial>,
270+
color_rng: &mut R,
271+
texture_rng: &mut R,
272+
material_textures: &[Handle<Image>],
273+
) -> Handle<StandardMaterial> {
274+
match (args.vary_material_data, args.material_texture_count) {
275+
(false, 0) => material.clone_weak(),
276+
(false, 1) => texture_material.unwrap().clone_weak(),
277+
(false, _) => materials.add(StandardMaterial {
278+
base_color: Color::WHITE,
279+
base_color_texture: get_texture(args, texture_rng, material_textures),
280+
..default()
281+
}),
282+
(true, _) => materials.add(StandardMaterial {
283+
base_color: Color::rgb_u8(color_rng.gen(), color_rng.gen(), color_rng.gen()),
284+
base_color_texture: get_texture(args, texture_rng, material_textures),
285+
..default()
286+
}),
287+
}
288+
}
289+
142290
// NOTE: This epsilon value is apparently optimal for optimizing for the average
143291
// nearest-neighbor distance. See:
144292
// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
@@ -159,9 +307,18 @@ fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
159307
}
160308

161309
// System for rotating the camera
162-
fn move_camera(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera>>) {
310+
fn move_camera(
311+
time: Res<Time>,
312+
args: Res<Args>,
313+
mut camera_query: Query<&mut Transform, With<Camera>>,
314+
) {
163315
let mut camera_transform = camera_query.single_mut();
164-
let delta = time.delta_seconds() * 0.15;
316+
let delta = 0.15
317+
* if args.benchmark {
318+
1.0 / 60.0
319+
} else {
320+
time.delta_seconds()
321+
};
165322
camera_transform.rotate_z(delta);
166323
camera_transform.rotate_x(delta);
167324
}

0 commit comments

Comments
 (0)