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