Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
# Stress tests
These examples are used to stress test Bevy's performance in various ways. These
should be run with the "stress-test" profile to accurately represent performance
in production, otherwise they will run in cargo's default "dev" profile which is
very slow.
## Example Command
```bash
cargo run --profile stress-test --example <EXAMPLE>
```

View File

@@ -0,0 +1,636 @@
//! This example provides a 2D benchmark.
//!
//! Usage: spawn more entities by clicking on the screen.
use core::time::Duration;
use std::str::FromStr;
use argh::FromArgs;
use bevy::{
color::palettes::basic::*,
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
render::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
},
sprite::AlphaMode2d,
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
use rand::{seq::SliceRandom, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
const BIRDS_PER_SECOND: u32 = 10000;
const GRAVITY: f32 = -9.8 * 100.0;
const MAX_VELOCITY: f32 = 750.;
const BIRD_SCALE: f32 = 0.15;
const BIRD_TEXTURE_SIZE: usize = 256;
const HALF_BIRD_SIZE: f32 = BIRD_TEXTURE_SIZE as f32 * BIRD_SCALE * 0.5;
#[derive(Resource)]
struct BevyCounter {
pub count: usize,
pub color: Color,
}
#[derive(Component)]
struct Bird {
velocity: Vec3,
}
#[derive(FromArgs, Resource)]
/// `bevymark` sprite / 2D mesh stress test
struct Args {
/// whether to use sprite or mesh2d
#[argh(option, default = "Mode::Sprite")]
mode: Mode,
/// 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 birds 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,
/// generate z values in increasing order rather than randomly
#[argh(switch)]
ordered_z: bool,
/// the alpha mode used to spawn the sprites
#[argh(option, default = "AlphaMode::Blend")]
alpha_mode: AlphaMode,
}
#[derive(Default, Clone)]
enum Mode {
#[default]
Sprite,
Mesh2d,
}
impl FromStr for Mode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"sprite" => Ok(Self::Sprite),
"mesh2d" => Ok(Self::Mesh2d),
_ => Err(format!(
"Unknown mode: '{s}', valid modes: 'sprite', 'mesh2d'"
)),
}
}
}
#[derive(Default, Clone)]
enum AlphaMode {
Opaque,
#[default]
Blend,
AlphaMask,
}
impl FromStr for AlphaMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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".into(),
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::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::<Fixed>::from_duration(Duration::from_secs_f32(
FIXED_TIMESTEP,
)))
.run();
}
#[derive(Resource)]
struct BirdScheduled {
waves: usize,
per_wave: usize,
}
fn scheduled_spawner(
mut commands: Commands,
args: Res<Args>,
window: Single<&Window>,
mut scheduled: ResMut<BirdScheduled>,
mut counter: ResMut<BevyCounter>,
bird_resources: ResMut<BirdResources>,
) {
if scheduled.waves > 0 {
let bird_resources = bird_resources.into_inner();
spawn_birds(
&mut commands,
args.into_inner(),
&window.resolution,
&mut counter,
scheduled.per_wave,
bird_resources,
None,
scheduled.waves - 1,
);
scheduled.waves -= 1;
}
}
#[derive(Resource)]
struct BirdResources {
textures: Vec<Handle<Image>>,
materials: Vec<Handle<ColorMaterial>>,
quad: Handle<Mesh>,
color_rng: ChaCha8Rng,
material_rng: ChaCha8Rng,
velocity_rng: ChaCha8Rng,
transform_rng: ChaCha8Rng,
}
#[derive(Component)]
struct StatsText;
fn setup(
mut commands: Commands,
args: Res<Args>,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
material_assets: ResMut<Assets<ColorMaterial>>,
images: ResMut<Assets<Image>>,
window: Single<&Window>,
counter: ResMut<BevyCounter>,
) {
warn!(include_str!("warning_string.txt"));
let args = args.into_inner();
let images = images.into_inner();
let mut textures = Vec::with_capacity(args.material_texture_count.max(1));
if matches!(args.mode, Mode::Sprite) || 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 bird_resources = BirdResources {
textures,
materials,
quad: meshes.add(Rectangle::from_size(Vec2::splat(BIRD_TEXTURE_SIZE as f32))),
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
color_rng: ChaCha8Rng::seed_from_u64(42),
material_rng: ChaCha8Rng::seed_from_u64(42),
velocity_rng: ChaCha8Rng::seed_from_u64(42),
transform_rng: ChaCha8Rng::seed_from_u64(42),
};
let font = TextFont {
font_size: 40.0,
..Default::default()
};
commands.spawn(Camera2d);
commands
.spawn((
Node {
position_type: PositionType::Absolute,
padding: UiRect::all(Val::Px(5.0)),
..default()
},
BackgroundColor(Color::BLACK.with_alpha(0.75)),
GlobalZIndex(i32::MAX),
))
.with_children(|p| {
p.spawn((Text::default(), StatsText)).with_children(|p| {
p.spawn((
TextSpan::new("Bird Count: "),
font.clone(),
TextColor(LIME.into()),
));
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
p.spawn((
TextSpan::new("\nFPS (raw): "),
font.clone(),
TextColor(LIME.into()),
));
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
p.spawn((
TextSpan::new("\nFPS (SMA): "),
font.clone(),
TextColor(LIME.into()),
));
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
p.spawn((
TextSpan::new("\nFPS (EMA): "),
font.clone(),
TextColor(LIME.into()),
));
p.spawn((TextSpan::new(""), font.clone(), TextColor(AQUA.into())));
});
});
let mut scheduled = BirdScheduled {
per_wave: args.per_wave,
waves: args.waves,
};
if args.benchmark {
let counter = counter.into_inner();
for wave in (0..scheduled.waves).rev() {
spawn_birds(
&mut commands,
args,
&window.resolution,
counter,
scheduled.per_wave,
&mut bird_resources,
Some(wave),
wave,
);
}
scheduled.waves = 0;
}
commands.insert_resource(bird_resources);
commands.insert_resource(scheduled);
}
fn mouse_handler(
mut commands: Commands,
args: Res<Args>,
time: Res<Time>,
mouse_button_input: Res<ButtonInput<MouseButton>>,
window: Query<&Window>,
bird_resources: ResMut<BirdResources>,
mut counter: ResMut<BevyCounter>,
mut rng: Local<Option<ChaCha8Rng>>,
mut wave: Local<usize>,
) {
let Ok(window) = window.single() else {
return;
};
if rng.is_none() {
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
*rng = Some(ChaCha8Rng::seed_from_u64(42));
}
let rng = rng.as_mut().unwrap();
if mouse_button_input.just_released(MouseButton::Left) {
counter.color = Color::linear_rgb(rng.r#gen(), rng.r#gen(), rng.r#gen());
}
if mouse_button_input.pressed(MouseButton::Left) {
let spawn_count = (BIRDS_PER_SECOND as f64 * time.delta_secs_f64()) as usize;
spawn_birds(
&mut commands,
args.into_inner(),
&window.resolution,
&mut counter,
spawn_count,
bird_resources.into_inner(),
None,
*wave,
);
*wave += 1;
}
}
fn bird_velocity_transform(
half_extents: Vec2,
mut translation: Vec3,
velocity_rng: &mut ChaCha8Rng,
waves: Option<usize>,
dt: f32,
) -> (Transform, Vec3) {
let mut velocity = Vec3::new(MAX_VELOCITY * (velocity_rng.r#gen::<f32>() - 0.5), 0., 0.);
if let Some(waves) = waves {
// Step the movement and handle collisions as if the wave had been spawned at fixed time intervals
// and with dt-spaced frames of simulation
for _ in 0..(waves * (FIXED_TIMESTEP / dt).round() as usize) {
step_movement(&mut translation, &mut velocity, dt);
handle_collision(half_extents, &translation, &mut velocity);
}
}
(
Transform::from_translation(translation).with_scale(Vec3::splat(BIRD_SCALE)),
velocity,
)
}
const FIXED_DELTA_TIME: f32 = 1.0 / 60.0;
fn spawn_birds(
commands: &mut Commands,
args: &Args,
primary_window_resolution: &WindowResolution,
counter: &mut BevyCounter,
spawn_count: usize,
bird_resources: &mut BirdResources,
waves_to_simulate: Option<usize>,
wave: usize,
) {
let bird_x = (primary_window_resolution.width() / -2.) + HALF_BIRD_SIZE;
let bird_y = (primary_window_resolution.height() / 2.) - HALF_BIRD_SIZE;
let half_extents = 0.5 * primary_window_resolution.size();
let color = counter.color;
let current_count = counter.count;
match args.mode {
Mode::Sprite => {
let batch = (0..spawn_count)
.map(|count| {
let bird_z = if args.ordered_z {
(current_count + count) as f32 * 0.00001
} else {
bird_resources.transform_rng.r#gen::<f32>()
};
let (transform, velocity) = bird_velocity_transform(
half_extents,
Vec3::new(bird_x, bird_y, bird_z),
&mut bird_resources.velocity_rng,
waves_to_simulate,
FIXED_DELTA_TIME,
);
let color = if args.vary_per_instance {
Color::linear_rgb(
bird_resources.color_rng.r#gen(),
bird_resources.color_rng.r#gen(),
bird_resources.color_rng.r#gen(),
)
} else {
color
};
(
Sprite {
image: bird_resources
.textures
.choose(&mut bird_resources.material_rng)
.unwrap()
.clone(),
color,
..default()
},
transform,
Bird { velocity },
)
})
.collect::<Vec<_>>();
commands.spawn_batch(batch);
}
Mode::Mesh2d => {
let batch = (0..spawn_count)
.map(|count| {
let bird_z = if args.ordered_z {
(current_count + count) as f32 * 0.00001
} else {
bird_resources.transform_rng.r#gen::<f32>()
};
let (transform, velocity) = bird_velocity_transform(
half_extents,
Vec3::new(bird_x, bird_y, bird_z),
&mut bird_resources.velocity_rng,
waves_to_simulate,
FIXED_DELTA_TIME,
);
let material =
if args.vary_per_instance || args.material_texture_count > args.waves {
bird_resources
.materials
.choose(&mut bird_resources.material_rng)
.unwrap()
.clone()
} else {
bird_resources.materials[wave % bird_resources.materials.len()].clone()
};
(
Mesh2d(bird_resources.quad.clone()),
MeshMaterial2d(material),
transform,
Bird { velocity },
)
})
.collect::<Vec<_>>();
commands.spawn_batch(batch);
}
}
counter.count += spawn_count;
counter.color = Color::linear_rgb(
bird_resources.color_rng.r#gen(),
bird_resources.color_rng.r#gen(),
bird_resources.color_rng.r#gen(),
);
}
fn step_movement(translation: &mut Vec3, velocity: &mut Vec3, dt: f32) {
translation.x += velocity.x * dt;
translation.y += velocity.y * dt;
velocity.y += GRAVITY * dt;
}
fn movement_system(
args: Res<Args>,
time: Res<Time>,
mut bird_query: Query<(&mut Bird, &mut Transform)>,
) {
let dt = if args.benchmark {
FIXED_DELTA_TIME
} else {
time.delta_secs()
};
for (mut bird, mut transform) in &mut bird_query {
step_movement(&mut transform.translation, &mut bird.velocity, dt);
}
}
fn handle_collision(half_extents: Vec2, translation: &Vec3, velocity: &mut Vec3) {
if (velocity.x > 0. && translation.x + HALF_BIRD_SIZE > half_extents.x)
|| (velocity.x <= 0. && translation.x - HALF_BIRD_SIZE < -half_extents.x)
{
velocity.x = -velocity.x;
}
let velocity_y = velocity.y;
if velocity_y < 0. && translation.y - HALF_BIRD_SIZE < -half_extents.y {
velocity.y = -velocity_y;
}
if translation.y + HALF_BIRD_SIZE > half_extents.y && velocity_y > 0.0 {
velocity.y = 0.0;
}
}
fn collision_system(window: Query<&Window>, mut bird_query: Query<(&mut Bird, &Transform)>) {
let Ok(window) = window.single() else {
return;
};
let half_extents = 0.5 * window.size();
for (mut bird, transform) in &mut bird_query {
handle_collision(half_extents, &transform.translation, &mut bird.velocity);
}
}
fn counter_system(
diagnostics: Res<DiagnosticsStore>,
counter: Res<BevyCounter>,
query: Single<Entity, With<StatsText>>,
mut writer: TextUiWriter,
) {
let text = *query;
if counter.is_changed() {
*writer.text(text, 2) = counter.count.to_string();
}
if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
if let Some(raw) = fps.value() {
*writer.text(text, 4) = format!("{raw:.2}");
}
if let Some(sma) = fps.average() {
*writer.text(text, 6) = format!("{sma:.2}");
}
if let Some(ema) = fps.smoothed() {
*writer.text(text, 8) = format!("{ema:.2}");
}
};
}
fn init_textures(textures: &mut Vec<Handle<Image>>, args: &Args, images: &mut Assets<Image>) {
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
while textures.len() < args.material_texture_count {
let pixel = [color_rng.r#gen(), color_rng.r#gen(), color_rng.r#gen(), 255];
textures.push(images.add(Image::new_fill(
Extent3d {
width: BIRD_TEXTURE_SIZE as u32,
height: BIRD_TEXTURE_SIZE as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&pixel,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
)));
}
}
fn init_materials(
args: &Args,
textures: &[Handle<Image>],
assets: &mut Assets<ColorMaterial>,
) -> Vec<Handle<ColorMaterial>> {
let capacity = if args.vary_per_instance {
args.per_wave * args.waves
} else {
args.material_texture_count.max(args.waves)
}
.max(1);
let alpha_mode = match args.alpha_mode {
AlphaMode::Opaque => AlphaMode2d::Opaque,
AlphaMode::Blend => AlphaMode2d::Blend,
AlphaMode::AlphaMask => AlphaMode2d::Mask(0.5),
};
let mut materials = Vec::with_capacity(capacity);
materials.push(assets.add(ColorMaterial {
color: Color::WHITE,
texture: textures.first().cloned(),
alpha_mode,
..default()
}));
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
materials.extend(
std::iter::repeat_with(|| {
assets.add(ColorMaterial {
color: Color::srgb_u8(color_rng.r#gen(), color_rng.r#gen(), color_rng.r#gen()),
texture: textures.choose(&mut texture_rng).cloned(),
alpha_mode,
..default()
})
})
.take(capacity - materials.len()),
);
materials
}

View File

@@ -0,0 +1,145 @@
//! Renders a lot of animated sprites to allow performance testing.
//!
//! This example sets up many animated sprites in different sizes, rotations, and scales in the world.
//! It also moves the camera over them to see how well frustum culling works.
use std::time::Duration;
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
use rand::Rng;
const CAMERA_SPEED: f32 = 1000.0;
fn main() {
App::new()
// Since this is also used as a benchmark, we want it to display performance data.
.add_plugins((
LogDiagnosticsPlugin::default(),
FrameTimeDiagnosticsPlugin::default(),
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
..default()
}),
..default()
}),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.add_systems(Startup, setup)
.add_systems(
Update,
(
animate_sprite,
print_sprite_count,
move_camera.after(print_sprite_count),
),
)
.run();
}
fn setup(
mut commands: Commands,
assets: Res<AssetServer>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
) {
warn!(include_str!("warning_string.txt"));
let mut rng = rand::thread_rng();
let tile_size = Vec2::splat(64.0);
let map_size = Vec2::splat(320.0);
let half_x = (map_size.x / 2.0) as i32;
let half_y = (map_size.y / 2.0) as i32;
let texture_handle = assets.load("textures/rpg/chars/gabe/gabe-idle-run.png");
let texture_atlas = TextureAtlasLayout::from_grid(UVec2::splat(24), 7, 1, None, None);
let texture_atlas_handle = texture_atlases.add(texture_atlas);
// Spawns the camera
commands.spawn(Camera2d);
// Builds and spawns the sprites
for y in -half_y..half_y {
for x in -half_x..half_x {
let position = Vec2::new(x as f32, y as f32);
let translation = (position * tile_size).extend(rng.r#gen::<f32>());
let rotation = Quat::from_rotation_z(rng.r#gen::<f32>());
let scale = Vec3::splat(rng.r#gen::<f32>() * 2.0);
let mut timer = Timer::from_seconds(0.1, TimerMode::Repeating);
timer.set_elapsed(Duration::from_secs_f32(rng.r#gen::<f32>()));
commands.spawn((
Sprite {
image: texture_handle.clone(),
texture_atlas: Some(TextureAtlas::from(texture_atlas_handle.clone())),
custom_size: Some(tile_size),
..default()
},
Transform {
translation,
rotation,
scale,
},
AnimationTimer(timer),
));
}
}
}
// System for rotating and translating the camera
fn move_camera(time: Res<Time>, mut camera_transform: Single<&mut Transform, With<Camera>>) {
camera_transform.rotate(Quat::from_rotation_z(time.delta_secs() * 0.5));
**camera_transform = **camera_transform
* Transform::from_translation(Vec3::X * CAMERA_SPEED * time.delta_secs());
}
#[derive(Component, Deref, DerefMut)]
struct AnimationTimer(Timer);
fn animate_sprite(
time: Res<Time>,
texture_atlases: Res<Assets<TextureAtlasLayout>>,
mut query: Query<(&mut AnimationTimer, &mut Sprite)>,
) {
for (mut timer, mut sprite) in query.iter_mut() {
timer.tick(time.delta());
if timer.just_finished() {
let Some(atlas) = &mut sprite.texture_atlas else {
continue;
};
let texture_atlas = texture_atlases.get(&atlas.layout).unwrap();
atlas.index = (atlas.index + 1) % texture_atlas.textures.len();
}
}
}
#[derive(Deref, DerefMut)]
struct PrintingTimer(Timer);
impl Default for PrintingTimer {
fn default() -> Self {
Self(Timer::from_seconds(1.0, TimerMode::Repeating))
}
}
// System for printing the number of sprites on every tick of the timer
fn print_sprite_count(time: Res<Time>, mut timer: Local<PrintingTimer>, sprites: Query<&Sprite>) {
timer.tick(time.delta());
if timer.just_finished() {
info!("Sprites: {}", sprites.iter().count());
}
}

View File

@@ -0,0 +1,396 @@
//! General UI benchmark that stress tests layouting, text, interaction and rendering
use argh::FromArgs;
use bevy::{
color::palettes::css::ORANGE_RED,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
text::TextColor,
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
const FONT_SIZE: f32 = 7.0;
#[derive(FromArgs, Resource)]
/// `many_buttons` general UI benchmark that stress tests layouting, text, interaction and rendering
struct Args {
/// whether to add text to each button
#[argh(switch)]
no_text: bool,
/// whether to add borders to each button
#[argh(switch)]
no_borders: bool,
/// whether to perform a full relayout each frame
#[argh(switch)]
relayout: bool,
/// whether to recompute all text each frame
#[argh(switch)]
recompute_text: bool,
/// how many buttons per row and column of the grid.
#[argh(option, default = "110")]
buttons: usize,
/// give every nth button an image
#[argh(option, default = "4")]
image_freq: usize,
/// use the grid layout model
#[argh(switch)]
grid: bool,
/// at the start of each frame despawn any existing UI nodes and spawn a new UI tree
#[argh(switch)]
respawn: bool,
/// set the root node to display none, removing all nodes from the layout.
#[argh(switch)]
display_none: bool,
/// spawn the layout without a camera
#[argh(switch)]
no_camera: bool,
/// a layout with a separate camera for each button
#[argh(switch)]
many_cameras: bool,
}
/// This example shows what happens when there is a lot of buttons on screen.
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();
warn!(include_str!("warning_string.txt"));
let mut app = App::new();
app.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0).with_scale_factor_override(1.0),
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.add_systems(Update, (button_system, set_text_colors_changed));
if !args.no_camera {
app.add_systems(Startup, |mut commands: Commands| {
commands.spawn(Camera2d);
});
}
if args.many_cameras {
app.add_systems(Startup, setup_many_cameras);
} else if args.grid {
app.add_systems(Startup, setup_grid);
} else {
app.add_systems(Startup, setup_flex);
}
if args.relayout {
app.add_systems(Update, |mut nodes: Query<&mut Node>| {
nodes.iter_mut().for_each(|mut node| node.set_changed());
});
}
if args.recompute_text {
app.add_systems(Update, |mut text_query: Query<&mut Text>| {
text_query
.iter_mut()
.for_each(|mut text| text.set_changed());
});
}
if args.respawn {
if args.grid {
app.add_systems(Update, (despawn_ui, setup_grid).chain());
} else {
app.add_systems(Update, (despawn_ui, setup_flex).chain());
}
}
app.insert_resource(args).run();
}
fn set_text_colors_changed(mut colors: Query<&mut TextColor>) {
for mut text_color in colors.iter_mut() {
text_color.set_changed();
}
}
#[derive(Component)]
struct IdleColor(Color);
fn button_system(
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor, &IdleColor),
Changed<Interaction>,
>,
) {
for (interaction, mut color, &IdleColor(idle_color)) in interaction_query.iter_mut() {
*color = match interaction {
Interaction::Hovered => ORANGE_RED.into(),
_ => idle_color.into(),
};
}
}
fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
let image = if 0 < args.image_freq {
Some(asset_server.load("branding/icon.png"))
} else {
None
};
let buttons_f = args.buttons as f32;
let border = if args.no_borders {
UiRect::ZERO
} else {
UiRect::all(Val::VMin(0.05 * 90. / buttons_f))
};
let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
commands
.spawn(Node {
display: if args.display_none {
Display::None
} else {
Display::Flex
},
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: Val::Percent(100.),
height: Val::Percent(100.),
..default()
})
.with_children(|commands| {
for column in 0..args.buttons {
commands.spawn(Node::default()).with_children(|commands| {
for row in 0..args.buttons {
let color = as_rainbow(row % column.max(1));
let border_color = Color::WHITE.with_alpha(0.5).into();
spawn_button(
commands,
color,
buttons_f,
column,
row,
!args.no_text,
border,
border_color,
image
.as_ref()
.filter(|_| (column + row) % args.image_freq == 0)
.cloned(),
);
}
});
}
});
}
fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
let image = if 0 < args.image_freq {
Some(asset_server.load("branding/icon.png"))
} else {
None
};
let buttons_f = args.buttons as f32;
let border = if args.no_borders {
UiRect::ZERO
} else {
UiRect::all(Val::VMin(0.05 * 90. / buttons_f))
};
let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
commands
.spawn(Node {
display: if args.display_none {
Display::None
} else {
Display::Grid
},
width: Val::Percent(100.),
height: Val::Percent(100.0),
grid_template_columns: RepeatedGridTrack::flex(args.buttons as u16, 1.0),
grid_template_rows: RepeatedGridTrack::flex(args.buttons as u16, 1.0),
..default()
})
.with_children(|commands| {
for column in 0..args.buttons {
for row in 0..args.buttons {
let color = as_rainbow(row % column.max(1));
let border_color = Color::WHITE.with_alpha(0.5).into();
spawn_button(
commands,
color,
buttons_f,
column,
row,
!args.no_text,
border,
border_color,
image
.as_ref()
.filter(|_| (column + row) % args.image_freq == 0)
.cloned(),
);
}
}
});
}
fn spawn_button(
commands: &mut ChildSpawnerCommands,
background_color: Color,
buttons: f32,
column: usize,
row: usize,
spawn_text: bool,
border: UiRect,
border_color: BorderColor,
image: Option<Handle<Image>>,
) {
let width = Val::Vw(90.0 / buttons);
let height = Val::Vh(90.0 / buttons);
let margin = UiRect::axes(width * 0.05, height * 0.05);
let mut builder = commands.spawn((
Button,
Node {
width,
height,
margin,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
border,
..default()
},
BackgroundColor(background_color),
border_color,
IdleColor(background_color),
));
if let Some(image) = image {
builder.insert(ImageNode::new(image));
}
if spawn_text {
builder.with_children(|parent| {
// These labels are split to stress test multi-span text
parent
.spawn((
Text(format!("{column}, ")),
TextFont {
font_size: FONT_SIZE,
..default()
},
TextColor(Color::srgb(0.5, 0.2, 0.2)),
))
.with_child((
TextSpan(format!("{row}")),
TextFont {
font_size: FONT_SIZE,
..default()
},
TextColor(Color::srgb(0.2, 0.2, 0.5)),
));
});
}
}
fn despawn_ui(mut commands: Commands, root_node: Single<Entity, (With<Node>, Without<ChildOf>)>) {
commands.entity(*root_node).despawn();
}
fn setup_many_cameras(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
let image = if 0 < args.image_freq {
Some(asset_server.load("branding/icon.png"))
} else {
None
};
let buttons_f = args.buttons as f32;
let border = if args.no_borders {
UiRect::ZERO
} else {
UiRect::all(Val::VMin(0.05 * 90. / buttons_f))
};
let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
for column in 0..args.buttons {
for row in 0..args.buttons {
let color = as_rainbow(row % column.max(1));
let border_color = Color::WHITE.with_alpha(0.5).into();
let camera = commands
.spawn((
Camera2d,
Camera {
order: (column * args.buttons + row) as isize + 1,
..Default::default()
},
))
.id();
commands
.spawn((
Node {
display: if args.display_none {
Display::None
} else {
Display::Flex
},
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: Val::Percent(100.),
height: Val::Percent(100.),
..default()
},
UiTargetCamera(camera),
))
.with_children(|commands| {
commands
.spawn(Node {
position_type: PositionType::Absolute,
top: Val::Vh(column as f32 * 100. / buttons_f),
left: Val::Vw(row as f32 * 100. / buttons_f),
..Default::default()
})
.with_children(|commands| {
spawn_button(
commands,
color,
buttons_f,
column,
row,
!args.no_text,
border,
border_color,
image
.as_ref()
.filter(|_| (column + row) % args.image_freq == 0)
.cloned(),
);
});
});
}
}
}

View File

@@ -0,0 +1,93 @@
//! Test rendering of many cameras and lights
use std::f32::consts::PI;
use bevy::{
math::ops::{cos, sin},
prelude::*,
render::camera::Viewport,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, rotate_cameras)
.run();
}
const CAMERA_ROWS: usize = 4;
const CAMERA_COLS: usize = 4;
const NUM_LIGHTS: usize = 5;
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
window: Query<&Window>,
) -> Result {
// circular base
commands.spawn((
Mesh3d(meshes.add(Circle::new(4.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
));
// cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// lights
for i in 0..NUM_LIGHTS {
let angle = (i as f32) / (NUM_LIGHTS as f32) * PI * 2.0;
commands.spawn((
PointLight {
color: Color::hsv(angle.to_degrees(), 1.0, 1.0),
intensity: 2_000_000.0 / NUM_LIGHTS as f32,
shadows_enabled: true,
..default()
},
Transform::from_xyz(sin(angle) * 4.0, 2.0, cos(angle) * 4.0),
));
}
// cameras
let window = window.single()?;
let width = window.resolution.width() / CAMERA_COLS as f32 * window.resolution.scale_factor();
let height = window.resolution.height() / CAMERA_ROWS as f32 * window.resolution.scale_factor();
let mut i = 0;
for y in 0..CAMERA_COLS {
for x in 0..CAMERA_ROWS {
let angle = i as f32 / (CAMERA_ROWS * CAMERA_COLS) as f32 * PI * 2.0;
commands.spawn((
Camera3d::default(),
Camera {
viewport: Some(Viewport {
physical_position: UVec2::new(
(x as f32 * width) as u32,
(y as f32 * height) as u32,
),
physical_size: UVec2::new(width as u32, height as u32),
..default()
}),
order: i,
..default()
},
Transform::from_xyz(sin(angle) * 4.0, 2.5, cos(angle) * 4.0)
.looking_at(Vec3::ZERO, Vec3::Y),
));
i += 1;
}
}
Ok(())
}
fn rotate_cameras(time: Res<Time>, mut query: Query<&mut Transform, With<Camera>>) {
for mut transform in query.iter_mut() {
transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(time.delta_secs()));
}
}

View File

@@ -0,0 +1,216 @@
//! Stress test for large ECS worlds.
//!
//! Running this example:
//!
//! ```
//! cargo run --profile stress-test --example many_components [<num_entities>] [<num_components>] [<num_systems>]
//! ```
//!
//! `num_entities`: The number of entities in the world (must be nonnegative)
//! `num_components`: the number of components in the world (must be at least 10)
//! `num_systems`: the number of systems in the world (must be nonnegative)
//!
//! If no valid number is provided, for each argument there's a reasonable default.
use bevy::{
diagnostic::{
DiagnosticPath, DiagnosticsPlugin, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin,
},
ecs::{
component::{ComponentCloneBehavior, ComponentDescriptor, ComponentId, StorageType},
system::QueryParamBuilder,
world::FilteredEntityMut,
},
log::LogPlugin,
prelude::{App, In, IntoSystem, Query, Schedule, SystemParamBuilder, Update},
ptr::{OwningPtr, PtrMut},
MinimalPlugins,
};
use rand::prelude::{Rng, SeedableRng, SliceRandom};
use rand_chacha::ChaCha8Rng;
use std::{alloc::Layout, mem::ManuallyDrop, num::Wrapping};
#[expect(unsafe_code, reason = "Reading dynamic components requires unsafe")]
// A simple system that matches against several components and does some menial calculation to create
// some non-trivial load.
fn base_system(access_components: In<Vec<ComponentId>>, mut query: Query<FilteredEntityMut>) {
#[cfg(feature = "trace")]
let _span = tracing::info_span!("base_system", components = ?access_components.0, count = query.iter().len()).entered();
for mut filtered_entity in &mut query {
// We calculate Faulhaber's formula mod 256 with n = value and p = exponent.
// See https://en.wikipedia.org/wiki/Faulhaber%27s_formula
// The time is takes to compute this depends on the number of entities and the values in
// each entity. This is to ensure that each system takes a different amount of time.
let mut total: Wrapping<u8> = Wrapping(0);
let mut exponent: u32 = 1;
for component_id in &access_components.0 {
// find the value of the component
let ptr = filtered_entity.get_by_id(*component_id).unwrap();
// SAFETY: All components have a u8 layout
let value: u8 = unsafe { *ptr.deref::<u8>() };
for i in 0..=value {
let mut product = Wrapping(1);
for _ in 1..=exponent {
product *= Wrapping(i);
}
total += product;
}
exponent += 1;
}
// we assign this value to all the components we can write to
for component_id in &access_components.0 {
if let Some(ptr) = filtered_entity.get_mut_by_id(*component_id) {
// SAFETY: All components have a u8 layout
unsafe {
let mut value = ptr.with_type::<u8>();
*value = total.0;
}
}
}
}
}
#[expect(unsafe_code, reason = "Using dynamic components requires unsafe")]
fn stress_test(num_entities: u32, num_components: u32, num_systems: u32) {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let mut app = App::default();
let world = app.world_mut();
// register a bunch of components
let component_ids: Vec<ComponentId> = (1..=num_components)
.map(|i| {
world.register_component_with_descriptor(
// SAFETY:
// * We don't implement a drop function
// * u8 is Sync and Send
unsafe {
ComponentDescriptor::new_with_layout(
format!("Component{}", i).to_string(),
StorageType::Table,
Layout::new::<u8>(),
None,
true, // is mutable
ComponentCloneBehavior::Default,
)
},
)
})
.collect();
// fill the schedule with systems
let mut schedule = Schedule::new(Update);
for _ in 1..=num_systems {
let num_access_components = rng.gen_range(1..10);
let access_components: Vec<ComponentId> = component_ids
.choose_multiple(&mut rng, num_access_components)
.copied()
.collect();
let system = (QueryParamBuilder::new(|builder| {
for &access_component in &access_components {
if rand::random::<bool>() {
builder.mut_id(access_component);
} else {
builder.ref_id(access_component);
}
}
}),)
.build_state(world)
.build_any_system(base_system);
schedule.add_systems((move || access_components.clone()).pipe(system));
}
// spawn a bunch of entities
for _ in 1..=num_entities {
let num_components = rng.gen_range(1..10);
let components: Vec<ComponentId> = component_ids
.choose_multiple(&mut rng, num_components)
.copied()
.collect();
let mut entity = world.spawn_empty();
// We use `ManuallyDrop` here as we need to avoid dropping the u8's when `values` is dropped
// since ownership of the values is passed to the world in `insert_by_ids`.
// But we do want to deallocate the memory when values is dropped.
let mut values: Vec<ManuallyDrop<u8>> = components
.iter()
.map(|_id| ManuallyDrop::new(rng.gen_range(0..255)))
.collect();
let ptrs: Vec<OwningPtr> = values
.iter_mut()
.map(|value| {
// SAFETY:
// * We don't read/write `values` binding after this and values are `ManuallyDrop`,
// so we have the right to drop/move the values
unsafe { PtrMut::from(value).promote() }
})
.collect();
// SAFETY:
// * component_id's are from the same world
// * `values` was initialized above, so references are valid
unsafe {
entity.insert_by_ids(&components, ptrs.into_iter());
}
}
println!(
"Number of Archetype-Components: {}",
world.archetypes().archetype_components_len()
);
// overwrite Update schedule in the app
app.add_schedule(schedule);
app.add_plugins(MinimalPlugins)
.add_plugins(DiagnosticsPlugin)
.add_plugins(LogPlugin::default())
.add_plugins(FrameTimeDiagnosticsPlugin::default())
.add_plugins(LogDiagnosticsPlugin::filtered(vec![DiagnosticPath::new(
"fps",
)]));
app.run();
}
fn main() {
const DEFAULT_NUM_ENTITIES: u32 = 50000;
const DEFAULT_NUM_COMPONENTS: u32 = 1000;
const DEFAULT_NUM_SYSTEMS: u32 = 800;
// take input
let num_entities = std::env::args()
.nth(1)
.and_then(|string| string.parse::<u32>().ok())
.unwrap_or_else(|| {
println!(
"No valid number of entities provided, using default {}",
DEFAULT_NUM_ENTITIES
);
DEFAULT_NUM_ENTITIES
});
let num_components = std::env::args()
.nth(2)
.and_then(|string| string.parse::<u32>().ok())
.and_then(|n| if n >= 10 { Some(n) } else { None })
.unwrap_or_else(|| {
println!(
"No valid number of components provided (>= 10), using default {}",
DEFAULT_NUM_COMPONENTS
);
DEFAULT_NUM_COMPONENTS
});
let num_systems = std::env::args()
.nth(3)
.and_then(|string| string.parse::<u32>().ok())
.unwrap_or_else(|| {
println!(
"No valid number of systems provided, using default {}",
DEFAULT_NUM_SYSTEMS
);
DEFAULT_NUM_SYSTEMS
});
stress_test(num_entities, num_components, num_systems);
}

View File

@@ -0,0 +1,499 @@
//! Simple benchmark to test per-entity draw overhead.
//!
//! To measure performance realistically, be sure to run this in release mode.
//! `cargo run --example many_cubes --release`
//!
//! By default, this arranges the meshes in a spherical pattern that
//! distributes the meshes evenly.
//!
//! See `cargo run --example many_cubes --release -- --help` for more options.
use std::{f64::consts::PI, str::FromStr};
use argh::FromArgs;
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
math::{DVec2, DVec3},
pbr::NotShadowCaster,
prelude::*,
render::{
batching::NoAutomaticBatching,
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
view::{NoCpuCulling, NoFrustumCulling, NoIndirectDrawing},
},
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
use rand::{seq::SliceRandom, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
#[derive(FromArgs, Resource)]
/// `many_cubes` stress test
struct Args {
/// how the cube instances should be positioned.
#[argh(option, default = "Layout::Sphere")]
layout: Layout,
/// whether to step the camera animation by a fixed amount such that each frame is the same across runs.
#[argh(switch)]
benchmark: bool,
/// whether to vary the material data in each instance.
#[argh(switch)]
vary_material_data_per_instance: bool,
/// the number of different textures from which to randomly select the material base color. 0 means no textures.
#[argh(option, default = "0")]
material_texture_count: usize,
/// the number of different meshes from which to randomly select. Clamped to at least 1.
#[argh(option, default = "1")]
mesh_count: usize,
/// whether to disable all frustum culling. Stresses queuing and batching as all mesh material entities in the scene are always drawn.
#[argh(switch)]
no_frustum_culling: bool,
/// whether to disable automatic batching. Skips batching resulting in heavy stress on render pass draw command encoding.
#[argh(switch)]
no_automatic_batching: bool,
/// whether to disable indirect drawing.
#[argh(switch)]
no_indirect_drawing: bool,
/// whether to disable CPU culling.
#[argh(switch)]
no_cpu_culling: bool,
/// whether to enable directional light cascaded shadow mapping.
#[argh(switch)]
shadows: bool,
/// animate the cube materials by updating the material from the cpu each frame
#[argh(switch)]
animate_materials: bool,
}
#[derive(Default, Clone)]
enum Layout {
Cube,
#[default]
Sphere,
}
impl FromStr for Layout {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cube" => Ok(Self::Cube),
"sphere" => Ok(Self::Sphere),
_ => Err(format!(
"Unknown layout value: '{s}', valid options: 'cube', 'sphere'"
)),
}
}
}
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();
let mut app = App::new();
app.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0).with_scale_factor_override(1.0),
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.add_systems(Startup, setup)
.add_systems(Update, (move_camera, print_mesh_count));
if args.animate_materials {
app.add_systems(Update, update_materials);
}
app.insert_resource(args).run();
}
const WIDTH: usize = 200;
const HEIGHT: usize = 200;
fn setup(
mut commands: Commands,
args: Res<Args>,
mesh_assets: ResMut<Assets<Mesh>>,
material_assets: ResMut<Assets<StandardMaterial>>,
images: ResMut<Assets<Image>>,
) {
warn!(include_str!("warning_string.txt"));
let args = args.into_inner();
let images = images.into_inner();
let material_assets = material_assets.into_inner();
let mesh_assets = mesh_assets.into_inner();
let meshes = init_meshes(args, mesh_assets);
let material_textures = init_textures(args, images);
let materials = init_materials(args, &material_textures, material_assets);
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
let mut material_rng = ChaCha8Rng::seed_from_u64(42);
match args.layout {
Layout::Sphere => {
// NOTE: This pattern is good for testing performance of culling as it provides roughly
// the same number of visible meshes regardless of the viewing angle.
const N_POINTS: usize = WIDTH * HEIGHT * 4;
// NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution
let radius = WIDTH as f64 * 2.5;
let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
for i in 0..N_POINTS {
let spherical_polar_theta_phi =
fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS);
let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
let (mesh, transform) = meshes.choose(&mut material_rng).unwrap();
commands
.spawn((
Mesh3d(mesh.clone()),
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
Transform::from_translation((radius * unit_sphere_p).as_vec3())
.looking_at(Vec3::ZERO, Vec3::Y)
.mul_transform(*transform),
))
.insert_if(NoFrustumCulling, || args.no_frustum_culling)
.insert_if(NoAutomaticBatching, || args.no_automatic_batching);
}
// camera
let mut camera = commands.spawn(Camera3d::default());
if args.no_indirect_drawing {
camera.insert(NoIndirectDrawing);
}
if args.no_cpu_culling {
camera.insert(NoCpuCulling);
}
// Inside-out box around the meshes onto which shadows are cast (though you cannot see them...)
commands.spawn((
Mesh3d(mesh_assets.add(Cuboid::from_size(Vec3::splat(radius as f32 * 2.2)))),
MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
Transform::from_scale(-Vec3::ONE),
NotShadowCaster,
));
}
_ => {
// NOTE: This pattern is good for demonstrating that frustum culling is working correctly
// as the number of visible meshes rises and falls depending on the viewing angle.
let scale = 2.5;
for x in 0..WIDTH {
for y in 0..HEIGHT {
// introduce spaces to break any kind of moiré pattern
if x % 10 == 0 || y % 10 == 0 {
continue;
}
// cube
commands.spawn((
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
Transform::from_xyz((x as f32) * scale, (y as f32) * scale, 0.0),
));
commands.spawn((
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
Transform::from_xyz(
(x as f32) * scale,
HEIGHT as f32 * scale,
(y as f32) * scale,
),
));
commands.spawn((
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
Transform::from_xyz((x as f32) * scale, 0.0, (y as f32) * scale),
));
commands.spawn((
Mesh3d(meshes.choose(&mut material_rng).unwrap().0.clone()),
MeshMaterial3d(materials.choose(&mut material_rng).unwrap().clone()),
Transform::from_xyz(0.0, (x as f32) * scale, (y as f32) * scale),
));
}
}
// camera
let center = 0.5 * scale * Vec3::new(WIDTH as f32, HEIGHT as f32, WIDTH as f32);
commands.spawn((Camera3d::default(), Transform::from_translation(center)));
// Inside-out box around the meshes onto which shadows are cast (though you cannot see them...)
commands.spawn((
Mesh3d(mesh_assets.add(Cuboid::from_size(2.0 * 1.1 * center))),
MeshMaterial3d(material_assets.add(StandardMaterial::from(Color::WHITE))),
Transform::from_scale(-Vec3::ONE).with_translation(center),
NotShadowCaster,
));
}
}
commands.spawn((
DirectionalLight {
shadows_enabled: args.shadows,
..default()
},
Transform::IDENTITY.looking_at(Vec3::new(0.0, -1.0, -1.0), Vec3::Y),
));
}
fn init_textures(args: &Args, images: &mut Assets<Image>) -> Vec<Handle<Image>> {
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
let color_bytes: Vec<u8> = (0..(args.material_texture_count * 4))
.map(|i| if (i % 4) == 3 { 255 } else { color_rng.r#gen() })
.collect();
color_bytes
.chunks(4)
.map(|pixel| {
images.add(Image::new_fill(
Extent3d {
width: 1,
height: 1,
depth_or_array_layers: 1,
},
TextureDimension::D2,
pixel,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
))
})
.collect()
}
fn init_materials(
args: &Args,
textures: &[Handle<Image>],
assets: &mut Assets<StandardMaterial>,
) -> Vec<Handle<StandardMaterial>> {
let capacity = if args.vary_material_data_per_instance {
match args.layout {
Layout::Cube => (WIDTH - WIDTH / 10) * (HEIGHT - HEIGHT / 10),
Layout::Sphere => WIDTH * HEIGHT * 4,
}
} else {
args.material_texture_count
}
.max(1);
let mut materials = Vec::with_capacity(capacity);
materials.push(assets.add(StandardMaterial {
base_color: Color::WHITE,
base_color_texture: textures.first().cloned(),
..default()
}));
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
let mut color_rng = ChaCha8Rng::seed_from_u64(42);
let mut texture_rng = ChaCha8Rng::seed_from_u64(42);
materials.extend(
std::iter::repeat_with(|| {
assets.add(StandardMaterial {
base_color: Color::srgb_u8(color_rng.r#gen(), color_rng.r#gen(), color_rng.r#gen()),
base_color_texture: textures.choose(&mut texture_rng).cloned(),
..default()
})
})
.take(capacity - materials.len()),
);
materials
}
fn init_meshes(args: &Args, assets: &mut Assets<Mesh>) -> Vec<(Handle<Mesh>, Transform)> {
let capacity = args.mesh_count.max(1);
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
let mut radius_rng = ChaCha8Rng::seed_from_u64(42);
let mut variant = 0;
std::iter::repeat_with(|| {
let radius = radius_rng.gen_range(0.25f32..=0.75f32);
let (handle, transform) = match variant % 15 {
0 => (
assets.add(Cuboid {
half_size: Vec3::splat(radius),
}),
Transform::IDENTITY,
),
1 => (
assets.add(Capsule3d {
radius,
half_length: radius,
}),
Transform::IDENTITY,
),
2 => (
assets.add(Circle { radius }),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
3 => {
let mut vertices = [Vec2::ZERO; 3];
let dtheta = std::f32::consts::TAU / 3.0;
for (i, vertex) in vertices.iter_mut().enumerate() {
let (s, c) = ops::sin_cos(i as f32 * dtheta);
*vertex = Vec2::new(c, s) * radius;
}
(
assets.add(Triangle2d { vertices }),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
)
}
4 => (
assets.add(Rectangle {
half_size: Vec2::splat(radius),
}),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
v if (5..=8).contains(&v) => (
assets.add(RegularPolygon {
circumcircle: Circle { radius },
sides: v,
}),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
9 => (
assets.add(Cylinder {
radius,
half_height: radius,
}),
Transform::IDENTITY,
),
10 => (
assets.add(Ellipse {
half_size: Vec2::new(radius, 0.5 * radius),
}),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
11 => (
assets.add(
Plane3d {
normal: Dir3::NEG_Z,
half_size: Vec2::splat(0.5),
}
.mesh()
.size(radius, radius),
),
Transform::IDENTITY,
),
12 => (assets.add(Sphere { radius }), Transform::IDENTITY),
13 => (
assets.add(Torus {
minor_radius: 0.5 * radius,
major_radius: radius,
}),
Transform::IDENTITY.looking_at(Vec3::Y, Vec3::Y),
),
14 => (
assets.add(Capsule2d {
radius,
half_length: radius,
}),
Transform::IDENTITY.looking_at(Vec3::Z, Vec3::Y),
),
_ => unreachable!(),
};
variant += 1;
(handle, transform)
})
.take(capacity)
.collect()
}
// NOTE: This epsilon value is apparently optimal for optimizing for the average
// nearest-neighbor distance. See:
// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
// for details.
const EPSILON: f64 = 0.36;
fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
DVec2::new(
PI * 2. * (i as f64 / golden_ratio),
f64::acos(1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)),
)
}
fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
let (sin_theta, cos_theta) = p.x.sin_cos();
let (sin_phi, cos_phi) = p.y.sin_cos();
DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
}
// System for rotating the camera
fn move_camera(
time: Res<Time>,
args: Res<Args>,
mut camera_transform: Single<&mut Transform, With<Camera>>,
) {
let delta = 0.15
* if args.benchmark {
1.0 / 60.0
} else {
time.delta_secs()
};
camera_transform.rotate_z(delta);
camera_transform.rotate_x(delta);
}
// System for printing the number of meshes on every tick of the timer
fn print_mesh_count(
time: Res<Time>,
mut timer: Local<PrintingTimer>,
sprites: Query<(&Mesh3d, &ViewVisibility)>,
) {
timer.tick(time.delta());
if timer.just_finished() {
info!(
"Meshes: {} - Visible Meshes {}",
sprites.iter().len(),
sprites.iter().filter(|(_, vis)| vis.get()).count(),
);
}
}
#[derive(Deref, DerefMut)]
struct PrintingTimer(Timer);
impl Default for PrintingTimer {
fn default() -> Self {
Self(Timer::from_seconds(1.0, TimerMode::Repeating))
}
}
fn update_materials(mut materials: ResMut<Assets<StandardMaterial>>, time: Res<Time>) {
let elapsed = time.elapsed_secs();
for (i, (_, material)) in materials.iter_mut().enumerate() {
let hue = (elapsed + i as f32 * 0.005).rem_euclid(1.0);
// This is much faster than using base_color.set_hue(hue), and in a tight loop it shows.
let color = fast_hue_to_rgb(hue);
material.base_color = Color::linear_rgb(color.x, color.y, color.z);
}
}
#[inline]
fn fast_hue_to_rgb(hue: f32) -> Vec3 {
(hue * 6.0 - vec3(3.0, 2.0, 4.0)).abs() * vec3(1.0, -1.0, -1.0) + vec3(-1.0, 2.0, 2.0)
}

View File

@@ -0,0 +1,329 @@
//! Loads animations from a skinned glTF, spawns many of them, and plays the
//! animation to stress test skinned meshes.
use std::{f32::consts::PI, time::Duration};
use argh::FromArgs;
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
pbr::CascadeShadowConfigBuilder,
prelude::*,
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
#[derive(FromArgs, Resource)]
/// `many_foxes` stress test
struct Args {
/// whether all foxes run in sync.
#[argh(switch)]
sync: bool,
/// total number of foxes.
#[argh(option, default = "1000")]
count: usize,
}
#[derive(Resource)]
struct Foxes {
count: usize,
speed: f32,
moving: bool,
sync: bool,
}
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: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".into(),
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.insert_resource(Foxes {
count: args.count,
speed: 2.0,
moving: true,
sync: args.sync,
})
.add_systems(Startup, setup)
.add_systems(
Update,
(
setup_scene_once_loaded,
keyboard_animation_control,
update_fox_rings.after(keyboard_animation_control),
),
)
.run();
}
#[derive(Resource)]
struct Animations {
node_indices: Vec<AnimationNodeIndex>,
graph: Handle<AnimationGraph>,
}
const RING_SPACING: f32 = 2.0;
const FOX_SPACING: f32 = 2.0;
#[derive(Component, Clone, Copy)]
enum RotationDirection {
CounterClockwise,
Clockwise,
}
impl RotationDirection {
fn sign(&self) -> f32 {
match self {
RotationDirection::CounterClockwise => 1.0,
RotationDirection::Clockwise => -1.0,
}
}
}
#[derive(Component)]
struct Ring {
radius: f32,
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
foxes: Res<Foxes>,
) {
warn!(include_str!("warning_string.txt"));
// Insert a resource with the current scene information
let animation_clips = [
asset_server.load(GltfAssetLabel::Animation(2).from_asset("models/animated/Fox.glb")),
asset_server.load(GltfAssetLabel::Animation(1).from_asset("models/animated/Fox.glb")),
asset_server.load(GltfAssetLabel::Animation(0).from_asset("models/animated/Fox.glb")),
];
let mut animation_graph = AnimationGraph::new();
let node_indices = animation_graph
.add_clips(animation_clips.iter().cloned(), 1.0, animation_graph.root)
.collect();
commands.insert_resource(Animations {
node_indices,
graph: animation_graphs.add(animation_graph),
});
// Foxes
// Concentric rings of foxes, running in opposite directions. The rings are spaced at 2m radius intervals.
// The foxes in each ring are spaced at least 2m apart around its circumference.'
// NOTE: This fox model faces +z
let fox_handle =
asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb"));
let ring_directions = [
(
Quat::from_rotation_y(PI),
RotationDirection::CounterClockwise,
),
(Quat::IDENTITY, RotationDirection::Clockwise),
];
let mut ring_index = 0;
let mut radius = RING_SPACING;
let mut foxes_remaining = foxes.count;
info!("Spawning {} foxes...", foxes.count);
while foxes_remaining > 0 {
let (base_rotation, ring_direction) = ring_directions[ring_index % 2];
let ring_parent = commands
.spawn((
Transform::default(),
Visibility::default(),
ring_direction,
Ring { radius },
))
.id();
let circumference = PI * 2. * radius;
let foxes_in_ring = ((circumference / FOX_SPACING) as usize).min(foxes_remaining);
let fox_spacing_angle = circumference / (foxes_in_ring as f32 * radius);
for fox_i in 0..foxes_in_ring {
let fox_angle = fox_i as f32 * fox_spacing_angle;
let (s, c) = ops::sin_cos(fox_angle);
let (x, z) = (radius * c, radius * s);
commands.entity(ring_parent).with_children(|builder| {
builder.spawn((
SceneRoot(fox_handle.clone()),
Transform::from_xyz(x, 0.0, z)
.with_scale(Vec3::splat(0.01))
.with_rotation(base_rotation * Quat::from_rotation_y(-fox_angle)),
));
});
}
foxes_remaining -= foxes_in_ring;
radius += RING_SPACING;
ring_index += 1;
}
// Camera
let zoom = 0.8;
let translation = Vec3::new(
radius * 1.25 * zoom,
radius * 0.5 * zoom,
radius * 1.5 * zoom,
);
commands.spawn((
Camera3d::default(),
Transform::from_translation(translation)
.looking_at(0.2 * Vec3::new(translation.x, 0.0, translation.z), Vec3::Y),
));
// Plane
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(5000.0, 5000.0))),
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
));
// Light
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
shadows_enabled: true,
..default()
},
CascadeShadowConfigBuilder {
first_cascade_far_bound: 0.9 * radius,
maximum_distance: 2.8 * radius,
..default()
}
.build(),
));
println!("Animation controls:");
println!(" - spacebar: play / pause");
println!(" - arrow up / down: speed up / slow down animation playback");
println!(" - arrow left / right: seek backward / forward");
println!(" - return: change animation");
}
// Once the scene is loaded, start the animation
fn setup_scene_once_loaded(
animations: Res<Animations>,
foxes: Res<Foxes>,
mut commands: Commands,
mut player: Query<(Entity, &mut AnimationPlayer)>,
mut done: Local<bool>,
) {
if !*done && player.iter().len() == foxes.count {
for (entity, mut player) in &mut player {
commands
.entity(entity)
.insert(AnimationGraphHandle(animations.graph.clone()))
.insert(AnimationTransitions::new());
let playing_animation = player.play(animations.node_indices[0]).repeat();
if !foxes.sync {
playing_animation.seek_to(entity.index() as f32 / 10.0);
}
}
*done = true;
}
}
fn update_fox_rings(
time: Res<Time>,
foxes: Res<Foxes>,
mut rings: Query<(&Ring, &RotationDirection, &mut Transform)>,
) {
if !foxes.moving {
return;
}
let dt = time.delta_secs();
for (ring, rotation_direction, mut transform) in &mut rings {
let angular_velocity = foxes.speed / ring.radius;
transform.rotate_y(rotation_direction.sign() * angular_velocity * dt);
}
}
fn keyboard_animation_control(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut animation_player: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>,
animations: Res<Animations>,
mut current_animation: Local<usize>,
mut foxes: ResMut<Foxes>,
) {
if keyboard_input.just_pressed(KeyCode::Space) {
foxes.moving = !foxes.moving;
}
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
foxes.speed *= 1.25;
}
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
foxes.speed *= 0.8;
}
if keyboard_input.just_pressed(KeyCode::Enter) {
*current_animation = (*current_animation + 1) % animations.node_indices.len();
}
for (mut player, mut transitions) in &mut animation_player {
if keyboard_input.just_pressed(KeyCode::Space) {
if player.all_paused() {
player.resume_all();
} else {
player.pause_all();
}
}
if keyboard_input.just_pressed(KeyCode::ArrowUp) {
player.adjust_speeds(1.25);
}
if keyboard_input.just_pressed(KeyCode::ArrowDown) {
player.adjust_speeds(0.8);
}
if keyboard_input.just_pressed(KeyCode::ArrowLeft) {
player.seek_all_by(-0.1);
}
if keyboard_input.just_pressed(KeyCode::ArrowRight) {
player.seek_all_by(0.1);
}
if keyboard_input.just_pressed(KeyCode::Enter) {
transitions
.play(
&mut player,
animations.node_indices[*current_animation],
Duration::from_millis(250),
)
.repeat();
}
}
}

View File

@@ -0,0 +1,118 @@
//! Test rendering of many gizmos.
use std::f32::consts::TAU;
use bevy::{
diagnostic::{Diagnostic, DiagnosticsStore, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
const SYSTEM_COUNT: u32 = 10;
fn main() {
let mut app = App::new();
app.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Many Debug Lines".to_string(),
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0).with_scale_factor_override(1.0),
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.insert_resource(Config {
line_count: 50_000,
fancy: false,
})
.add_systems(Startup, setup)
.add_systems(Update, (input, ui_system));
for _ in 0..SYSTEM_COUNT {
app.add_systems(Update, system);
}
app.run();
}
#[derive(Resource, Debug)]
struct Config {
line_count: u32,
fancy: bool,
}
fn input(mut config: ResMut<Config>, input: Res<ButtonInput<KeyCode>>) {
if input.just_pressed(KeyCode::ArrowUp) {
config.line_count += 10_000;
}
if input.just_pressed(KeyCode::ArrowDown) {
config.line_count = config.line_count.saturating_sub(10_000);
}
if input.just_pressed(KeyCode::Space) {
config.fancy = !config.fancy;
}
}
fn system(config: Res<Config>, time: Res<Time>, mut draw: Gizmos) {
if !config.fancy {
for _ in 0..(config.line_count / SYSTEM_COUNT) {
draw.line(Vec3::NEG_Y, Vec3::Y, Color::BLACK);
}
} else {
for i in 0..(config.line_count / SYSTEM_COUNT) {
let angle = i as f32 / (config.line_count / SYSTEM_COUNT) as f32 * TAU;
let vector = Vec2::from(ops::sin_cos(angle)).extend(ops::sin(time.elapsed_secs()));
let start_color = LinearRgba::rgb(vector.x, vector.z, 0.5);
let end_color = LinearRgba::rgb(-vector.z, -vector.y, 0.5);
draw.line_gradient(vector, -vector, start_color, end_color);
}
}
}
fn setup(mut commands: Commands) {
warn!(include_str!("warning_string.txt"));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(3., 1., 5.).looking_at(Vec3::ZERO, Vec3::Y),
));
commands.spawn((
Text::default(),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
}
fn ui_system(mut text: Single<&mut Text>, config: Res<Config>, diag: Res<DiagnosticsStore>) {
let Some(fps) = diag
.get(&FrameTimeDiagnosticsPlugin::FPS)
.and_then(Diagnostic::smoothed)
else {
return;
};
text.0 = format!(
"Line count: {}\n\
FPS: {:.0}\n\n\
Controls:\n\
Up/Down: Raise or lower the line count.\n\
Spacebar: Toggle fancy mode.",
config.line_count, fps,
);
}

View File

@@ -0,0 +1,115 @@
//! Simple text rendering benchmark.
//!
//! Creates a text block with a single span containing `100_000` glyphs,
//! and renders it with the UI in a white color and with Text2d in a red color.
//!
//! To recompute all text each frame run
//! `cargo run --example many_glyphs --release recompute-text`
use argh::FromArgs;
use bevy::{
color::palettes::basic::RED,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
text::{LineBreak, TextBounds},
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
#[derive(FromArgs, Resource)]
/// `many_glyphs` stress test
struct Args {
/// don't draw the UI text.
#[argh(switch)]
no_ui: bool,
/// don't draw the Text2d text.
#[argh(switch)]
no_text2d: bool,
/// whether to force the text to recompute every frame by triggering change detection.
#[argh(switch)]
recompute_text: bool,
}
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();
let mut app = App::new();
app.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0).with_scale_factor_override(1.0),
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.add_systems(Startup, setup);
if args.recompute_text {
app.add_systems(Update, force_text_recomputation);
}
app.insert_resource(args).run();
}
fn setup(mut commands: Commands, args: Res<Args>) {
warn!(include_str!("warning_string.txt"));
commands.spawn(Camera2d);
let text_string = "0123456789".repeat(10_000);
let text_font = TextFont {
font_size: 4.,
..Default::default()
};
let text_block = TextLayout {
justify: JustifyText::Left,
linebreak: LineBreak::AnyCharacter,
};
if !args.no_ui {
commands
.spawn(Node {
width: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
})
.with_children(|commands| {
commands
.spawn(Node {
width: Val::Px(1000.),
..Default::default()
})
.with_child((Text(text_string.clone()), text_font.clone(), text_block));
});
}
if !args.no_text2d {
commands.spawn((
Text2d::new(text_string),
text_font.clone(),
TextColor(RED.into()),
bevy::sprite::Anchor::Center,
TextBounds::new_horizontal(1000.),
text_block,
));
}
}
fn force_text_recomputation(mut text_query: Query<&mut TextLayout>) {
for mut block in &mut text_query {
block.set_changed();
}
}

View File

@@ -0,0 +1,189 @@
//! Simple benchmark to test rendering many point lights.
//! Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights.
use std::f64::consts::PI;
use bevy::{
color::palettes::css::DEEP_PINK,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
math::{DVec2, DVec3},
pbr::{ExtractedPointLight, GlobalClusterableObjectMeta},
prelude::*,
render::{camera::ScalingMode, Render, RenderApp, RenderSet},
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
use rand::{thread_rng, Rng};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
title: "many_lights".into(),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
LogVisibleLights,
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.add_systems(Startup, setup)
.add_systems(Update, (move_camera, print_light_count))
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
warn!(include_str!("warning_string.txt"));
const LIGHT_RADIUS: f32 = 0.3;
const LIGHT_INTENSITY: f32 = 1000.0;
const RADIUS: f32 = 50.0;
const N_LIGHTS: usize = 100_000;
commands.spawn((
Mesh3d(meshes.add(Sphere::new(RADIUS).mesh().ico(9).unwrap())),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_scale(Vec3::NEG_ONE),
));
let mesh = meshes.add(Cuboid::default());
let material = materials.add(StandardMaterial {
base_color: DEEP_PINK.into(),
..default()
});
// NOTE: This pattern is good for testing performance of culling as it provides roughly
// the same number of visible meshes regardless of the viewing angle.
// NOTE: f64 is used to avoid precision issues that produce visual artifacts in the distribution
let golden_ratio = 0.5f64 * (1.0f64 + 5.0f64.sqrt());
// Spawn N_LIGHTS many lights
commands.spawn_batch((0..N_LIGHTS).map(move |i| {
let mut rng = thread_rng();
let spherical_polar_theta_phi = fibonacci_spiral_on_sphere(golden_ratio, i, N_LIGHTS);
let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi);
(
PointLight {
range: LIGHT_RADIUS,
intensity: LIGHT_INTENSITY,
color: Color::hsl(rng.gen_range(0.0..360.0), 1.0, 0.5),
..default()
},
Transform::from_translation((RADIUS as f64 * unit_sphere_p).as_vec3()),
)
}));
// camera
match std::env::args().nth(1).as_deref() {
Some("orthographic") => commands.spawn((
Camera3d::default(),
Projection::from(OrthographicProjection {
scaling_mode: ScalingMode::FixedHorizontal {
viewport_width: 20.0,
},
..OrthographicProjection::default_3d()
}),
)),
_ => commands.spawn(Camera3d::default()),
};
// add one cube, the only one with strong handles
// also serves as a reference point during rotation
commands.spawn((
Mesh3d(mesh),
MeshMaterial3d(material),
Transform {
translation: Vec3::new(0.0, RADIUS, 0.0),
scale: Vec3::splat(5.0),
..default()
},
));
}
// NOTE: This epsilon value is apparently optimal for optimizing for the average
// nearest-neighbor distance. See:
// http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/
// for details.
const EPSILON: f64 = 0.36;
fn fibonacci_spiral_on_sphere(golden_ratio: f64, i: usize, n: usize) -> DVec2 {
DVec2::new(
PI * 2. * (i as f64 / golden_ratio),
ops::acos((1.0 - 2.0 * (i as f64 + EPSILON) / (n as f64 - 1.0 + 2.0 * EPSILON)) as f32)
as f64,
)
}
fn spherical_polar_to_cartesian(p: DVec2) -> DVec3 {
let (sin_theta, cos_theta) = p.x.sin_cos();
let (sin_phi, cos_phi) = p.y.sin_cos();
DVec3::new(cos_theta * sin_phi, sin_theta * sin_phi, cos_phi)
}
// System for rotating the camera
fn move_camera(time: Res<Time>, mut camera_transform: Single<&mut Transform, With<Camera>>) {
let delta = time.delta_secs() * 0.15;
camera_transform.rotate_z(delta);
camera_transform.rotate_x(delta);
}
// System for printing the number of meshes on every tick of the timer
fn print_light_count(time: Res<Time>, mut timer: Local<PrintingTimer>, lights: Query<&PointLight>) {
timer.0.tick(time.delta());
if timer.0.just_finished() {
info!("Lights: {}", lights.iter().len());
}
}
struct LogVisibleLights;
impl Plugin for LogVisibleLights {
fn build(&self, app: &mut App) {
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return;
};
render_app.add_systems(Render, print_visible_light_count.in_set(RenderSet::Prepare));
}
}
// System for printing the number of meshes on every tick of the timer
fn print_visible_light_count(
time: Res<Time>,
mut timer: Local<PrintingTimer>,
visible: Query<&ExtractedPointLight>,
global_light_meta: Res<GlobalClusterableObjectMeta>,
) {
timer.0.tick(time.delta());
if timer.0.just_finished() {
info!(
"Visible Lights: {}, Rendered Lights: {}",
visible.iter().len(),
global_light_meta.entity_to_index.len()
);
}
}
struct PrintingTimer(Timer);
impl Default for PrintingTimer {
fn default() -> Self {
Self(Timer::from_seconds(1.0, TimerMode::Repeating))
}
}

View File

@@ -0,0 +1,103 @@
//! Benchmark to test rendering many animated materials
use argh::FromArgs;
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
window::{PresentMode, WindowPlugin, WindowResolution},
};
use std::f32::consts::PI;
#[derive(FromArgs, Resource)]
/// Command-line arguments for the `many_materials` stress test.
struct Args {
/// the size of the grid of materials to render (n x n)
#[argh(option, short = 'n', default = "10")]
grid_size: usize,
}
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 {
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
title: "many_materials".into(),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(args)
.add_systems(Startup, setup)
.add_systems(Update, animate_materials)
.run();
}
fn setup(
mut commands: Commands,
args: Res<Args>,
mesh_assets: ResMut<Assets<Mesh>>,
material_assets: ResMut<Assets<StandardMaterial>>,
) {
let args = args.into_inner();
let material_assets = material_assets.into_inner();
let mesh_assets = mesh_assets.into_inner();
let n = args.grid_size;
// Camera
let w = n as f32;
commands.spawn((
Camera3d::default(),
Transform::from_xyz(w * 1.25, w + 1.0, w * 1.25)
.looking_at(Vec3::new(0.0, (w * -1.1) + 1.0, 0.0), Vec3::Y),
));
// Light
commands.spawn((
Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)),
DirectionalLight {
illuminance: 3000.0,
shadows_enabled: true,
..default()
},
));
// Cubes
let mesh_handle = mesh_assets.add(Cuboid::from_size(Vec3::ONE));
for x in 0..n {
for z in 0..n {
commands.spawn((
Mesh3d(mesh_handle.clone()),
MeshMaterial3d(material_assets.add(Color::WHITE)),
Transform::from_translation(Vec3::new(x as f32, 0.0, z as f32)),
));
}
}
}
fn animate_materials(
material_handles: Query<&MeshMaterial3d<StandardMaterial>>,
time: Res<Time>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
for (i, material_handle) in material_handles.iter().enumerate() {
if let Some(material) = materials.get_mut(material_handle) {
let color = Color::hsl(
((i as f32 * 2.345 + time.elapsed_secs()) * 100.0) % 360.0,
1.0,
0.5,
);
material.base_color = color;
}
}
}

View File

@@ -0,0 +1,129 @@
//! Renders a lot of sprites to allow performance testing.
//! See <https://github.com/bevyengine/bevy/pull/1492>
//!
//! This example sets up many sprites in different sizes, rotations, and scales in the world.
//! It also moves the camera over them to see how well frustum culling works.
//!
//! Add the `--colored` arg to run with color tinted sprites. This will cause the sprites to be rendered
//! in multiple batches, reducing performance but useful for testing.
use bevy::{
color::palettes::css::*,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
use rand::Rng;
const CAMERA_SPEED: f32 = 1000.0;
const COLORS: [Color; 3] = [Color::Srgba(BLUE), Color::Srgba(WHITE), Color::Srgba(RED)];
#[derive(Resource)]
struct ColorTint(bool);
fn main() {
App::new()
.insert_resource(ColorTint(
std::env::args().nth(1).unwrap_or_default() == "--colored",
))
// Since this is also used as a benchmark, we want it to display performance data.
.add_plugins((
LogDiagnosticsPlugin::default(),
FrameTimeDiagnosticsPlugin::default(),
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
..default()
}),
..default()
}),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.add_systems(Startup, setup)
.add_systems(
Update,
(print_sprite_count, move_camera.after(print_sprite_count)),
)
.run();
}
fn setup(mut commands: Commands, assets: Res<AssetServer>, color_tint: Res<ColorTint>) {
warn!(include_str!("warning_string.txt"));
let mut rng = rand::thread_rng();
let tile_size = Vec2::splat(64.0);
let map_size = Vec2::splat(320.0);
let half_x = (map_size.x / 2.0) as i32;
let half_y = (map_size.y / 2.0) as i32;
let sprite_handle = assets.load("branding/icon.png");
// Spawns the camera
commands.spawn(Camera2d);
// Builds and spawns the sprites
let mut sprites = vec![];
for y in -half_y..half_y {
for x in -half_x..half_x {
let position = Vec2::new(x as f32, y as f32);
let translation = (position * tile_size).extend(rng.r#gen::<f32>());
let rotation = Quat::from_rotation_z(rng.r#gen::<f32>());
let scale = Vec3::splat(rng.r#gen::<f32>() * 2.0);
sprites.push((
Sprite {
image: sprite_handle.clone(),
custom_size: Some(tile_size),
color: if color_tint.0 {
COLORS[rng.gen_range(0..3)]
} else {
Color::WHITE
},
..default()
},
Transform {
translation,
rotation,
scale,
},
));
}
}
commands.spawn_batch(sprites);
}
// System for rotating and translating the camera
fn move_camera(time: Res<Time>, mut camera_transform: Single<&mut Transform, With<Camera>>) {
camera_transform.rotate_z(time.delta_secs() * 0.5);
**camera_transform = **camera_transform
* Transform::from_translation(Vec3::X * CAMERA_SPEED * time.delta_secs());
}
#[derive(Deref, DerefMut)]
struct PrintingTimer(Timer);
impl Default for PrintingTimer {
fn default() -> Self {
Self(Timer::from_seconds(1.0, TimerMode::Repeating))
}
}
// System for printing the number of sprites on every tick of the timer
fn print_sprite_count(time: Res<Time>, mut timer: Local<PrintingTimer>, sprites: Query<&Sprite>) {
timer.tick(time.delta());
if timer.just_finished() {
info!("Sprites: {}", sprites.iter().count());
}
}

View File

@@ -0,0 +1,227 @@
//! Renders a lot of `Text2d`s
use std::ops::RangeInclusive;
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
render::view::NoFrustumCulling,
text::FontAtlasSets,
window::{PresentMode, WindowResolution},
};
use argh::FromArgs;
use rand::{
seq::{IteratorRandom, SliceRandom},
Rng, SeedableRng,
};
use rand_chacha::ChaCha8Rng;
const CAMERA_SPEED: f32 = 1000.0;
// Some code points for valid glyphs in `FiraSans-Bold.ttf`
const CODE_POINT_RANGES: [RangeInclusive<u32>; 5] = [
0x20..=0x7e,
0xa0..=0x17e,
0x180..=0x2b2,
0x3f0..=0x479,
0x48a..=0x52f,
];
#[derive(FromArgs, Resource)]
/// `many_text2d` stress test
struct Args {
/// whether to use many different glyphs to increase the amount of font atlas textures used.
#[argh(switch)]
many_glyphs: bool,
/// whether to use many different font sizes to increase the amount of font atlas textures used.
#[argh(switch)]
many_font_sizes: bool,
/// whether to force the text to recompute every frame by triggering change detection.
#[argh(switch)]
recompute: bool,
/// whether to disable all frustum culling.
#[argh(switch)]
no_frustum_culling: bool,
/// whether the text should use `JustifyText::Center`.
#[argh(switch)]
center: bool,
}
#[derive(Resource)]
struct FontHandle(Handle<Font>);
impl FromWorld for FontHandle {
fn from_world(world: &mut World) -> Self {
Self(world.load_asset("fonts/FiraSans-Bold.ttf"))
}
}
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();
let mut app = App::new();
app.add_plugins((
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0).with_scale_factor_override(1.0),
..default()
}),
..default()
}),
))
.init_resource::<FontHandle>()
.add_systems(Startup, setup)
.add_systems(Update, (move_camera, print_counts));
if args.recompute {
app.add_systems(Update, recompute);
}
app.insert_resource(args).run();
}
#[derive(Deref, DerefMut)]
struct PrintingTimer(Timer);
impl Default for PrintingTimer {
fn default() -> Self {
Self(Timer::from_seconds(1.0, TimerMode::Repeating))
}
}
fn setup(mut commands: Commands, font: Res<FontHandle>, args: Res<Args>) {
warn!(include_str!("warning_string.txt"));
let mut rng = ChaCha8Rng::seed_from_u64(42);
let tile_size = Vec2::splat(64.0);
let map_size = Vec2::splat(640.0);
let half_x = (map_size.x / 4.0) as i32;
let half_y = (map_size.y / 4.0) as i32;
// Spawns the camera
commands.spawn(Camera2d);
// Builds and spawns the `Text2d`s, distributing them in a way that ensures a
// good distribution of on-screen and off-screen entities.
let mut text2ds = vec![];
for y in -half_y..half_y {
for x in -half_x..half_x {
let position = Vec2::new(x as f32, y as f32);
let translation = (position * tile_size).extend(rng.r#gen::<f32>());
let rotation = Quat::from_rotation_z(rng.r#gen::<f32>());
let scale = Vec3::splat(rng.r#gen::<f32>() * 2.0);
let color = Hsla::hsl(rng.gen_range(0.0..360.0), 0.8, 0.8);
text2ds.push((
Text2d(random_text(&mut rng, &args)),
random_text_font(&mut rng, &args, font.0.clone()),
TextColor(color.into()),
TextLayout::new_with_justify(if args.center {
JustifyText::Center
} else {
JustifyText::Left
}),
Transform {
translation,
rotation,
scale,
},
));
}
}
if args.no_frustum_culling {
let bundles = text2ds.into_iter().map(|bundle| (bundle, NoFrustumCulling));
commands.spawn_batch(bundles);
} else {
commands.spawn_batch(text2ds);
}
}
// System for rotating and translating the camera
fn move_camera(time: Res<Time>, mut camera_query: Query<&mut Transform, With<Camera>>) {
let Ok(mut camera_transform) = camera_query.single_mut() else {
return;
};
camera_transform.rotate_z(time.delta_secs() * 0.5);
*camera_transform =
*camera_transform * Transform::from_translation(Vec3::X * CAMERA_SPEED * time.delta_secs());
}
// System for printing the number of texts on every tick of the timer
fn print_counts(
time: Res<Time>,
mut timer: Local<PrintingTimer>,
texts: Query<&ViewVisibility, With<Text2d>>,
atlases: Res<FontAtlasSets>,
font: Res<FontHandle>,
) {
timer.tick(time.delta());
if !timer.just_finished() {
return;
}
let num_atlases = atlases
.get(font.0.id())
.map(|set| set.iter().map(|atlas| atlas.1.len()).sum())
.unwrap_or(0);
let visible_texts = texts.iter().filter(|visibility| visibility.get()).count();
info!(
"Texts: {} Visible: {} Atlases: {}",
texts.iter().count(),
visible_texts,
num_atlases
);
}
fn random_text_font(rng: &mut ChaCha8Rng, args: &Args, font: Handle<Font>) -> TextFont {
let font_size = if args.many_font_sizes {
*[10.0, 20.0, 30.0, 40.0, 50.0, 60.0].choose(rng).unwrap()
} else {
60.0
};
TextFont {
font_size,
font,
..default()
}
}
fn random_text(rng: &mut ChaCha8Rng, args: &Args) -> String {
if !args.many_glyphs {
return "Bevy".to_string();
}
CODE_POINT_RANGES
.choose(rng)
.unwrap()
.clone()
.choose_multiple(rng, 4)
.into_iter()
.map(|cp| char::from_u32(cp).unwrap())
.collect::<String>()
}
fn recompute(mut query: Query<&mut Text2d>) {
for mut text2d in &mut query {
text2d.set_changed();
}
}

View File

@@ -0,0 +1,90 @@
//! Text pipeline benchmark.
//!
//! Continuously recomputes a large block of text with 100 text spans.
use bevy::{
color::palettes::basic::{BLUE, YELLOW},
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
text::{LineBreak, TextBounds},
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
};
fn main() {
App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
..default()
}),
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::Continuous,
})
.add_systems(Startup, spawn)
.add_systems(Update, update_text_bounds)
.run();
}
fn spawn(mut commands: Commands, asset_server: Res<AssetServer>) {
warn!(include_str!("warning_string.txt"));
commands.spawn(Camera2d);
let make_spans = |i| {
[
(
TextSpan("text".repeat(i)),
TextFont {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: (4 + i % 10) as f32,
..Default::default()
},
TextColor(BLUE.into()),
),
(
TextSpan("pipeline".repeat(i)),
TextFont {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: (4 + i % 11) as f32,
..default()
},
TextColor(YELLOW.into()),
),
]
};
let spans = (1..50).flat_map(|i| make_spans(i).into_iter());
commands
.spawn((
Text2d::default(),
TextLayout {
justify: JustifyText::Center,
linebreak: LineBreak::AnyCharacter,
},
TextBounds::default(),
))
.with_children(|p| {
for span in spans {
p.spawn(span);
}
});
}
// changing the bounds of the text will cause a recomputation
fn update_text_bounds(time: Res<Time>, mut text_bounds_query: Query<&mut TextBounds>) {
let width = (1. + ops::sin(time.elapsed_secs())) * 600.0;
for mut text_bounds in text_bounds_query.iter_mut() {
text_bounds.width = Some(width);
}
}

View File

@@ -0,0 +1,550 @@
//! Hierarchy and transform propagation stress test.
//!
//! Running this example:
//!
//! ```
//! cargo r --release --example transform_hierarchy <configuration name>
//! ```
//!
//! | Configuration | Description |
//! | -------------------- | ----------------------------------------------------------------- |
//! | `large_tree` | A fairly wide and deep tree. |
//! | `wide_tree` | A shallow but very wide tree. |
//! | `deep_tree` | A deep but not very wide tree. |
//! | `chain` | A chain. 2500 levels deep. |
//! | `update_leaves` | Same as `large_tree`, but only leaves are updated. |
//! | `update_shallow` | Same as `large_tree`, but only the first few levels are updated. |
//! | `humanoids_active` | 4000 active humanoid rigs. |
//! | `humanoids_inactive` | 4000 humanoid rigs. Only 10 are active. |
//! | `humanoids_mixed` | 2000 active and 2000 inactive humanoid rigs. |
use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
window::ExitCondition,
};
use rand::Rng;
/// pre-defined test configurations with name
const CONFIGS: [(&str, Cfg); 9] = [
(
"large_tree",
Cfg {
test_case: TestCase::NonUniformTree {
depth: 18,
branch_width: 8,
},
update_filter: UpdateFilter {
probability: 0.5,
min_depth: 0,
max_depth: u32::MAX,
},
},
),
(
"wide_tree",
Cfg {
test_case: TestCase::Tree {
depth: 3,
branch_width: 500,
},
update_filter: UpdateFilter {
probability: 0.5,
min_depth: 0,
max_depth: u32::MAX,
},
},
),
(
"deep_tree",
Cfg {
test_case: TestCase::NonUniformTree {
depth: 25,
branch_width: 2,
},
update_filter: UpdateFilter {
probability: 0.5,
min_depth: 0,
max_depth: u32::MAX,
},
},
),
(
"chain",
Cfg {
test_case: TestCase::Tree {
depth: 2500,
branch_width: 1,
},
update_filter: UpdateFilter {
probability: 0.5,
min_depth: 0,
max_depth: u32::MAX,
},
},
),
(
"update_leaves",
Cfg {
test_case: TestCase::Tree {
depth: 18,
branch_width: 2,
},
update_filter: UpdateFilter {
probability: 0.5,
min_depth: 17,
max_depth: u32::MAX,
},
},
),
(
"update_shallow",
Cfg {
test_case: TestCase::Tree {
depth: 18,
branch_width: 2,
},
update_filter: UpdateFilter {
probability: 0.5,
min_depth: 0,
max_depth: 8,
},
},
),
(
"humanoids_active",
Cfg {
test_case: TestCase::Humanoids {
active: 4000,
inactive: 0,
},
update_filter: UpdateFilter {
probability: 1.0,
min_depth: 0,
max_depth: u32::MAX,
},
},
),
(
"humanoids_inactive",
Cfg {
test_case: TestCase::Humanoids {
active: 10,
inactive: 3990,
},
update_filter: UpdateFilter {
probability: 1.0,
min_depth: 0,
max_depth: u32::MAX,
},
},
),
(
"humanoids_mixed",
Cfg {
test_case: TestCase::Humanoids {
active: 2000,
inactive: 2000,
},
update_filter: UpdateFilter {
probability: 1.0,
min_depth: 0,
max_depth: u32::MAX,
},
},
),
];
fn print_available_configs() {
println!("available configurations:");
for (name, _) in CONFIGS {
println!(" {name}");
}
}
fn main() {
// parse cli argument and find the selected test configuration
let cfg: Cfg = match std::env::args().nth(1) {
Some(arg) => match CONFIGS.iter().find(|(name, _)| *name == arg) {
Some((name, cfg)) => {
println!("test configuration: {name}");
cfg.clone()
}
None => {
println!("test configuration \"{arg}\" not found.\n");
print_available_configs();
return;
}
},
None => {
println!("missing argument: <test configuration>\n");
print_available_configs();
return;
}
};
println!("\n{cfg:#?}");
App::new()
.insert_resource(cfg)
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: None,
exit_condition: ExitCondition::DontExit,
..default()
}),
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.add_systems(Startup, setup)
// Updating transforms *must* be done before `PostUpdate`
// or the hierarchy will momentarily be in an invalid state.
.add_systems(Update, update)
.run();
}
/// test configuration
#[derive(Resource, Debug, Clone)]
struct Cfg {
/// which test case should be inserted
test_case: TestCase,
/// which entities should be updated
update_filter: UpdateFilter,
}
#[derive(Debug, Clone)]
enum TestCase {
/// a uniform tree, exponentially growing with depth
Tree {
/// total depth
depth: u32,
/// number of children per node
branch_width: u32,
},
/// a non uniform tree (one side is deeper than the other)
/// creates significantly less nodes than `TestCase::Tree` with the same parameters
NonUniformTree {
/// the maximum depth
depth: u32,
/// max number of children per node
branch_width: u32,
},
/// one or multiple humanoid rigs
Humanoids {
/// number of active instances (uses the specified [`UpdateFilter`])
active: u32,
/// number of inactive instances (always inactive)
inactive: u32,
},
}
/// a filter to restrict which nodes are updated
#[derive(Debug, Clone)]
struct UpdateFilter {
/// starting depth (inclusive)
min_depth: u32,
/// end depth (inclusive)
max_depth: u32,
/// probability of a node to get updated (evaluated at insertion time, not during update)
/// 0 (never) .. 1 (always)
probability: f32,
}
/// update component with some per-component value
#[derive(Component)]
struct UpdateValue(f32);
/// update positions system
fn update(time: Res<Time>, mut query: Query<(&mut Transform, &mut UpdateValue)>) {
for (mut t, mut u) in &mut query {
u.0 += time.delta_secs() * 0.1;
set_translation(&mut t.translation, u.0);
}
}
/// set translation based on the angle `a`
fn set_translation(translation: &mut Vec3, a: f32) {
translation.x = ops::cos(a) * 32.0;
translation.y = ops::sin(a) * 32.0;
}
fn setup(mut commands: Commands, cfg: Res<Cfg>) {
warn!(include_str!("warning_string.txt"));
commands.spawn((Camera2d, Transform::from_xyz(0.0, 0.0, 100.0)));
let result = match cfg.test_case {
TestCase::Tree {
depth,
branch_width,
} => {
let tree = gen_tree(depth, branch_width);
spawn_tree(&tree, &mut commands, &cfg.update_filter, default())
}
TestCase::NonUniformTree {
depth,
branch_width,
} => {
let tree = gen_non_uniform_tree(depth, branch_width);
spawn_tree(&tree, &mut commands, &cfg.update_filter, default())
}
TestCase::Humanoids { active, inactive } => {
let mut result = InsertResult::default();
let mut rng = rand::thread_rng();
for _ in 0..active {
result.combine(spawn_tree(
&HUMANOID_RIG,
&mut commands,
&cfg.update_filter,
Transform::from_xyz(
rng.r#gen::<f32>() * 500.0 - 250.0,
rng.r#gen::<f32>() * 500.0 - 250.0,
0.0,
),
));
}
for _ in 0..inactive {
result.combine(spawn_tree(
&HUMANOID_RIG,
&mut commands,
&UpdateFilter {
// force inactive by setting the probability < 0
probability: -1.0,
..cfg.update_filter
},
Transform::from_xyz(
rng.r#gen::<f32>() * 500.0 - 250.0,
rng.r#gen::<f32>() * 500.0 - 250.0,
0.0,
),
));
}
result
}
};
println!("\n{result:#?}");
}
/// overview of the inserted hierarchy
#[derive(Default, Debug)]
struct InsertResult {
/// total number of nodes inserted
inserted_nodes: usize,
/// number of nodes that get updated each frame
active_nodes: usize,
/// maximum depth of the hierarchy tree
maximum_depth: usize,
}
impl InsertResult {
fn combine(&mut self, rhs: Self) -> &mut Self {
self.inserted_nodes += rhs.inserted_nodes;
self.active_nodes += rhs.active_nodes;
self.maximum_depth = self.maximum_depth.max(rhs.maximum_depth);
self
}
}
/// spawns a tree defined by a parent map (excluding root)
/// the parent map must be ordered (parent must exist before child)
fn spawn_tree(
parent_map: &[usize],
commands: &mut Commands,
update_filter: &UpdateFilter,
root_transform: Transform,
) -> InsertResult {
// total count (# of nodes + root)
let count = parent_map.len() + 1;
#[derive(Default, Clone, Copy)]
struct NodeInfo {
child_count: u32,
depth: u32,
}
// node index -> entity lookup list
let mut ents: Vec<Entity> = Vec::with_capacity(count);
let mut node_info: Vec<NodeInfo> = vec![default(); count];
for (i, &parent_idx) in parent_map.iter().enumerate() {
// assert spawn order (parent must be processed before child)
assert!(parent_idx <= i, "invalid spawn order");
node_info[parent_idx].child_count += 1;
}
// insert root
ents.push(commands.spawn(root_transform).id());
let mut result = InsertResult::default();
let mut rng = rand::thread_rng();
// used to count through the number of children (used only for visual layout)
let mut child_idx: Vec<u16> = vec![0; count];
// insert children
for (current_idx, &parent_idx) in parent_map.iter().enumerate() {
let current_idx = current_idx + 1;
// separation factor to visually separate children (0..1)
let sep = child_idx[parent_idx] as f32 / node_info[parent_idx].child_count as f32;
child_idx[parent_idx] += 1;
// calculate and set depth
// this works because it's guaranteed that we have already iterated over the parent
let depth = node_info[parent_idx].depth + 1;
let info = &mut node_info[current_idx];
info.depth = depth;
// update max depth of tree
result.maximum_depth = result.maximum_depth.max(depth.try_into().unwrap());
// insert child
let child_entity = {
let mut cmd = commands.spawn_empty();
// check whether or not to update this node
let update = (rng.r#gen::<f32>() <= update_filter.probability)
&& (depth >= update_filter.min_depth && depth <= update_filter.max_depth);
if update {
cmd.insert(UpdateValue(sep));
result.active_nodes += 1;
}
let transform = {
let mut translation = Vec3::ZERO;
// use the same placement fn as the `update` system
// this way the entities won't be all at (0, 0, 0) when they don't have an `Update` component
set_translation(&mut translation, sep);
Transform::from_translation(translation)
};
// only insert the components necessary for the transform propagation
cmd.insert(transform);
cmd.id()
};
commands.entity(ents[parent_idx]).add_child(child_entity);
ents.push(child_entity);
}
result.inserted_nodes = ents.len();
result
}
/// generate a tree `depth` levels deep, where each node has `branch_width` children
fn gen_tree(depth: u32, branch_width: u32) -> Vec<usize> {
// calculate the total count of branches
let mut count: usize = 0;
for i in 0..(depth - 1) {
count += TryInto::<usize>::try_into(branch_width.pow(i)).unwrap();
}
// the tree is built using this pattern:
// 0, 0, 0, ... 1, 1, 1, ... 2, 2, 2, ... (count - 1)
(0..count)
.flat_map(|i| std::iter::repeat_n(i, branch_width.try_into().unwrap()))
.collect()
}
/// recursive part of [`gen_non_uniform_tree`]
fn add_children_non_uniform(
tree: &mut Vec<usize>,
parent: usize,
mut curr_depth: u32,
max_branch_width: u32,
) {
for _ in 0..max_branch_width {
tree.push(parent);
curr_depth = curr_depth.checked_sub(1).unwrap();
if curr_depth == 0 {
return;
}
add_children_non_uniform(tree, tree.len(), curr_depth, max_branch_width);
}
}
/// generate a tree that has more nodes on one side that the other
/// the deepest hierarchy path is `max_depth` and the widest branches have `max_branch_width` children
fn gen_non_uniform_tree(max_depth: u32, max_branch_width: u32) -> Vec<usize> {
let mut tree = Vec::new();
add_children_non_uniform(&mut tree, 0, max_depth, max_branch_width);
tree
}
/// parent map for a decently complex humanoid rig (based on mixamo rig)
const HUMANOID_RIG: [usize; 67] = [
// (0: root)
0, // 1: hips
1, // 2: spine
2, // 3: spine 1
3, // 4: spine 2
4, // 5: neck
5, // 6: head
6, // 7: head top
6, // 8: left eye
6, // 9: right eye
4, // 10: left shoulder
10, // 11: left arm
11, // 12: left forearm
12, // 13: left hand
13, // 14: left hand thumb 1
14, // 15: left hand thumb 2
15, // 16: left hand thumb 3
16, // 17: left hand thumb 4
13, // 18: left hand index 1
18, // 19: left hand index 2
19, // 20: left hand index 3
20, // 21: left hand index 4
13, // 22: left hand middle 1
22, // 23: left hand middle 2
23, // 24: left hand middle 3
24, // 25: left hand middle 4
13, // 26: left hand ring 1
26, // 27: left hand ring 2
27, // 28: left hand ring 3
28, // 29: left hand ring 4
13, // 30: left hand pinky 1
30, // 31: left hand pinky 2
31, // 32: left hand pinky 3
32, // 33: left hand pinky 4
4, // 34: right shoulder
34, // 35: right arm
35, // 36: right forearm
36, // 37: right hand
37, // 38: right hand thumb 1
38, // 39: right hand thumb 2
39, // 40: right hand thumb 3
40, // 41: right hand thumb 4
37, // 42: right hand index 1
42, // 43: right hand index 2
43, // 44: right hand index 3
44, // 45: right hand index 4
37, // 46: right hand middle 1
46, // 47: right hand middle 2
47, // 48: right hand middle 3
48, // 49: right hand middle 4
37, // 50: right hand ring 1
50, // 51: right hand ring 2
51, // 52: right hand ring 3
52, // 53: right hand ring 4
37, // 54: right hand pinky 1
54, // 55: right hand pinky 2
55, // 56: right hand pinky 3
56, // 57: right hand pinky 4
1, // 58: left upper leg
58, // 59: left leg
59, // 60: left foot
60, // 61: left toe base
61, // 62: left toe end
1, // 63: right upper leg
63, // 64: right leg
64, // 65: right foot
65, // 66: right toe base
66, // 67: right toe end
];

View File

@@ -0,0 +1 @@
This is a stress test used to push Bevy to its limit and debug performance issues. It is not representative of an actual game. It must be run in release mode using --release or it will be very slow.