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_render::AlphaMode2d,
16    window::{PresentMode, WindowResolution},
17    winit::WinitSettings,
18};
19use rand::{seq::IndexedRandom, Rng, SeedableRng};
20use rand_chacha::ChaCha8Rng;
21
22const BIRDS_PER_SECOND: u32 = 10000;
23const GRAVITY: f32 = -9.8 * 100.0;
24const MAX_VELOCITY: f32 = 750.;
25const BIRD_SCALE: f32 = 0.15;
26const BIRD_TEXTURE_SIZE: usize = 256;
27const HALF_BIRD_SIZE: f32 = BIRD_TEXTURE_SIZE as f32 * BIRD_SCALE * 0.5;
28
29#[derive(Resource)]
30struct BevyCounter {
31    pub count: usize,
32    pub color: Color,
33}
34
35#[derive(Component)]
36struct Bird {
37    velocity: Vec3,
38}
39
40#[derive(FromArgs, Resource)]
41/// `bevymark` sprite / 2D mesh stress test
42struct 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)]
79enum Mode {
80    #[default]
81    Sprite,
82    Mesh2d,
83}
84
85impl 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)]
100enum AlphaMode {
101    Opaque,
102    #[default]
103    Blend,
104    AlphaMask,
105}
106
107impl 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
122const FIXED_TIMESTEP: f32 = 0.2;
123
124fn 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)]
169struct BirdScheduled {
170    waves: usize,
171    per_wave: usize,
172}
173
174fn 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)]
200struct 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)]
211struct StatsText;
212
213fn 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
319fn 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
361fn 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
384const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
385
386fn 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
495fn 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
501fn 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
516fn 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}
530fn 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
542fn 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
567fn 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
592fn 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}