bevymark/
bevymark.rs

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