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