Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/bevymark.rs
9328 views
1
//! This example provides a 2D benchmark.
2
//!
3
//! Usage: spawn more entities by clicking on the screen.
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
sprite::SpriteAlphaMode,
16
sprite_render::AlphaMode2d,
17
window::{PresentMode, WindowResolution},
18
winit::WinitSettings,
19
};
20
use rand::{seq::IndexedRandom, Rng, SeedableRng};
21
use rand_chacha::ChaCha8Rng;
22
23
const BIRDS_PER_SECOND: u32 = 10000;
24
const GRAVITY: f32 = -9.8 * 100.0;
25
const MAX_VELOCITY: f32 = 750.;
26
const BIRD_SCALE: f32 = 0.15;
27
const BIRD_TEXTURE_SIZE: usize = 256;
28
const HALF_BIRD_SIZE: f32 = BIRD_TEXTURE_SIZE as f32 * BIRD_SCALE * 0.5;
29
30
#[derive(Resource)]
31
struct BevyCounter {
32
pub count: usize,
33
pub color: Color,
34
}
35
36
#[derive(Component)]
37
struct Bird {
38
velocity: Vec3,
39
}
40
41
#[derive(FromArgs, Resource)]
42
/// `bevymark` sprite / 2D mesh stress test
43
struct Args {
44
/// whether to use sprite or mesh2d
45
#[argh(option, default = "Mode::Sprite")]
46
mode: Mode,
47
48
/// whether to step animations by a fixed amount such that each frame is the same across runs.
49
/// If spawning waves, all are spawned up-front to immediately start rendering at the heaviest
50
/// load.
51
#[argh(switch)]
52
benchmark: bool,
53
54
/// how many birds to spawn per wave.
55
#[argh(option, default = "0")]
56
per_wave: usize,
57
58
/// the number of waves to spawn.
59
#[argh(option, default = "0")]
60
waves: usize,
61
62
/// whether to vary the material data in each instance.
63
#[argh(switch)]
64
vary_per_instance: bool,
65
66
/// the number of different textures from which to randomly select the material color. 0 means no textures.
67
#[argh(option, default = "1")]
68
material_texture_count: usize,
69
70
/// generate z values in increasing order rather than randomly
71
#[argh(switch)]
72
ordered_z: bool,
73
74
/// the alpha mode used to spawn the sprites
75
#[argh(option, default = "AlphaMode::Blend")]
76
alpha_mode: AlphaMode,
77
}
78
79
#[derive(Default, Clone)]
80
enum Mode {
81
#[default]
82
Sprite,
83
SpriteMesh,
84
Mesh2d,
85
}
86
87
impl FromStr for Mode {
88
type Err = String;
89
90
fn from_str(s: &str) -> Result<Self, Self::Err> {
91
match s {
92
"sprite" => Ok(Self::Sprite),
93
"mesh2d" => Ok(Self::Mesh2d),
94
"sprite_mesh" => Ok(Self::SpriteMesh),
95
_ => Err(format!(
96
"Unknown mode: '{s}', valid modes: 'sprite', 'mesh2d', 'sprite_mesh'"
97
)),
98
}
99
}
100
}
101
102
#[derive(Default, Clone)]
103
enum AlphaMode {
104
Opaque,
105
#[default]
106
Blend,
107
AlphaMask,
108
}
109
110
impl FromStr for AlphaMode {
111
type Err = String;
112
113
fn from_str(s: &str) -> Result<Self, Self::Err> {
114
match s {
115
"opaque" => Ok(Self::Opaque),
116
"blend" => Ok(Self::Blend),
117
"alpha_mask" => Ok(Self::AlphaMask),
118
_ => Err(format!(
119
"Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'"
120
)),
121
}
122
}
123
}
124
125
const FIXED_TIMESTEP: f32 = 0.2;
126
127
fn main() {
128
// `from_env` panics on the web
129
#[cfg(not(target_arch = "wasm32"))]
130
let args: Args = argh::from_env();
131
#[cfg(target_arch = "wasm32")]
132
let args = Args::from_args(&[], &[]).unwrap();
133
134
App::new()
135
.add_plugins((
136
DefaultPlugins.set(WindowPlugin {
137
primary_window: Some(Window {
138
title: "BevyMark".into(),
139
resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
140
present_mode: PresentMode::AutoNoVsync,
141
..default()
142
}),
143
..default()
144
}),
145
FrameTimeDiagnosticsPlugin::default(),
146
LogDiagnosticsPlugin::default(),
147
))
148
.insert_resource(StaticTransformOptimizations::disabled())
149
.insert_resource(WinitSettings::continuous())
150
.insert_resource(args)
151
.insert_resource(BevyCounter {
152
count: 0,
153
color: Color::WHITE,
154
})
155
.add_systems(Startup, setup)
156
.add_systems(FixedUpdate, scheduled_spawner)
157
.add_systems(
158
Update,
159
(
160
mouse_handler,
161
movement_system,
162
collision_system,
163
counter_system,
164
),
165
)
166
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
167
FIXED_TIMESTEP,
168
)))
169
.run();
170
}
171
172
#[derive(Resource)]
173
struct BirdScheduled {
174
waves: usize,
175
per_wave: usize,
176
}
177
178
fn scheduled_spawner(
179
mut commands: Commands,
180
args: Res<Args>,
181
window: Single<&Window>,
182
mut scheduled: ResMut<BirdScheduled>,
183
mut counter: ResMut<BevyCounter>,
184
bird_resources: ResMut<BirdResources>,
185
) {
186
if scheduled.waves > 0 {
187
let bird_resources = bird_resources.into_inner();
188
spawn_birds(
189
&mut commands,
190
args.into_inner(),
191
&window.resolution,
192
&mut counter,
193
scheduled.per_wave,
194
bird_resources,
195
None,
196
scheduled.waves - 1,
197
);
198
199
scheduled.waves -= 1;
200
}
201
}
202
203
#[derive(Resource)]
204
struct BirdResources {
205
textures: Vec<Handle<Image>>,
206
materials: Vec<Handle<ColorMaterial>>,
207
quad: Handle<Mesh>,
208
color_rng: ChaCha8Rng,
209
material_rng: ChaCha8Rng,
210
velocity_rng: ChaCha8Rng,
211
transform_rng: ChaCha8Rng,
212
}
213
214
#[derive(Component)]
215
struct StatsText;
216
217
fn setup(
218
mut commands: Commands,
219
args: Res<Args>,
220
asset_server: Res<AssetServer>,
221
mut meshes: ResMut<Assets<Mesh>>,
222
material_assets: ResMut<Assets<ColorMaterial>>,
223
images: ResMut<Assets<Image>>,
224
window: Single<&Window>,
225
counter: ResMut<BevyCounter>,
226
) {
227
warn!(include_str!("warning_string.txt"));
228
229
let args = args.into_inner();
230
let images = images.into_inner();
231
232
let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
233
if matches!(args.mode, Mode::Sprite) || args.material_texture_count > 0 {
234
textures.push(asset_server.load("branding/icon.png"));
235
}
236
init_textures(&mut textures, args, images);
237
238
let material_assets = material_assets.into_inner();
239
let materials = init_materials(args, &textures, material_assets);
240
241
let mut bird_resources = BirdResources {
242
textures,
243
materials,
244
quad: meshes.add(Rectangle::from_size(Vec2::splat(BIRD_TEXTURE_SIZE as f32))),
245
// We're seeding the PRNG here to make this example deterministic for testing purposes.
246
// This isn't strictly required in practical use unless you need your app to be deterministic.
247
color_rng: ChaCha8Rng::seed_from_u64(42),
248
material_rng: ChaCha8Rng::seed_from_u64(42),
249
velocity_rng: ChaCha8Rng::seed_from_u64(42),
250
transform_rng: ChaCha8Rng::seed_from_u64(42),
251
};
252
253
let font = TextFont {
254
font_size: FontSize::Px(40.0),
255
..Default::default()
256
};
257
258
commands.spawn(Camera2d);
259
commands
260
.spawn((
261
Node {
262
position_type: PositionType::Absolute,
263
padding: UiRect::all(px(5)),
264
..default()
265
},
266
BackgroundColor(Color::BLACK.with_alpha(0.75)),
267
GlobalZIndex(i32::MAX),
268
))
269
.with_children(|p| {
270
p.spawn((Text::default(), StatsText)).with_children(|p| {
271
p.spawn((
272
TextSpan::new("Bird Count: "),
273
font.clone(),
274
TextColor(LIME.into()),
275
));
276
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
277
p.spawn((
278
TextSpan::new("\nFPS (raw): "),
279
font.clone(),
280
TextColor(LIME.into()),
281
));
282
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
283
p.spawn((
284
TextSpan::new("\nFPS (SMA): "),
285
font.clone(),
286
TextColor(LIME.into()),
287
));
288
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
289
p.spawn((
290
TextSpan::new("\nFPS (EMA): "),
291
font.clone(),
292
TextColor(LIME.into()),
293
));
294
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
295
});
296
});
297
298
let mut scheduled = BirdScheduled {
299
per_wave: args.per_wave,
300
waves: args.waves,
301
};
302
303
if args.benchmark {
304
let counter = counter.into_inner();
305
for wave in (0..scheduled.waves).rev() {
306
spawn_birds(
307
&mut commands,
308
args,
309
&window.resolution,
310
counter,
311
scheduled.per_wave,
312
&mut bird_resources,
313
Some(wave),
314
wave,
315
);
316
}
317
scheduled.waves = 0;
318
}
319
commands.insert_resource(bird_resources);
320
commands.insert_resource(scheduled);
321
}
322
323
fn mouse_handler(
324
mut commands: Commands,
325
args: Res<Args>,
326
time: Res<Time>,
327
mouse_button_input: Res<ButtonInput<MouseButton>>,
328
window: Query<&Window>,
329
bird_resources: ResMut<BirdResources>,
330
mut counter: ResMut<BevyCounter>,
331
mut rng: Local<Option<ChaCha8Rng>>,
332
mut wave: Local<usize>,
333
) {
334
let Ok(window) = window.single() else {
335
return;
336
};
337
338
if rng.is_none() {
339
// We're seeding the PRNG here to make this example deterministic for testing purposes.
340
// This isn't strictly required in practical use unless you need your app to be deterministic.
341
*rng = Some(ChaCha8Rng::seed_from_u64(42));
342
}
343
let rng = rng.as_mut().unwrap();
344
345
if mouse_button_input.just_released(MouseButton::Left) {
346
counter.color = Color::linear_rgb(rng.random(), rng.random(), rng.random());
347
}
348
349
if mouse_button_input.pressed(MouseButton::Left) {
350
let spawn_count = (BIRDS_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
351
spawn_birds(
352
&mut commands,
353
args.into_inner(),
354
&window.resolution,
355
&mut counter,
356
spawn_count,
357
bird_resources.into_inner(),
358
None,
359
*wave,
360
);
361
*wave += 1;
362
}
363
}
364
365
fn bird_velocity_transform(
366
half_extents: Vec2,
367
mut translation: Vec3,
368
velocity_rng: &mut ChaCha8Rng,
369
waves: Option<usize>,
370
dt: f32,
371
) -> (Transform, Vec3) {
372
let mut velocity = Vec3::new(MAX_VELOCITY * (velocity_rng.random::<f32>() - 0.5), 0., 0.);
373
374
if let Some(waves) = waves {
375
// Step the movement and handle collisions as if the wave had been spawned at fixed time intervals
376
// and with dt-spaced frames of simulation
377
for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
378
step_movement(&mut translation, &mut velocity, dt);
379
handle_collision(half_extents, &translation, &mut velocity);
380
}
381
}
382
(
383
Transform::from_translation(translation).with_scale(Vec3::splat(BIRD_SCALE)),
384
velocity,
385
)
386
}
387
388
const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
389
390
fn spawn_birds(
391
commands: &mut Commands,
392
args: &Args,
393
primary_window_resolution: &WindowResolution,
394
counter: &mut BevyCounter,
395
spawn_count: usize,
396
bird_resources: &mut BirdResources,
397
waves_to_simulate: Option<usize>,
398
wave: usize,
399
) {
400
let bird_x = (primary_window_resolution.width() / -2.) + HALF_BIRD_SIZE;
401
let bird_y = (primary_window_resolution.height() / 2.) - HALF_BIRD_SIZE;
402
403
let half_extents = 0.5 * primary_window_resolution.size();
404
405
let color = counter.color;
406
let current_count = counter.count;
407
408
match args.mode {
409
Mode::Sprite => {
410
let batch = (0..spawn_count)
411
.map(|count| {
412
let bird_z = if args.ordered_z {
413
(current_count + count) as f32 * 0.00001
414
} else {
415
bird_resources.transform_rng.random::<f32>()
416
};
417
418
let (transform, velocity) = bird_velocity_transform(
419
half_extents,
420
Vec3::new(bird_x, bird_y, bird_z),
421
&mut bird_resources.velocity_rng,
422
waves_to_simulate,
423
FIXED_DELTA_TIME,
424
);
425
426
let color = if args.vary_per_instance {
427
Color::linear_rgb(
428
bird_resources.color_rng.random(),
429
bird_resources.color_rng.random(),
430
bird_resources.color_rng.random(),
431
)
432
} else {
433
color
434
};
435
(
436
Sprite {
437
image: bird_resources
438
.textures
439
.choose(&mut bird_resources.material_rng)
440
.unwrap()
441
.clone(),
442
color,
443
..default()
444
},
445
transform,
446
Bird { velocity },
447
)
448
})
449
.collect::<Vec<_>>();
450
commands.spawn_batch(batch);
451
}
452
Mode::SpriteMesh => {
453
let alpha_mode = match args.alpha_mode {
454
AlphaMode::Opaque => SpriteAlphaMode::Opaque,
455
AlphaMode::Blend => SpriteAlphaMode::Blend,
456
AlphaMode::AlphaMask => SpriteAlphaMode::Mask(0.5),
457
};
458
459
let batch = (0..spawn_count)
460
.map(|count| {
461
let bird_z = if args.ordered_z {
462
(current_count + count) as f32 * 0.00001
463
} else {
464
bird_resources.transform_rng.random::<f32>()
465
};
466
467
let (transform, velocity) = bird_velocity_transform(
468
half_extents,
469
Vec3::new(bird_x, bird_y, bird_z),
470
&mut bird_resources.velocity_rng,
471
waves_to_simulate,
472
FIXED_DELTA_TIME,
473
);
474
475
let color = if args.vary_per_instance {
476
Color::linear_rgb(
477
bird_resources.color_rng.random(),
478
bird_resources.color_rng.random(),
479
bird_resources.color_rng.random(),
480
)
481
} else {
482
color
483
};
484
(
485
SpriteMesh {
486
image: bird_resources
487
.textures
488
.choose(&mut bird_resources.material_rng)
489
.unwrap()
490
.clone(),
491
color,
492
alpha_mode,
493
..default()
494
},
495
transform,
496
Bird { velocity },
497
)
498
})
499
.collect::<Vec<_>>();
500
commands.spawn_batch(batch);
501
}
502
Mode::Mesh2d => {
503
let batch = (0..spawn_count)
504
.map(|count| {
505
let bird_z = if args.ordered_z {
506
(current_count + count) as f32 * 0.00001
507
} else {
508
bird_resources.transform_rng.random::<f32>()
509
};
510
511
let (transform, velocity) = bird_velocity_transform(
512
half_extents,
513
Vec3::new(bird_x, bird_y, bird_z),
514
&mut bird_resources.velocity_rng,
515
waves_to_simulate,
516
FIXED_DELTA_TIME,
517
);
518
519
let material =
520
if args.vary_per_instance || args.material_texture_count > args.waves {
521
bird_resources
522
.materials
523
.choose(&mut bird_resources.material_rng)
524
.unwrap()
525
.clone()
526
} else {
527
bird_resources.materials[wave % bird_resources.materials.len()].clone()
528
};
529
(
530
Mesh2d(bird_resources.quad.clone()),
531
MeshMaterial2d(material),
532
transform,
533
Bird { velocity },
534
)
535
})
536
.collect::<Vec<_>>();
537
commands.spawn_batch(batch);
538
}
539
}
540
541
counter.count += spawn_count;
542
counter.color = Color::linear_rgb(
543
bird_resources.color_rng.random(),
544
bird_resources.color_rng.random(),
545
bird_resources.color_rng.random(),
546
);
547
}
548
549
fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
550
translation.x += velocity.x * dt;
551
translation.y += velocity.y * dt;
552
velocity.y += GRAVITY * dt;
553
}
554
555
fn movement_system(
556
args: Res<Args>,
557
time: Res<Time>,
558
mut bird_query: Query<(&mut Bird, &mut Transform)>,
559
) {
560
let dt = if args.benchmark {
561
FIXED_DELTA_TIME
562
} else {
563
time.delta_secs()
564
};
565
for (mut bird, mut transform) in &mut bird_query {
566
step_movement(&mut transform.translation, &mut bird.velocity, dt);
567
}
568
}
569
570
fn handle_collision(half_extents: Vec2, translation: &Vec3, velocity: &mut Vec3) {
571
if (velocity.x > 0. && translation.x + HALF_BIRD_SIZE > half_extents.x)
572
|| (velocity.x <= 0. && translation.x - HALF_BIRD_SIZE < -half_extents.x)
573
{
574
velocity.x = -velocity.x;
575
}
576
let velocity_y = velocity.y;
577
if velocity_y < 0. && translation.y - HALF_BIRD_SIZE < -half_extents.y {
578
velocity.y = -velocity_y;
579
}
580
if translation.y + HALF_BIRD_SIZE > half_extents.y && velocity_y > 0.0 {
581
velocity.y = 0.0;
582
}
583
}
584
fn collision_system(window: Query<&Window>, mut bird_query: Query<(&mut Bird, &Transform)>) {
585
let Ok(window) = window.single() else {
586
return;
587
};
588
589
let half_extents = 0.5 * window.size();
590
591
for (mut bird, transform) in &mut bird_query {
592
handle_collision(half_extents, &transform.translation, &mut bird.velocity);
593
}
594
}
595
596
fn counter_system(
597
diagnostics: Res<DiagnosticsStore>,
598
counter: Res<BevyCounter>,
599
query: Single<Entity, With<StatsText>>,
600
mut writer: TextUiWriter,
601
) {
602
let text = *query;
603
604
if counter.is_changed() {
605
*writer.text(text, 2) = counter.count.to_string();
606
}
607
608
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
609
if let Some(raw) = fps.value() {
610
*writer.text(text, 4) = format!("{raw:.2}");
611
}
612
if let Some(sma) = fps.average() {
613
*writer.text(text, 6) = format!("{sma:.2}");
614
}
615
if let Some(ema) = fps.smoothed() {
616
*writer.text(text, 8) = format!("{ema:.2}");
617
}
618
};
619
}
620
621
fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
622
// We're seeding the PRNG here to make this example deterministic for testing purposes.
623
// This isn't strictly required in practical use unless you need your app to be deterministic.
624
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
625
while textures.len() < args.material_texture_count {
626
let pixel = [
627
color_rng.random(),
628
color_rng.random(),
629
color_rng.random(),
630
255,
631
];
632
textures.push(images.add(Image::new_fill(
633
Extent3d {
634
width: BIRD_TEXTURE_SIZE as u32,
635
height: BIRD_TEXTURE_SIZE as u32,
636
depth_or_array_layers: 1,
637
},
638
TextureDimension::D2,
639
&pixel,
640
TextureFormat::Rgba8UnormSrgb,
641
RenderAssetUsages::RENDER_WORLD,
642
)));
643
}
644
}
645
646
fn init_materials(
647
args: &Args,
648
textures: &[Handle<Image>],
649
assets: &mut Assets<ColorMaterial>,
650
) -> Vec<Handle<ColorMaterial>> {
651
let capacity = if args.vary_per_instance {
652
args.per_wave * args.waves
653
} else {
654
args.material_texture_count.max(args.waves)
655
}
656
.max(1);
657
658
let alpha_mode = match args.alpha_mode {
659
AlphaMode::Opaque => AlphaMode2d::Opaque,
660
AlphaMode::Blend => AlphaMode2d::Blend,
661
AlphaMode::AlphaMask => AlphaMode2d::Mask(0.5),
662
};
663
664
let mut materials = Vec::with_capacity(capacity);
665
materials.push(assets.add(ColorMaterial {
666
color: Color::WHITE,
667
texture: textures.first().cloned(),
668
alpha_mode,
669
..default()
670
}));
671
672
// We're seeding the PRNG here to make this example deterministic for testing purposes.
673
// This isn't strictly required in practical use unless you need your app to be deterministic.
674
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
675
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
676
materials.extend(
677
std::iter::repeat_with(|| {
678
assets.add(ColorMaterial {
679
color: Color::srgb_u8(color_rng.random(), color_rng.random(), color_rng.random()),
680
texture: textures.choose(&mut texture_rng).cloned(),
681
alpha_mode,
682
..default()
683
})
684
})
685
.take(capacity - materials.len()),
686
);
687
688
materials
689
}
690
691