Skip to main content

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    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};
20use chacha20::ChaCha8Rng;
21use rand::{seq::IndexedRandom, RngExt, SeedableRng};
22
23const BIRDS_PER_SECOND: u32 = 10000;
24const GRAVITY: f32 = -9.8 * 100.0;
25const MAX_VELOCITY: f32 = 750.;
26const BIRD_SCALE: f32 = 0.15;
27const BIRD_TEXTURE_SIZE: usize = 256;
28const HALF_BIRD_SIZE: f32 = BIRD_TEXTURE_SIZE as f32 * BIRD_SCALE * 0.5;
29
30#[derive(Resource)]
31struct BevyCounter {
32    pub count: usize,
33    pub color: Color,
34}
35
36#[derive(Component)]
37struct Bird {
38    velocity: Vec3,
39}
40
41#[derive(FromArgs, Resource)]
42/// `bevymark` sprite / 2D mesh stress test
43struct 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)]
80enum Mode {
81    #[default]
82    Sprite,
83    SpriteMesh,
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            "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)]
103enum AlphaMode {
104    Opaque,
105    #[default]
106    Blend,
107    AlphaMask,
108}
109
110impl 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
125const FIXED_TIMESTEP: f32 = 0.2;
126
127fn 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)]
173struct BirdScheduled {
174    waves: usize,
175    per_wave: usize,
176}
177
178fn 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)]
204struct 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)]
215struct StatsText;
216
217fn 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
323fn 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
365fn 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
388const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
389
390fn 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
549fn 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
555fn 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
570fn 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}
584fn 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
596fn 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
621fn 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
646fn 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}