Skip to main content

bevymark_3d/
bevymark_3d.rs

1//! This example provides a 3D benchmark.
2//!
3//! Usage: spawn more entities by clicking with the left mouse button.
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    window::{PresentMode, WindowResolution},
16    winit::WinitSettings,
17};
18use chacha20::ChaCha8Rng;
19use rand::{seq::IndexedRandom, RngExt, SeedableRng};
20
21const CUBES_PER_SECOND: u32 = 10000;
22const GRAVITY: f32 = -9.8;
23const MAX_VELOCITY: f32 = 10.;
24const CUBE_SCALE: f32 = 1.0;
25const CUBE_TEXTURE_SIZE: usize = 256;
26const HALF_CUBE_SIZE: f32 = CUBE_SCALE * 0.5;
27const VOLUME_WIDTH: usize = 50;
28const VOLUME_SIZE: Vec3 = Vec3::splat(VOLUME_WIDTH as f32);
29
30#[derive(Resource)]
31struct BevyCounter {
32    pub count: usize,
33    pub color: Color,
34}
35
36#[derive(Component)]
37struct Cube {
38    velocity: Vec3,
39}
40
41#[derive(FromArgs, Resource)]
42/// `bevymark_3d` cube stress test
43struct Args {
44    /// whether to step animations by a fixed amount such that each frame is the same across runs.
45    /// If spawning waves, all are spawned up-front to immediately start rendering at the heaviest
46    /// load.
47    #[argh(switch)]
48    benchmark: bool,
49
50    /// how many cubes to spawn per wave.
51    #[argh(option, default = "0")]
52    per_wave: usize,
53
54    /// the number of waves to spawn.
55    #[argh(option, default = "0")]
56    waves: usize,
57
58    /// whether to vary the material data in each instance.
59    #[argh(switch)]
60    vary_per_instance: bool,
61
62    /// the number of different textures from which to randomly select the material color. 0 means no textures.
63    #[argh(option, default = "1")]
64    material_texture_count: usize,
65
66    /// the alpha mode used to spawn the cubes
67    #[argh(option, default = "AlphaMode::Opaque")]
68    alpha_mode: AlphaMode,
69}
70
71#[derive(Default, Clone)]
72enum AlphaMode {
73    #[default]
74    Opaque,
75    Blend,
76    AlphaMask,
77}
78
79impl FromStr for AlphaMode {
80    type Err = String;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        match s {
84            "opaque" => Ok(Self::Opaque),
85            "blend" => Ok(Self::Blend),
86            "alpha_mask" => Ok(Self::AlphaMask),
87            _ => Err(format!(
88                "Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'"
89            )),
90        }
91    }
92}
93
94const FIXED_TIMESTEP: f32 = 0.2;
95
96fn main() {
97    // `from_env` panics on the web
98    #[cfg(not(target_arch = "wasm32"))]
99    let args: Args = argh::from_env();
100    #[cfg(target_arch = "wasm32")]
101    let args = Args::from_args(&[], &[]).unwrap();
102
103    App::new()
104        .add_plugins((
105            DefaultPlugins.set(WindowPlugin {
106                primary_window: Some(Window {
107                    title: "BevyMark 3D".into(),
108                    resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0),
109                    present_mode: PresentMode::AutoNoVsync,
110                    ..default()
111                }),
112                ..default()
113            }),
114            FrameTimeDiagnosticsPlugin::default(),
115            LogDiagnosticsPlugin::default(),
116        ))
117        .insert_resource(WinitSettings::continuous())
118        .insert_resource(args)
119        .insert_resource(BevyCounter {
120            count: 0,
121            color: Color::WHITE,
122        })
123        .add_systems(Startup, setup)
124        .add_systems(FixedUpdate, scheduled_spawner)
125        .add_systems(
126            Update,
127            (
128                mouse_handler,
129                movement_system,
130                collision_system,
131                counter_system,
132            ),
133        )
134        .insert_resource(Time::<Fixed>::from_duration(Duration::from_secs_f32(
135            FIXED_TIMESTEP,
136        )))
137        .run();
138}
139
140#[derive(Resource)]
141struct CubeScheduled {
142    waves: usize,
143    per_wave: usize,
144}
145
146fn scheduled_spawner(
147    mut commands: Commands,
148    args: Res<Args>,
149    mut scheduled: ResMut<CubeScheduled>,
150    mut counter: ResMut<BevyCounter>,
151    cube_resources: ResMut<CubeResources>,
152) {
153    if scheduled.waves > 0 {
154        let cube_resources = cube_resources.into_inner();
155        spawn_cubes(
156            &mut commands,
157            args.into_inner(),
158            &mut counter,
159            scheduled.per_wave,
160            cube_resources,
161            None,
162            scheduled.waves - 1,
163        );
164
165        scheduled.waves -= 1;
166    }
167}
168
169#[derive(Resource)]
170struct CubeResources {
171    _textures: Vec<Handle<Image>>,
172    materials: Vec<Handle<StandardMaterial>>,
173    cube_mesh: Handle<Mesh>,
174    color_rng: ChaCha8Rng,
175    material_rng: ChaCha8Rng,
176    velocity_rng: ChaCha8Rng,
177    transform_rng: ChaCha8Rng,
178}
179
180#[derive(Component)]
181struct StatsText;
182
183fn setup(
184    mut commands: Commands,
185    args: Res<Args>,
186    asset_server: Res<AssetServer>,
187    mut meshes: ResMut<Assets<Mesh>>,
188    material_assets: ResMut<Assets<StandardMaterial>>,
189    images: ResMut<Assets<Image>>,
190    counter: ResMut<BevyCounter>,
191) {
192    warn!(include_str!("warning_string.txt"));
193
194    let args = args.into_inner();
195    let images = images.into_inner();
196
197    let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
198    if args.material_texture_count > 0 {
199        textures.push(asset_server.load("branding/icon.png"));
200    }
201    init_textures(&mut textures, args, images);
202
203    let material_assets = material_assets.into_inner();
204    let materials = init_materials(args, &textures, material_assets);
205
206    let mut cube_resources = CubeResources {
207        _textures: textures,
208        materials,
209        cube_mesh: meshes.add(Cuboid::from_size(Vec3::splat(CUBE_SCALE))),
210        color_rng: ChaCha8Rng::seed_from_u64(42),
211        material_rng: ChaCha8Rng::seed_from_u64(12),
212        velocity_rng: ChaCha8Rng::seed_from_u64(97),
213        transform_rng: ChaCha8Rng::seed_from_u64(26),
214    };
215
216    let font = TextFont {
217        font_size: FontSize::Px(40.0),
218        ..Default::default()
219    };
220
221    commands.spawn((
222        Camera3d::default(),
223        Transform::from_translation(VOLUME_SIZE * 1.3).looking_at(Vec3::ZERO, Vec3::Y),
224    ));
225
226    commands.spawn((
227        DirectionalLight {
228            illuminance: 10000.0,
229            shadow_maps_enabled: false,
230            ..default()
231        },
232        Transform::from_xyz(1.0, 2.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
233    ));
234
235    commands.spawn((
236        Node {
237            position_type: PositionType::Absolute,
238            padding: UiRect::all(px(5)),
239            ..default()
240        },
241        BackgroundColor(Color::BLACK.with_alpha(0.75)),
242        GlobalZIndex(i32::MAX),
243        children![(
244            Text::default(),
245            StatsText,
246            children![
247                (
248                    TextSpan::new("Cube Count: "),
249                    font.clone(),
250                    TextColor(LIME.into()),
251                ),
252                (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
253                (
254                    TextSpan::new("\nFPS (raw): "),
255                    font.clone(),
256                    TextColor(LIME.into()),
257                ),
258                (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
259                (
260                    TextSpan::new("\nFPS (SMA): "),
261                    font.clone(),
262                    TextColor(LIME.into()),
263                ),
264                (TextSpan::new(""), font.clone(), TextColor(AQUA.into())),
265                (
266                    TextSpan::new("\nFPS (EMA): "),
267                    font.clone(),
268                    TextColor(LIME.into()),
269                ),
270                (TextSpan::new(""), font.clone(), TextColor(AQUA.into()))
271            ]
272        )],
273    ));
274
275    let mut scheduled = CubeScheduled {
276        per_wave: args.per_wave,
277        waves: args.waves,
278    };
279
280    if args.benchmark {
281        let counter = counter.into_inner();
282        for wave in (0..scheduled.waves).rev() {
283            spawn_cubes(
284                &mut commands,
285                args,
286                counter,
287                scheduled.per_wave,
288                &mut cube_resources,
289                Some(wave),
290                wave,
291            );
292        }
293        scheduled.waves = 0;
294    }
295    commands.insert_resource(cube_resources);
296    commands.insert_resource(scheduled);
297}
298
299fn mouse_handler(
300    mut commands: Commands,
301    args: Res<Args>,
302    time: Res<Time>,
303    mouse_button_input: Res<ButtonInput<MouseButton>>,
304    cube_resources: ResMut<CubeResources>,
305    mut counter: ResMut<BevyCounter>,
306    mut rng: Local<Option<ChaCha8Rng>>,
307    mut wave: Local<usize>,
308) {
309    if rng.is_none() {
310        *rng = Some(ChaCha8Rng::seed_from_u64(42));
311    }
312    let rng = rng.as_mut().unwrap();
313
314    if mouse_button_input.just_released(MouseButton::Left) {
315        counter.color = Color::linear_rgb(rng.random(), rng.random(), rng.random());
316    }
317
318    if mouse_button_input.pressed(MouseButton::Left) {
319        let spawn_count = (CUBES_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
320        spawn_cubes(
321            &mut commands,
322            args.into_inner(),
323            &mut counter,
324            spawn_count,
325            cube_resources.into_inner(),
326            None,
327            *wave,
328        );
329        *wave += 1;
330    }
331}
332
333fn cube_velocity_transform(
334    mut translation: Vec3,
335    velocity_rng: &mut ChaCha8Rng,
336    waves: Option<usize>,
337    dt: f32,
338) -> (Transform, Vec3) {
339    let mut velocity = Vec3::new(0., 0., MAX_VELOCITY * velocity_rng.random::<f32>());
340
341    if let Some(waves) = waves {
342        for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
343            step_movement(&mut translation, &mut velocity, dt);
344            handle_collision(&translation, &mut velocity);
345        }
346    }
347    (Transform::from_translation(translation), velocity)
348}
349
350const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
351
352fn spawn_cubes(
353    commands: &mut Commands,
354    args: &Args,
355    counter: &mut BevyCounter,
356    spawn_count: usize,
357    cube_resources: &mut CubeResources,
358    waves_to_simulate: Option<usize>,
359    wave: usize,
360) {
361    let batch_material = cube_resources.materials[wave % cube_resources.materials.len()].clone();
362
363    let spawn_y = VOLUME_SIZE.y / 2.0 - HALF_CUBE_SIZE;
364    let spawn_z = -VOLUME_SIZE.z / 2.0 + HALF_CUBE_SIZE;
365
366    let batch = (0..spawn_count)
367        .map(|_| {
368            let spawn_pos = Vec3::new(
369                (cube_resources.transform_rng.random::<f32>() - 0.5) * VOLUME_SIZE.x,
370                spawn_y,
371                spawn_z,
372            );
373
374            let (transform, velocity) = cube_velocity_transform(
375                spawn_pos,
376                &mut cube_resources.velocity_rng,
377                waves_to_simulate,
378                FIXED_DELTA_TIME,
379            );
380
381            let material = if args.vary_per_instance {
382                cube_resources
383                    .materials
384                    .choose(&mut cube_resources.material_rng)
385                    .unwrap()
386                    .clone()
387            } else {
388                batch_material.clone()
389            };
390
391            (
392                Mesh3d(cube_resources.cube_mesh.clone()),
393                MeshMaterial3d(material),
394                transform,
395                Cube { velocity },
396            )
397        })
398        .collect::<Vec<_>>();
399    commands.spawn_batch(batch);
400
401    counter.count += spawn_count;
402    counter.color = Color::linear_rgb(
403        cube_resources.color_rng.random(),
404        cube_resources.color_rng.random(),
405        cube_resources.color_rng.random(),
406    );
407}
408
409fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
410    translation.x += velocity.x * dt;
411    translation.y += velocity.y * dt;
412    translation.z += velocity.z * dt;
413    velocity.y += GRAVITY * dt;
414}
415
416fn movement_system(
417    args: Res<Args>,
418    time: Res<Time>,
419    mut cube_query: Query<(&mut Cube, &mut Transform)>,
420) {
421    let dt = if args.benchmark {
422        FIXED_DELTA_TIME
423    } else {
424        time.delta_secs()
425    };
426    for (mut cube, mut transform) in &mut cube_query {
427        step_movement(&mut transform.translation, &mut cube.velocity, dt);
428    }
429}
430
431fn handle_collision(translation: &Vec3, velocity: &mut Vec3) {
432    if (velocity.x > 0. && translation.x + HALF_CUBE_SIZE > VOLUME_SIZE.x / 2.0)
433        || (velocity.x <= 0. && translation.x - HALF_CUBE_SIZE < -VOLUME_SIZE.x / 2.0)
434    {
435        velocity.x = -velocity.x;
436    }
437    if (velocity.z > 0. && translation.z + HALF_CUBE_SIZE > VOLUME_SIZE.z / 2.0)
438        || (velocity.z <= 0. && translation.z - HALF_CUBE_SIZE < -VOLUME_SIZE.z / 2.0)
439    {
440        velocity.z = -velocity.z;
441    }
442
443    let velocity_y = velocity.y;
444    if velocity_y < 0. && translation.y - HALF_CUBE_SIZE < -VOLUME_SIZE.y / 2.0 {
445        velocity.y = -velocity_y;
446    }
447    if translation.y + HALF_CUBE_SIZE > VOLUME_SIZE.y / 2.0 && velocity_y > 0.0 {
448        velocity.y = 0.0;
449    }
450}
451
452fn collision_system(mut cube_query: Query<(&mut Cube, &Transform)>) {
453    cube_query.par_iter_mut().for_each(|(mut cube, transform)| {
454        handle_collision(&transform.translation, &mut cube.velocity);
455    });
456}
457
458fn counter_system(
459    diagnostics: Res<DiagnosticsStore>,
460    counter: Res<BevyCounter>,
461    query: Single<Entity, With<StatsText>>,
462    mut writer: TextUiWriter,
463) {
464    let text = *query;
465
466    if counter.is_changed() {
467        *writer.text(text, 2) = counter.count.to_string();
468    }
469
470    if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
471        if let Some(raw) = fps.value() {
472            *writer.text(text, 4) = format!("{raw:.2}");
473        }
474        if let Some(sma) = fps.average() {
475            *writer.text(text, 6) = format!("{sma:.2}");
476        }
477        if let Some(ema) = fps.smoothed() {
478            *writer.text(text, 8) = format!("{ema:.2}");
479        }
480    };
481}
482
483fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
484    let mut color_rng = ChaCha8Rng::seed_from_u64(42);
485    while textures.len() < args.material_texture_count {
486        let pixel = [
487            color_rng.random(),
488            color_rng.random(),
489            color_rng.random(),
490            255,
491        ];
492        textures.push(images.add(Image::new_fill(
493            Extent3d {
494                width: CUBE_TEXTURE_SIZE as u32,
495                height: CUBE_TEXTURE_SIZE as u32,
496                depth_or_array_layers: 1,
497            },
498            TextureDimension::D2,
499            &pixel,
500            TextureFormat::Rgba8UnormSrgb,
501            RenderAssetUsages::RENDER_WORLD,
502        )));
503    }
504}
505
506fn init_materials(
507    args: &Args,
508    textures: &[Handle<Image>],
509    assets: &mut Assets<StandardMaterial>,
510) -> Vec<Handle<StandardMaterial>> {
511    let mut capacity = if args.vary_per_instance {
512        args.per_wave * args.waves
513    } else {
514        args.material_texture_count.max(args.waves)
515    };
516    if !args.benchmark {
517        capacity = capacity.max(256);
518    }
519    capacity = capacity.max(1);
520
521    let alpha_mode = match args.alpha_mode {
522        AlphaMode::Opaque => bevy::prelude::AlphaMode::Opaque,
523        AlphaMode::Blend => bevy::prelude::AlphaMode::Blend,
524        AlphaMode::AlphaMask => bevy::prelude::AlphaMode::Mask(0.5),
525    };
526
527    let mut materials = Vec::with_capacity(capacity);
528    materials.push(assets.add(StandardMaterial {
529        base_color: Color::WHITE,
530        base_color_texture: textures.first().cloned(),
531        alpha_mode,
532        ..default()
533    }));
534
535    let mut color_rng = ChaCha8Rng::seed_from_u64(42);
536    let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
537    materials.extend(
538        std::iter::repeat_with(|| {
539            assets.add(StandardMaterial {
540                base_color: Color::linear_rgb(
541                    color_rng.random(),
542                    color_rng.random(),
543                    color_rng.random(),
544                ),
545                base_color_texture: textures.choose(&mut texture_rng).cloned(),
546                alpha_mode,
547                ..default()
548            })
549        })
550        .take(capacity - materials.len()),
551    );
552
553    materials
554}