diff --git a/Cargo.toml b/Cargo.toml index 7dda86c8f1418..98a169b0c4d10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3163,6 +3163,17 @@ description = "A heavy sprite rendering workload to benchmark your system with B category = "Stress Tests" wasm = true +[[example]] +name = "bevymark_3d" +path = "examples/stress_tests/bevymark_3d.rs" +doc-scrape-examples = true + +[package.metadata.example.bevymark_3d] +name = "Bevymark 3D" +description = "A heavy 3D cube rendering workload to benchmark your system with Bevy" +category = "Stress Tests" +wasm = true + [[example]] name = "many_animated_sprites" path = "examples/stress_tests/many_animated_sprites.rs" diff --git a/examples/README.md b/examples/README.md index da86eac2fbdc1..6aedb17601da6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -517,6 +517,7 @@ cargo run --release --example Example | Description --- | --- [Bevymark](../examples/stress_tests/bevymark.rs) | A heavy sprite rendering workload to benchmark your system with Bevy +[Bevymark 3D](../examples/stress_tests/bevymark_3d.rs) | A heavy 3D cube rendering workload to benchmark your system with Bevy [Many Animated Materials](../examples/stress_tests/many_materials.rs) | Benchmark to test rendering many animated materials [Many Animated Sprites](../examples/stress_tests/many_animated_sprites.rs) | Displays many animated sprites in a grid arrangement with slight offsets to their animation timers. Used for performance testing. [Many Buttons](../examples/stress_tests/many_buttons.rs) | Test rendering of many UI elements diff --git a/examples/stress_tests/bevymark_3d.rs b/examples/stress_tests/bevymark_3d.rs new file mode 100644 index 0000000000000..f71c69671bfb1 --- /dev/null +++ b/examples/stress_tests/bevymark_3d.rs @@ -0,0 +1,552 @@ +//! This example provides a 3D benchmark. +//! +//! Usage: spawn more entities by clicking with the left mouse button. + +use core::time::Duration; +use std::str::FromStr; + +use argh::FromArgs; +use bevy::{ + asset::RenderAssetUsages, + color::palettes::basic::*, + diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + prelude::*, + render::render_resource::{Extent3d, TextureDimension, TextureFormat}, + window::{PresentMode, WindowResolution}, + winit::WinitSettings, +}; +use rand::{seq::IndexedRandom, Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +const CUBES_PER_SECOND: u32 = 10000; +const GRAVITY: f32 = -9.8; +const MAX_VELOCITY: f32 = 10.; +const CUBE_SCALE: f32 = 1.0; +const CUBE_TEXTURE_SIZE: usize = 256; +const HALF_CUBE_SIZE: f32 = CUBE_SCALE * 0.5; +const VOLUME_WIDTH: usize = 50; +const VOLUME_SIZE: Vec3 = Vec3::splat(VOLUME_WIDTH as f32); + +#[derive(Resource)] +struct BevyCounter { + pub count: usize, + pub color: Color, +} + +#[derive(Component)] +struct Cube { + velocity: Vec3, +} + +#[derive(FromArgs, Resource)] +/// `bevymark_3d` cube stress test +struct Args { + /// whether to step animations by a fixed amount such that each frame is the same across runs. + /// If spawning waves, all are spawned up-front to immediately start rendering at the heaviest + /// load. + #[argh(switch)] + benchmark: bool, + + /// how many cubes to spawn per wave. + #[argh(option, default = "0")] + per_wave: usize, + + /// the number of waves to spawn. + #[argh(option, default = "0")] + waves: usize, + + /// whether to vary the material data in each instance. + #[argh(switch)] + vary_per_instance: bool, + + /// the number of different textures from which to randomly select the material color. 0 means no textures. + #[argh(option, default = "1")] + material_texture_count: usize, + + /// the alpha mode used to spawn the cubes + #[argh(option, default = "AlphaMode::Opaque")] + alpha_mode: AlphaMode, +} + +#[derive(Default, Clone)] +enum AlphaMode { + #[default] + Opaque, + Blend, + AlphaMask, +} + +impl FromStr for AlphaMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "opaque" => Ok(Self::Opaque), + "blend" => Ok(Self::Blend), + "alpha_mask" => Ok(Self::AlphaMask), + _ => Err(format!( + "Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'" + )), + } + } +} + +const FIXED_TIMESTEP: f32 = 0.2; + +fn main() { + // `from_env` panics on the web + #[cfg(not(target_arch = "wasm32"))] + let args: Args = argh::from_env(); + #[cfg(target_arch = "wasm32")] + let args = Args::from_args(&[], &[]).unwrap(); + + App::new() + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "BevyMark 3D".into(), + resolution: WindowResolution::new(1920, 1080).with_scale_factor_override(1.0), + present_mode: PresentMode::AutoNoVsync, + ..default() + }), + ..default() + }), + FrameTimeDiagnosticsPlugin::default(), + LogDiagnosticsPlugin::default(), + )) + .insert_resource(WinitSettings::continuous()) + .insert_resource(args) + .insert_resource(BevyCounter { + count: 0, + color: Color::WHITE, + }) + .add_systems(Startup, setup) + .add_systems(FixedUpdate, scheduled_spawner) + .add_systems( + Update, + ( + mouse_handler, + movement_system, + collision_system, + counter_system, + ), + ) + .insert_resource(Time::::from_duration(Duration::from_secs_f32( + FIXED_TIMESTEP, + ))) + .run(); +} + +#[derive(Resource)] +struct CubeScheduled { + waves: usize, + per_wave: usize, +} + +fn scheduled_spawner( + mut commands: Commands, + args: Res, + mut scheduled: ResMut, + mut counter: ResMut, + cube_resources: ResMut, +) { + if scheduled.waves > 0 { + let cube_resources = cube_resources.into_inner(); + spawn_cubes( + &mut commands, + args.into_inner(), + &mut counter, + scheduled.per_wave, + cube_resources, + None, + scheduled.waves - 1, + ); + + scheduled.waves -= 1; + } +} + +#[derive(Resource)] +struct CubeResources { + _textures: Vec>, + materials: Vec>, + cube_mesh: Handle, + color_rng: ChaCha8Rng, + material_rng: ChaCha8Rng, + velocity_rng: ChaCha8Rng, + transform_rng: ChaCha8Rng, +} + +#[derive(Component)] +struct StatsText; + +fn setup( + mut commands: Commands, + args: Res, + asset_server: Res, + mut meshes: ResMut>, + material_assets: ResMut>, + images: ResMut>, + counter: ResMut, +) { + let args = args.into_inner(); + let images = images.into_inner(); + + let mut textures = Vec::with_capacity(args.material_texture_count.max(1)); + if args.material_texture_count > 0 { + textures.push(asset_server.load("branding/icon.png")); + } + init_textures(&mut textures, args, images); + + let material_assets = material_assets.into_inner(); + let materials = init_materials(args, &textures, material_assets); + + let mut cube_resources = CubeResources { + _textures: textures, + materials, + cube_mesh: meshes.add(Cuboid::from_size(Vec3::splat(CUBE_SCALE))), + color_rng: ChaCha8Rng::seed_from_u64(42), + material_rng: ChaCha8Rng::seed_from_u64(12), + velocity_rng: ChaCha8Rng::seed_from_u64(97), + transform_rng: ChaCha8Rng::seed_from_u64(26), + }; + + let font = TextFont { + font_size: 40.0, + ..Default::default() + }; + + commands.spawn(( + Camera3d::default(), + Transform::from_translation(VOLUME_SIZE * 1.3).looking_at(Vec3::ZERO, Vec3::Y), + )); + + commands.spawn(( + DirectionalLight { + illuminance: 10000.0, + shadows_enabled: false, + ..default() + }, + Transform::from_xyz(1.0, 2.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y), + )); + + commands.spawn(( + Node { + position_type: PositionType::Absolute, + padding: UiRect::all(px(5)), + ..default() + }, + BackgroundColor(Color::BLACK.with_alpha(0.75)), + GlobalZIndex(i32::MAX), + children![( + Text::default(), + StatsText, + children![ + ( + TextSpan::new("Cube Count: "), + font.clone(), + TextColor(LIME.into()), + ), + (TextSpan::new(""), font.clone(), TextColor(AQUA.into())), + ( + TextSpan::new("\nFPS (raw): "), + font.clone(), + TextColor(LIME.into()), + ), + (TextSpan::new(""), font.clone(), TextColor(AQUA.into())), + ( + TextSpan::new("\nFPS (SMA): "), + font.clone(), + TextColor(LIME.into()), + ), + (TextSpan::new(""), font.clone(), TextColor(AQUA.into())), + ( + TextSpan::new("\nFPS (EMA): "), + font.clone(), + TextColor(LIME.into()), + ), + (TextSpan::new(""), font.clone(), TextColor(AQUA.into())) + ] + )], + )); + + let mut scheduled = CubeScheduled { + per_wave: args.per_wave, + waves: args.waves, + }; + + if args.benchmark { + let counter = counter.into_inner(); + for wave in (0..scheduled.waves).rev() { + spawn_cubes( + &mut commands, + args, + counter, + scheduled.per_wave, + &mut cube_resources, + Some(wave), + wave, + ); + } + scheduled.waves = 0; + } + commands.insert_resource(cube_resources); + commands.insert_resource(scheduled); +} + +fn mouse_handler( + mut commands: Commands, + args: Res, + time: Res