Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/bevymark.rs
6849 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::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::continuous())
146
.insert_resource(args)
147
.insert_resource(BevyCounter {
148
count: 0,
149
color: Color::WHITE,
150
})
151
.add_systems(Startup, setup)
152
.add_systems(FixedUpdate, scheduled_spawner)
153
.add_systems(
154
Update,
155
(
156
mouse_handler,
157
movement_system,
158
collision_system,
159
counter_system,
160
),
161
)
162
.insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
163
FIXED_TIMESTEP,
164
)))
165
.run();
166
}
167
168
#[derive(Resource)]
169
struct BirdScheduled {
170
waves: usize,
171
per_wave: usize,
172
}
173
174
fn scheduled_spawner(
175
mut commands: Commands,
176
args: Res<Args>,
177
window: Single<&Window>,
178
mut scheduled: ResMut<BirdScheduled>,
179
mut counter: ResMut<BevyCounter>,
180
bird_resources: ResMut<BirdResources>,
181
) {
182
if scheduled.waves > 0 {
183
let bird_resources = bird_resources.into_inner();
184
spawn_birds(
185
&mut commands,
186
args.into_inner(),
187
&window.resolution,
188
&mut counter,
189
scheduled.per_wave,
190
bird_resources,
191
None,
192
scheduled.waves - 1,
193
);
194
195
scheduled.waves -= 1;
196
}
197
}
198
199
#[derive(Resource)]
200
struct BirdResources {
201
textures: Vec<Handle<Image>>,
202
materials: Vec<Handle<ColorMaterial>>,
203
quad: Handle<Mesh>,
204
color_rng: ChaCha8Rng,
205
material_rng: ChaCha8Rng,
206
velocity_rng: ChaCha8Rng,
207
transform_rng: ChaCha8Rng,
208
}
209
210
#[derive(Component)]
211
struct StatsText;
212
213
fn setup(
214
mut commands: Commands,
215
args: Res<Args>,
216
asset_server: Res<AssetServer>,
217
mut meshes: ResMut<Assets<Mesh>>,
218
material_assets: ResMut<Assets<ColorMaterial>>,
219
images: ResMut<Assets<Image>>,
220
window: Single<&Window>,
221
counter: ResMut<BevyCounter>,
222
) {
223
warn!(include_str!("warning_string.txt"));
224
225
let args = args.into_inner();
226
let images = images.into_inner();
227
228
let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
229
if matches!(args.mode, Mode::Sprite) || args.material_texture_count > 0 {
230
textures.push(asset_server.load("branding/icon.png"));
231
}
232
init_textures(&mut textures, args, images);
233
234
let material_assets = material_assets.into_inner();
235
let materials = init_materials(args, &textures, material_assets);
236
237
let mut bird_resources = BirdResources {
238
textures,
239
materials,
240
quad: meshes.add(Rectangle::from_size(Vec2::splat(BIRD_TEXTURE_SIZE as f32))),
241
// We're seeding the PRNG here to make this example deterministic for testing purposes.
242
// This isn't strictly required in practical use unless you need your app to be deterministic.
243
color_rng: ChaCha8Rng::seed_from_u64(42),
244
material_rng: ChaCha8Rng::seed_from_u64(42),
245
velocity_rng: ChaCha8Rng::seed_from_u64(42),
246
transform_rng: ChaCha8Rng::seed_from_u64(42),
247
};
248
249
let font = TextFont {
250
font_size: 40.0,
251
..Default::default()
252
};
253
254
commands.spawn(Camera2d);
255
commands
256
.spawn((
257
Node {
258
position_type: PositionType::Absolute,
259
padding: UiRect::all(px(5)),
260
..default()
261
},
262
BackgroundColor(Color::BLACK.with_alpha(0.75)),
263
GlobalZIndex(i32::MAX),
264
))
265
.with_children(|p| {
266
p.spawn((Text::default(), StatsText)).with_children(|p| {
267
p.spawn((
268
TextSpan::new("Bird Count: "),
269
font.clone(),
270
TextColor(LIME.into()),
271
));
272
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
273
p.spawn((
274
TextSpan::new("\nFPS (raw): "),
275
font.clone(),
276
TextColor(LIME.into()),
277
));
278
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
279
p.spawn((
280
TextSpan::new("\nFPS (SMA): "),
281
font.clone(),
282
TextColor(LIME.into()),
283
));
284
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
285
p.spawn((
286
TextSpan::new("\nFPS (EMA): "),
287
font.clone(),
288
TextColor(LIME.into()),
289
));
290
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
291
});
292
});
293
294
let mut scheduled = BirdScheduled {
295
per_wave: args.per_wave,
296
waves: args.waves,
297
};
298
299
if args.benchmark {
300
let counter = counter.into_inner();
301
for wave in (0..scheduled.waves).rev() {
302
spawn_birds(
303
&mut commands,
304
args,
305
&window.resolution,
306
counter,
307
scheduled.per_wave,
308
&mut bird_resources,
309
Some(wave),
310
wave,
311
);
312
}
313
scheduled.waves = 0;
314
}
315
commands.insert_resource(bird_resources);
316
commands.insert_resource(scheduled);
317
}
318
319
fn mouse_handler(
320
mut commands: Commands,
321
args: Res<Args>,
322
time: Res<Time>,
323
mouse_button_input: Res<ButtonInput<MouseButton>>,
324
window: Query<&Window>,
325
bird_resources: ResMut<BirdResources>,
326
mut counter: ResMut<BevyCounter>,
327
mut rng: Local<Option<ChaCha8Rng>>,
328
mut wave: Local<usize>,
329
) {
330
let Ok(window) = window.single() else {
331
return;
332
};
333
334
if rng.is_none() {
335
// We're seeding the PRNG here to make this example deterministic for testing purposes.
336
// This isn't strictly required in practical use unless you need your app to be deterministic.
337
*rng = Some(ChaCha8Rng::seed_from_u64(42));
338
}
339
let rng = rng.as_mut().unwrap();
340
341
if mouse_button_input.just_released(MouseButton::Left) {
342
counter.color = Color::linear_rgb(rng.random(), rng.random(), rng.random());
343
}
344
345
if mouse_button_input.pressed(MouseButton::Left) {
346
let spawn_count = (BIRDS_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
347
spawn_birds(
348
&mut commands,
349
args.into_inner(),
350
&window.resolution,
351
&mut counter,
352
spawn_count,
353
bird_resources.into_inner(),
354
None,
355
*wave,
356
);
357
*wave += 1;
358
}
359
}
360
361
fn bird_velocity_transform(
362
half_extents: Vec2,
363
mut translation: Vec3,
364
velocity_rng: &mut ChaCha8Rng,
365
waves: Option<usize>,
366
dt: f32,
367
) -> (Transform, Vec3) {
368
let mut velocity = Vec3::new(MAX_VELOCITY * (velocity_rng.random::<f32>() - 0.5), 0., 0.);
369
370
if let Some(waves) = waves {
371
// Step the movement and handle collisions as if the wave had been spawned at fixed time intervals
372
// and with dt-spaced frames of simulation
373
for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
374
step_movement(&mut translation, &mut velocity, dt);
375
handle_collision(half_extents, &translation, &mut velocity);
376
}
377
}
378
(
379
Transform::from_translation(translation).with_scale(Vec3::splat(BIRD_SCALE)),
380
velocity,
381
)
382
}
383
384
const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
385
386
fn spawn_birds(
387
commands: &mut Commands,
388
args: &Args,
389
primary_window_resolution: &WindowResolution,
390
counter: &mut BevyCounter,
391
spawn_count: usize,
392
bird_resources: &mut BirdResources,
393
waves_to_simulate: Option<usize>,
394
wave: usize,
395
) {
396
let bird_x = (primary_window_resolution.width() / -2.) + HALF_BIRD_SIZE;
397
let bird_y = (primary_window_resolution.height() / 2.) - HALF_BIRD_SIZE;
398
399
let half_extents = 0.5 * primary_window_resolution.size();
400
401
let color = counter.color;
402
let current_count = counter.count;
403
404
match args.mode {
405
Mode::Sprite => {
406
let batch = (0..spawn_count)
407
.map(|count| {
408
let bird_z = if args.ordered_z {
409
(current_count + count) as f32 * 0.00001
410
} else {
411
bird_resources.transform_rng.random::<f32>()
412
};
413
414
let (transform, velocity) = bird_velocity_transform(
415
half_extents,
416
Vec3::new(bird_x, bird_y, bird_z),
417
&mut bird_resources.velocity_rng,
418
waves_to_simulate,
419
FIXED_DELTA_TIME,
420
);
421
422
let color = if args.vary_per_instance {
423
Color::linear_rgb(
424
bird_resources.color_rng.random(),
425
bird_resources.color_rng.random(),
426
bird_resources.color_rng.random(),
427
)
428
} else {
429
color
430
};
431
(
432
Sprite {
433
image: bird_resources
434
.textures
435
.choose(&mut bird_resources.material_rng)
436
.unwrap()
437
.clone(),
438
color,
439
..default()
440
},
441
transform,
442
Bird { velocity },
443
)
444
})
445
.collect::<Vec<_>>();
446
commands.spawn_batch(batch);
447
}
448
Mode::Mesh2d => {
449
let batch = (0..spawn_count)
450
.map(|count| {
451
let bird_z = if args.ordered_z {
452
(current_count + count) as f32 * 0.00001
453
} else {
454
bird_resources.transform_rng.random::<f32>()
455
};
456
457
let (transform, velocity) = bird_velocity_transform(
458
half_extents,
459
Vec3::new(bird_x, bird_y, bird_z),
460
&mut bird_resources.velocity_rng,
461
waves_to_simulate,
462
FIXED_DELTA_TIME,
463
);
464
465
let material =
466
if args.vary_per_instance || args.material_texture_count > args.waves {
467
bird_resources
468
.materials
469
.choose(&mut bird_resources.material_rng)
470
.unwrap()
471
.clone()
472
} else {
473
bird_resources.materials[wave % bird_resources.materials.len()].clone()
474
};
475
(
476
Mesh2d(bird_resources.quad.clone()),
477
MeshMaterial2d(material),
478
transform,
479
Bird { velocity },
480
)
481
})
482
.collect::<Vec<_>>();
483
commands.spawn_batch(batch);
484
}
485
}
486
487
counter.count += spawn_count;
488
counter.color = Color::linear_rgb(
489
bird_resources.color_rng.random(),
490
bird_resources.color_rng.random(),
491
bird_resources.color_rng.random(),
492
);
493
}
494
495
fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
496
translation.x += velocity.x * dt;
497
translation.y += velocity.y * dt;
498
velocity.y += GRAVITY * dt;
499
}
500
501
fn movement_system(
502
args: Res<Args>,
503
time: Res<Time>,
504
mut bird_query: Query<(&mut Bird, &mut Transform)>,
505
) {
506
let dt = if args.benchmark {
507
FIXED_DELTA_TIME
508
} else {
509
time.delta_secs()
510
};
511
for (mut bird, mut transform) in &mut bird_query {
512
step_movement(&mut transform.translation, &mut bird.velocity, dt);
513
}
514
}
515
516
fn handle_collision(half_extents: Vec2, translation: &Vec3, velocity: &mut Vec3) {
517
if (velocity.x > 0. && translation.x + HALF_BIRD_SIZE > half_extents.x)
518
|| (velocity.x <= 0. && translation.x - HALF_BIRD_SIZE < -half_extents.x)
519
{
520
velocity.x = -velocity.x;
521
}
522
let velocity_y = velocity.y;
523
if velocity_y < 0. && translation.y - HALF_BIRD_SIZE < -half_extents.y {
524
velocity.y = -velocity_y;
525
}
526
if translation.y + HALF_BIRD_SIZE > half_extents.y && velocity_y > 0.0 {
527
velocity.y = 0.0;
528
}
529
}
530
fn collision_system(window: Query<&Window>, mut bird_query: Query<(&mut Bird, &Transform)>) {
531
let Ok(window) = window.single() else {
532
return;
533
};
534
535
let half_extents = 0.5 * window.size();
536
537
for (mut bird, transform) in &mut bird_query {
538
handle_collision(half_extents, &transform.translation, &mut bird.velocity);
539
}
540
}
541
542
fn counter_system(
543
diagnostics: Res<DiagnosticsStore>,
544
counter: Res<BevyCounter>,
545
query: Single<Entity, With<StatsText>>,
546
mut writer: TextUiWriter,
547
) {
548
let text = *query;
549
550
if counter.is_changed() {
551
*writer.text(text, 2) = counter.count.to_string();
552
}
553
554
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
555
if let Some(raw) = fps.value() {
556
*writer.text(text, 4) = format!("{raw:.2}");
557
}
558
if let Some(sma) = fps.average() {
559
*writer.text(text, 6) = format!("{sma:.2}");
560
}
561
if let Some(ema) = fps.smoothed() {
562
*writer.text(text, 8) = format!("{ema:.2}");
563
}
564
};
565
}
566
567
fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
568
// We're seeding the PRNG here to make this example deterministic for testing purposes.
569
// This isn't strictly required in practical use unless you need your app to be deterministic.
570
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
571
while textures.len() < args.material_texture_count {
572
let pixel = [
573
color_rng.random(),
574
color_rng.random(),
575
color_rng.random(),
576
255,
577
];
578
textures.push(images.add(Image::new_fill(
579
Extent3d {
580
width: BIRD_TEXTURE_SIZE as u32,
581
height: BIRD_TEXTURE_SIZE as u32,
582
depth_or_array_layers: 1,
583
},
584
TextureDimension::D2,
585
&pixel,
586
TextureFormat::Rgba8UnormSrgb,
587
RenderAssetUsages::RENDER_WORLD,
588
)));
589
}
590
}
591
592
fn init_materials(
593
args: &Args,
594
textures: &[Handle<Image>],
595
assets: &mut Assets<ColorMaterial>,
596
) -> Vec<Handle<ColorMaterial>> {
597
let capacity = if args.vary_per_instance {
598
args.per_wave * args.waves
599
} else {
600
args.material_texture_count.max(args.waves)
601
}
602
.max(1);
603
604
let alpha_mode = match args.alpha_mode {
605
AlphaMode::Opaque => AlphaMode2d::Opaque,
606
AlphaMode::Blend => AlphaMode2d::Blend,
607
AlphaMode::AlphaMask => AlphaMode2d::Mask(0.5),
608
};
609
610
let mut materials = Vec::with_capacity(capacity);
611
materials.push(assets.add(ColorMaterial {
612
color: Color::WHITE,
613
texture: textures.first().cloned(),
614
alpha_mode,
615
..default()
616
}));
617
618
// We're seeding the PRNG here to make this example deterministic for testing purposes.
619
// This isn't strictly required in practical use unless you need your app to be deterministic.
620
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
621
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
622
materials.extend(
623
std::iter::repeat_with(|| {
624
assets.add(ColorMaterial {
625
color: Color::srgb_u8(color_rng.random(), color_rng.random(), color_rng.random()),
626
texture: textures.choose(&mut texture_rng).cloned(),
627
alpha_mode,
628
..default()
629
})
630
})
631
.take(capacity - materials.len()),
632
);
633
634
materials
635
}
636
637