docs/bevy/examples/stress_tests/bevymark_3d.rs 15.8 K · 554 lines · raw · history

1 //! This example provides a 3D benchmark.
2 //!
3 //! Usage: spawn more entities by clicking with the left mouse button.
4
5 use core::time::Duration;
6 use std::str::FromStr;
7
8 use argh::FromArgs;
9 use bevy::{
10 asset::RenderAssetUsages,
11 color::palettes::basic::*,
12 diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
13 prelude::*,
14 render::render_resource::{Extent3d, TextureDimension, TextureFormat},
15 window::{PresentMode, WindowResolution},
16 winit::WinitSettings,
17 };
18 use chacha20::ChaCha8Rng;
19 use rand::{seq::IndexedRandom, RngExt, SeedableRng};
20
21 const CUBES_PER_SECOND: u32 = 10000;
22 const GRAVITY: f32 = -9.8;
23 const MAX_VELOCITY: f32 = 10.;
24 const CUBE_SCALE: f32 = 1.0;
25 const CUBE_TEXTURE_SIZE: usize = 256;
26 const HALF_CUBE_SIZE: f32 = CUBE_SCALE * 0.5;
27 const VOLUME_WIDTH: usize = 50;
28 const VOLUME_SIZE: Vec3 = Vec3::splat(VOLUME_WIDTH as f32);
29
30 #[derive(Resource)]
31 struct BevyCounter {
32 pub count: usize,
33 pub color: Color,
34 }
35
36 #[derive(Component)]
37 struct Cube {
38 velocity: Vec3,
39 }
40
41 #[derive(FromArgs, Resource)]
42 /// `bevymark_3d` cube stress test
43 struct Args {
44 /// whether to step animations by a fixed amount such that each frame is the same across runs.
45 /// If spawning waves, all are spawned up-front to immediately start rendering at the heaviest
46 /// load.
47 #[argh(switch)]
48 benchmark: bool,
49
50 /// how many cubes to spawn per wave.
51 #[argh(option, default = "0")]
52 per_wave: usize,
53
54 /// the number of waves to spawn.
55 #[argh(option, default = "0")]
56 waves: usize,
57
58 /// whether to vary the material data in each instance.
59 #[argh(switch)]
60 vary_per_instance: bool,
61
62 /// the number of different textures from which to randomly select the material color. 0 means no textures.
63 #[argh(option, default = "1")]
64 material_texture_count: usize,
65
66 /// the alpha mode used to spawn the cubes
67 #[argh(option, default = "AlphaMode::Opaque")]
68 alpha_mode: AlphaMode,
69 }
70
71 #[derive(Default, Clone)]
72 enum AlphaMode {
73 #[default]
74 Opaque,
75 Blend,
76 AlphaMask,
77 }
78
79 impl FromStr for AlphaMode {
80 type Err = String;
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 match s {
84 "opaque" => Ok(Self::Opaque),
85 "blend" => Ok(Self::Blend),
86 "alpha_mask" => Ok(Self::AlphaMask),
87 _ => Err(format!(
88 "Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'"
89 )),
90 }
91 }
92 }
93
94 const FIXED_TIMESTEP: f32 = 0.2;
95
96 fn main() {
97 // `from_env` panics on the web
98 #[cfg(not(target_arch = "wasm32"))]
99 let args: Args = argh::from_env();
100 #[cfg(target_arch = "wasm32")]
101 let args = Args::from_args(&[], &[]).unwrap();
102
103 App::new()
104 .add_plugins((
105 DefaultPlugins.set(WindowPlugin {
106 primary_window: Some(Window {
107 title: "BevyMark 3D".into(),
108 resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
109 present_mode: PresentMode::AutoNoVsync,
110 ..default()
111 }),
112 ..default()
113 }),
114 FrameTimeDiagnosticsPlugin::default(),
115 LogDiagnosticsPlugin::default(),
116 ))
117 .insert_resource(WinitSettings::continuous())
118 .insert_resource(args)
119 .insert_resource(BevyCounter {
120 count: 0,
121 color: Color::WHITE,
122 })
123 .add_systems(Startup, setup)
124 .add_systems(FixedUpdate, scheduled_spawner)
125 .add_systems(
126 Update,
127 (
128 mouse_handler,
129 movement_system,
130 collision_system,
131 counter_system,
132 ),
133 )
134 .insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
135 FIXED_TIMESTEP,
136 )))
137 .run();
138 }
139
140 #[derive(Resource)]
141 struct CubeScheduled {
142 waves: usize,
143 per_wave: usize,
144 }
145
146 fn scheduled_spawner(
147 mut commands: Commands,
148 args: Res<Args>,
149 mut scheduled: ResMut<CubeScheduled>,
150 mut counter: ResMut<BevyCounter>,
151 cube_resources: ResMut<CubeResources>,
152 ) {
153 if scheduled.waves > 0 {
154 let cube_resources = cube_resources.into_inner();
155 spawn_cubes(
156 &mut commands,
157 args.into_inner(),
158 &mut counter,
159 scheduled.per_wave,
160 cube_resources,
161 None,
162 scheduled.waves - 1,
163 );
164
165 scheduled.waves -= 1;
166 }
167 }
168
169 #[derive(Resource)]
170 struct CubeResources {
171 _textures: Vec<Handle<Image>>,
172 materials: Vec<Handle<StandardMaterial>>,
173 cube_mesh: Handle<Mesh>,
174 color_rng: ChaCha8Rng,
175 material_rng: ChaCha8Rng,
176 velocity_rng: ChaCha8Rng,
177 transform_rng: ChaCha8Rng,
178 }
179
180 #[derive(Component)]
181 struct StatsText;
182
183 fn setup(
184 mut commands: Commands,
185 args: Res<Args>,
186 asset_server: Res<AssetServer>,
187 mut meshes: ResMut<Assets<Mesh>>,
188 material_assets: ResMut<Assets<StandardMaterial>>,
189 images: ResMut<Assets<Image>>,
190 counter: ResMut<BevyCounter>,
191 ) {
192 warn!(include_str!("warning_string.txt"));
193
194 let args = args.into_inner();
195 let images = images.into_inner();
196
197 let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
198 if args.material_texture_count > 0 {
199 textures.push(asset_server.load("branding/icon.png"));
200 }
201 init_textures(&mut textures, args, images);
202
203 let material_assets = material_assets.into_inner();
204 let materials = init_materials(args, &textures, material_assets);
205
206 let mut cube_resources = CubeResources {
207 _textures: textures,
208 materials,
209 cube_mesh: meshes.add(Cuboid::from_size(Vec3::splat(CUBE_SCALE))),
210 color_rng: ChaCha8Rng::seed_from_u64(42),
211 material_rng: ChaCha8Rng::seed_from_u64(12),
212 velocity_rng: ChaCha8Rng::seed_from_u64(97),
213 transform_rng: ChaCha8Rng::seed_from_u64(26),
214 };
215
216 let font = TextFont {
217 font_size: FontSize::Px(40.0),
218 ..Default::default()
219 };
220
221 commands.spawn((
222 Camera3d::default(),
223 Transform::from_translation(VOLUME_SIZE * 1.3).looking_at(Vec3::ZERO, Vec3::Y),
224 ));
225
226 commands.spawn((
227 DirectionalLight {
228 illuminance: 10000.0,
229 shadow_maps_enabled: false,
230 ..default()
231 },
232 Transform::from_xyz(1.0, 2.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
233 ));
234
235 commands.spawn((
236 Node {
237 position_type: PositionType::Absolute,
238 padding: UiRect::all(px(5)),
239 ..default()
240 },
241 BackgroundColor(Color::BLACK.with_alpha(0.75)),
242 GlobalZIndex(i32::MAX),
243 children![(
244 Text::default(),
245 StatsText,
246 children![
247 (
248 TextSpan::new("Cube Count: "),
249 font.clone(),
250 TextColor(LIME.into()),
251 ),
252 (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
253 (
254 TextSpan::new("\nFPS (raw): "),
255 font.clone(),
256 TextColor(LIME.into()),
257 ),
258 (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
259 (
260 TextSpan::new("\nFPS (SMA): "),
261 font.clone(),
262 TextColor(LIME.into()),
263 ),
264 (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
265 (
266 TextSpan::new("\nFPS (EMA): "),
267 font.clone(),
268 TextColor(LIME.into()),
269 ),
270 (TextSpan::new(""), font.clone(), TextColor(AQUA.into()))
271 ]
272 )],
273 ));
274
275 let mut scheduled = CubeScheduled {
276 per_wave: args.per_wave,
277 waves: args.waves,
278 };
279
280 if args.benchmark {
281 let counter = counter.into_inner();
282 for wave in (0..scheduled.waves).rev() {
283 spawn_cubes(
284 &mut commands,
285 args,
286 counter,
287 scheduled.per_wave,
288 &mut cube_resources,
289 Some(wave),
290 wave,
291 );
292 }
293 scheduled.waves = 0;
294 }
295 commands.insert_resource(cube_resources);
296 commands.insert_resource(scheduled);
297 }
298
299 fn mouse_handler(
300 mut commands: Commands,
301 args: Res<Args>,
302 time: Res<Time>,
303 mouse_button_input: Res<ButtonInput<MouseButton>>,
304 cube_resources: ResMut<CubeResources>,
305 mut counter: ResMut<BevyCounter>,
306 mut rng: Local<Option<ChaCha8Rng>>,
307 mut wave: Local<usize>,
308 ) {
309 if rng.is_none() {
310 *rng = Some(ChaCha8Rng::seed_from_u64(42));
311 }
312 let rng = rng.as_mut().unwrap();
313
314 if mouse_button_input.just_released(MouseButton::Left) {
315 counter.color = Color::linear_rgb(rng.random(), rng.random(), rng.random());
316 }
317
318 if mouse_button_input.pressed(MouseButton::Left) {
319 let spawn_count = (CUBES_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
320 spawn_cubes(
321 &mut commands,
322 args.into_inner(),
323 &mut counter,
324 spawn_count,
325 cube_resources.into_inner(),
326 None,
327 *wave,
328 );
329 *wave += 1;
330 }
331 }
332
333 fn cube_velocity_transform(
334 mut translation: Vec3,
335 velocity_rng: &mut ChaCha8Rng,
336 waves: Option<usize>,
337 dt: f32,
338 ) -> (Transform, Vec3) {
339 let mut velocity = Vec3::new(0., 0., MAX_VELOCITY * velocity_rng.random::<f32>());
340
341 if let Some(waves) = waves {
342 for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
343 step_movement(&mut translation, &mut velocity, dt);
344 handle_collision(&translation, &mut velocity);
345 }
346 }
347 (Transform::from_translation(translation), velocity)
348 }
349
350 const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
351
352 fn spawn_cubes(
353 commands: &mut Commands,
354 args: &Args,
355 counter: &mut BevyCounter,
356 spawn_count: usize,
357 cube_resources: &mut CubeResources,
358 waves_to_simulate: Option<usize>,
359 wave: usize,
360 ) {
361 let batch_material = cube_resources.materials[wave % cube_resources.materials.len()].clone();
362
363 let spawn_y = VOLUME_SIZE.y / 2.0 - HALF_CUBE_SIZE;
364 let spawn_z = -VOLUME_SIZE.z / 2.0 + HALF_CUBE_SIZE;
365
366 let batch = (0..spawn_count)
367 .map(|_| {
368 let spawn_pos = Vec3::new(
369 (cube_resources.transform_rng.random::<f32>() - 0.5) * VOLUME_SIZE.x,
370 spawn_y,
371 spawn_z,
372 );
373
374 let (transform, velocity) = cube_velocity_transform(
375 spawn_pos,
376 &mut cube_resources.velocity_rng,
377 waves_to_simulate,
378 FIXED_DELTA_TIME,
379 );
380
381 let material = if args.vary_per_instance {
382 cube_resources
383 .materials
384 .choose(&mut cube_resources.material_rng)
385 .unwrap()
386 .clone()
387 } else {
388 batch_material.clone()
389 };
390
391 (
392 Mesh3d(cube_resources.cube_mesh.clone()),
393 MeshMaterial3d(material),
394 transform,
395 Cube { velocity },
396 )
397 })
398 .collect::<Vec<_>>();
399 commands.spawn_batch(batch);
400
401 counter.count += spawn_count;
402 counter.color = Color::linear_rgb(
403 cube_resources.color_rng.random(),
404 cube_resources.color_rng.random(),
405 cube_resources.color_rng.random(),
406 );
407 }
408
409 fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
410 translation.x += velocity.x * dt;
411 translation.y += velocity.y * dt;
412 translation.z += velocity.z * dt;
413 velocity.y += GRAVITY * dt;
414 }
415
416 fn movement_system(
417 args: Res<Args>,
418 time: Res<Time>,
419 mut cube_query: Query<(&mut Cube, &mut Transform)>,
420 ) {
421 let dt = if args.benchmark {
422 FIXED_DELTA_TIME
423 } else {
424 time.delta_secs()
425 };
426 for (mut cube, mut transform) in &mut cube_query {
427 step_movement(&mut transform.translation, &mut cube.velocity, dt);
428 }
429 }
430
431 fn handle_collision(translation: &Vec3, velocity: &mut Vec3) {
432 if (velocity.x > 0. && translation.x + HALF_CUBE_SIZE > VOLUME_SIZE.x / 2.0)
433 || (velocity.x <= 0. && translation.x - HALF_CUBE_SIZE < -VOLUME_SIZE.x / 2.0)
434 {
435 velocity.x = -velocity.x;
436 }
437 if (velocity.z > 0. && translation.z + HALF_CUBE_SIZE > VOLUME_SIZE.z / 2.0)
438 || (velocity.z <= 0. && translation.z - HALF_CUBE_SIZE < -VOLUME_SIZE.z / 2.0)
439 {
440 velocity.z = -velocity.z;
441 }
442
443 let velocity_y = velocity.y;
444 if velocity_y < 0. && translation.y - HALF_CUBE_SIZE < -VOLUME_SIZE.y / 2.0 {
445 velocity.y = -velocity_y;
446 }
447 if translation.y + HALF_CUBE_SIZE > VOLUME_SIZE.y / 2.0 && velocity_y > 0.0 {
448 velocity.y = 0.0;
449 }
450 }
451
452 fn collision_system(mut cube_query: Query<(&mut Cube, &Transform)>) {
453 cube_query.par_iter_mut().for_each(|(mut cube, transform)| {
454 handle_collision(&transform.translation, &mut cube.velocity);
455 });
456 }
457
458 fn counter_system(
459 diagnostics: Res<DiagnosticsStore>,
460 counter: Res<BevyCounter>,
461 query: Single<Entity, With<StatsText>>,
462 mut writer: TextUiWriter,
463 ) {
464 let text = *query;
465
466 if counter.is_changed() {
467 *writer.text(text, 2) = counter.count.to_string();
468 }
469
470 if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
471 if let Some(raw) = fps.value() {
472 *writer.text(text, 4) = format!("{raw:.2}");
473 }
474 if let Some(sma) = fps.average() {
475 *writer.text(text, 6) = format!("{sma:.2}");
476 }
477 if let Some(ema) = fps.smoothed() {
478 *writer.text(text, 8) = format!("{ema:.2}");
479 }
480 };
481 }
482
483 fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
484 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
485 while textures.len() < args.material_texture_count {
486 let pixel = [
487 color_rng.random(),
488 color_rng.random(),
489 color_rng.random(),
490 255,
491 ];
492 textures.push(images.add(Image::new_fill(
493 Extent3d {
494 width: CUBE_TEXTURE_SIZE as u32,
495 height: CUBE_TEXTURE_SIZE as u32,
496 depth_or_array_layers: 1,
497 },
498 TextureDimension::D2,
499 &pixel,
500 TextureFormat::Rgba8UnormSrgb,
501 RenderAssetUsages::RENDER_WORLD,
502 )));
503 }
504 }
505
506 fn init_materials(
507 args: &Args,
508 textures: &[Handle<Image>],
509 assets: &mut Assets<StandardMaterial>,
510 ) -> Vec<Handle<StandardMaterial>> {
511 let mut capacity = if args.vary_per_instance {
512 args.per_wave * args.waves
513 } else {
514 args.material_texture_count.max(args.waves)
515 };
516 if !args.benchmark {
517 capacity = capacity.max(256);
518 }
519 capacity = capacity.max(1);
520
521 let alpha_mode = match args.alpha_mode {
522 AlphaMode::Opaque => bevy::prelude::AlphaMode::Opaque,
523 AlphaMode::Blend => bevy::prelude::AlphaMode::Blend,
524 AlphaMode::AlphaMask => bevy::prelude::AlphaMode::Mask(0.5),
525 };
526
527 let mut materials = Vec::with_capacity(capacity);
528 materials.push(assets.add(StandardMaterial {
529 base_color: Color::WHITE,
530 base_color_texture: textures.first().cloned(),
531 alpha_mode,
532 ..default()
533 }));
534
535 let mut color_rng = ChaCha8Rng::seed_from_u64(42);
536 let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
537 materials.extend(
538 std::iter::repeat_with(|| {
539 assets.add(StandardMaterial {
540 base_color: Color::linear_rgb(
541 color_rng.random(),
542 color_rng.random(),
543 color_rng.random(),
544 ),
545 base_color_texture: textures.choose(&mut texture_rng).cloned(),
546 alpha_mode,
547 ..default()
548 })
549 })
550 .take(capacity - materials.len()),
551 );
552
553 materials
554 }