Path: blob/main/examples/stress_tests/transform_hierarchy.rs
6592 views
//! Hierarchy and transform propagation stress test.1//!2//! Running this example:3//!4//! ```5//! cargo r --release --example transform_hierarchy <configuration name>6//! ```7//!8//! | Configuration | Description |9//! | -------------------- | ----------------------------------------------------------------- |10//! | `large_tree` | A fairly wide and deep tree. |11//! | `wide_tree` | A shallow but very wide tree. |12//! | `deep_tree` | A deep but not very wide tree. |13//! | `chain` | A chain. 2500 levels deep. |14//! | `update_leaves` | Same as `large_tree`, but only leaves are updated. |15//! | `update_shallow` | Same as `large_tree`, but only the first few levels are updated. |16//! | `humanoids_active` | 4000 active humanoid rigs. |17//! | `humanoids_inactive` | 4000 humanoid rigs. Only 10 are active. |18//! | `humanoids_mixed` | 2000 active and 2000 inactive humanoid rigs. |1920use bevy::{21diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},22prelude::*,23window::ExitCondition,24};25use rand::Rng;2627/// pre-defined test configurations with name28const CONFIGS: [(&str, Cfg); 9] = [29(30"large_tree",31Cfg {32test_case: TestCase::NonUniformTree {33depth: 18,34branch_width: 8,35},36update_filter: UpdateFilter {37probability: 0.5,38min_depth: 0,39max_depth: u32::MAX,40},41},42),43(44"wide_tree",45Cfg {46test_case: TestCase::Tree {47depth: 3,48branch_width: 500,49},50update_filter: UpdateFilter {51probability: 0.5,52min_depth: 0,53max_depth: u32::MAX,54},55},56),57(58"deep_tree",59Cfg {60test_case: TestCase::NonUniformTree {61depth: 25,62branch_width: 2,63},64update_filter: UpdateFilter {65probability: 0.5,66min_depth: 0,67max_depth: u32::MAX,68},69},70),71(72"chain",73Cfg {74test_case: TestCase::Tree {75depth: 2500,76branch_width: 1,77},78update_filter: UpdateFilter {79probability: 0.5,80min_depth: 0,81max_depth: u32::MAX,82},83},84),85(86"update_leaves",87Cfg {88test_case: TestCase::Tree {89depth: 18,90branch_width: 2,91},92update_filter: UpdateFilter {93probability: 0.5,94min_depth: 17,95max_depth: u32::MAX,96},97},98),99(100"update_shallow",101Cfg {102test_case: TestCase::Tree {103depth: 18,104branch_width: 2,105},106update_filter: UpdateFilter {107probability: 0.5,108min_depth: 0,109max_depth: 8,110},111},112),113(114"humanoids_active",115Cfg {116test_case: TestCase::Humanoids {117active: 4000,118inactive: 0,119},120update_filter: UpdateFilter {121probability: 1.0,122min_depth: 0,123max_depth: u32::MAX,124},125},126),127(128"humanoids_inactive",129Cfg {130test_case: TestCase::Humanoids {131active: 10,132inactive: 3990,133},134update_filter: UpdateFilter {135probability: 1.0,136min_depth: 0,137max_depth: u32::MAX,138},139},140),141(142"humanoids_mixed",143Cfg {144test_case: TestCase::Humanoids {145active: 2000,146inactive: 2000,147},148update_filter: UpdateFilter {149probability: 1.0,150min_depth: 0,151max_depth: u32::MAX,152},153},154),155];156157fn print_available_configs() {158println!("available configurations:");159for (name, _) in CONFIGS {160println!(" {name}");161}162}163164fn main() {165// parse cli argument and find the selected test configuration166let cfg: Cfg = match std::env::args().nth(1) {167Some(arg) => match CONFIGS.iter().find(|(name, _)| *name == arg) {168Some((name, cfg)) => {169println!("test configuration: {name}");170cfg.clone()171}172None => {173println!("test configuration \"{arg}\" not found.\n");174print_available_configs();175return;176}177},178None => {179println!("missing argument: <test configuration>\n");180print_available_configs();181return;182}183};184185println!("\n{cfg:#?}");186187App::new()188.insert_resource(cfg)189.add_plugins((190DefaultPlugins.set(WindowPlugin {191primary_window: None,192exit_condition: ExitCondition::DontExit,193..default()194}),195FrameTimeDiagnosticsPlugin::default(),196LogDiagnosticsPlugin::default(),197))198.add_systems(Startup, setup)199// Updating transforms *must* be done before `PostUpdate`200// or the hierarchy will momentarily be in an invalid state.201.add_systems(Update, update)202.run();203}204205/// test configuration206#[derive(Resource, Debug, Clone)]207struct Cfg {208/// which test case should be inserted209test_case: TestCase,210/// which entities should be updated211update_filter: UpdateFilter,212}213214#[derive(Debug, Clone)]215enum TestCase {216/// a uniform tree, exponentially growing with depth217Tree {218/// total depth219depth: u32,220/// number of children per node221branch_width: u32,222},223/// a non uniform tree (one side is deeper than the other)224/// creates significantly less nodes than `TestCase::Tree` with the same parameters225NonUniformTree {226/// the maximum depth227depth: u32,228/// max number of children per node229branch_width: u32,230},231/// one or multiple humanoid rigs232Humanoids {233/// number of active instances (uses the specified [`UpdateFilter`])234active: u32,235/// number of inactive instances (always inactive)236inactive: u32,237},238}239240/// a filter to restrict which nodes are updated241#[derive(Debug, Clone)]242struct UpdateFilter {243/// starting depth (inclusive)244min_depth: u32,245/// end depth (inclusive)246max_depth: u32,247/// probability of a node to get updated (evaluated at insertion time, not during update)248/// 0 (never) .. 1 (always)249probability: f32,250}251252/// update component with some per-component value253#[derive(Component)]254struct UpdateValue(f32);255256/// update positions system257fn update(time: Res<Time>, mut query: Query<(&mut Transform, &mut UpdateValue)>) {258for (mut t, mut u) in &mut query {259u.0 += time.delta_secs() * 0.1;260set_translation(&mut t.translation, u.0);261}262}263264/// set translation based on the angle `a`265fn set_translation(translation: &mut Vec3, a: f32) {266translation.x = ops::cos(a) * 32.0;267translation.y = ops::sin(a) * 32.0;268}269270fn setup(mut commands: Commands, cfg: Res<Cfg>) {271warn!(include_str!("warning_string.txt"));272273commands.spawn((Camera2d, Transform::from_xyz(0.0, 0.0, 100.0)));274275let result = match cfg.test_case {276TestCase::Tree {277depth,278branch_width,279} => {280let tree = gen_tree(depth, branch_width);281spawn_tree(&tree, &mut commands, &cfg.update_filter, default())282}283TestCase::NonUniformTree {284depth,285branch_width,286} => {287let tree = gen_non_uniform_tree(depth, branch_width);288spawn_tree(&tree, &mut commands, &cfg.update_filter, default())289}290TestCase::Humanoids { active, inactive } => {291let mut result = InsertResult::default();292let mut rng = rand::rng();293294for _ in 0..active {295result.combine(spawn_tree(296&HUMANOID_RIG,297&mut commands,298&cfg.update_filter,299Transform::from_xyz(300rng.random::<f32>() * 500.0 - 250.0,301rng.random::<f32>() * 500.0 - 250.0,3020.0,303),304));305}306307for _ in 0..inactive {308result.combine(spawn_tree(309&HUMANOID_RIG,310&mut commands,311&UpdateFilter {312// force inactive by setting the probability < 0313probability: -1.0,314..cfg.update_filter315},316Transform::from_xyz(317rng.random::<f32>() * 500.0 - 250.0,318rng.random::<f32>() * 500.0 - 250.0,3190.0,320),321));322}323324result325}326};327328println!("\n{result:#?}");329}330331/// overview of the inserted hierarchy332#[derive(Default, Debug)]333struct InsertResult {334/// total number of nodes inserted335inserted_nodes: usize,336/// number of nodes that get updated each frame337active_nodes: usize,338/// maximum depth of the hierarchy tree339maximum_depth: usize,340}341342impl InsertResult {343fn combine(&mut self, rhs: Self) -> &mut Self {344self.inserted_nodes += rhs.inserted_nodes;345self.active_nodes += rhs.active_nodes;346self.maximum_depth = self.maximum_depth.max(rhs.maximum_depth);347self348}349}350351/// spawns a tree defined by a parent map (excluding root)352/// the parent map must be ordered (parent must exist before child)353fn spawn_tree(354parent_map: &[usize],355commands: &mut Commands,356update_filter: &UpdateFilter,357root_transform: Transform,358) -> InsertResult {359// total count (# of nodes + root)360let count = parent_map.len() + 1;361362#[derive(Default, Clone, Copy)]363struct NodeInfo {364child_count: u32,365depth: u32,366}367368// node index -> entity lookup list369let mut ents: Vec<Entity> = Vec::with_capacity(count);370let mut node_info: Vec<NodeInfo> = vec![default(); count];371for (i, &parent_idx) in parent_map.iter().enumerate() {372// assert spawn order (parent must be processed before child)373assert!(parent_idx <= i, "invalid spawn order");374node_info[parent_idx].child_count += 1;375}376377// insert root378ents.push(commands.spawn(root_transform).id());379380let mut result = InsertResult::default();381let mut rng = rand::rng();382// used to count through the number of children (used only for visual layout)383let mut child_idx: Vec<u16> = vec![0; count];384385// insert children386for (current_idx, &parent_idx) in parent_map.iter().enumerate() {387let current_idx = current_idx + 1;388389// separation factor to visually separate children (0..1)390let sep = child_idx[parent_idx] as f32 / node_info[parent_idx].child_count as f32;391child_idx[parent_idx] += 1;392393// calculate and set depth394// this works because it's guaranteed that we have already iterated over the parent395let depth = node_info[parent_idx].depth + 1;396let info = &mut node_info[current_idx];397info.depth = depth;398399// update max depth of tree400result.maximum_depth = result.maximum_depth.max(depth.try_into().unwrap());401402// insert child403let child_entity = {404let mut cmd = commands.spawn_empty();405406// check whether or not to update this node407let update = (rng.random::<f32>() <= update_filter.probability)408&& (depth >= update_filter.min_depth && depth <= update_filter.max_depth);409410if update {411cmd.insert(UpdateValue(sep));412result.active_nodes += 1;413}414415let transform = {416let mut translation = Vec3::ZERO;417// use the same placement fn as the `update` system418// this way the entities won't be all at (0, 0, 0) when they don't have an `Update` component419set_translation(&mut translation, sep);420Transform::from_translation(translation)421};422423// only insert the components necessary for the transform propagation424cmd.insert(transform);425426cmd.id()427};428429commands.entity(ents[parent_idx]).add_child(child_entity);430431ents.push(child_entity);432}433434result.inserted_nodes = ents.len();435result436}437438/// generate a tree `depth` levels deep, where each node has `branch_width` children439fn gen_tree(depth: u32, branch_width: u32) -> Vec<usize> {440// calculate the total count of branches441let mut count: usize = 0;442for i in 0..(depth - 1) {443count += TryInto::<usize>::try_into(branch_width.pow(i)).unwrap();444}445446// the tree is built using this pattern:447// 0, 0, 0, ... 1, 1, 1, ... 2, 2, 2, ... (count - 1)448(0..count)449.flat_map(|i| std::iter::repeat_n(i, branch_width.try_into().unwrap()))450.collect()451}452453/// recursive part of [`gen_non_uniform_tree`]454fn add_children_non_uniform(455tree: &mut Vec<usize>,456parent: usize,457mut curr_depth: u32,458max_branch_width: u32,459) {460for _ in 0..max_branch_width {461tree.push(parent);462463curr_depth = curr_depth.checked_sub(1).unwrap();464if curr_depth == 0 {465return;466}467add_children_non_uniform(tree, tree.len(), curr_depth, max_branch_width);468}469}470471/// generate a tree that has more nodes on one side that the other472/// the deepest hierarchy path is `max_depth` and the widest branches have `max_branch_width` children473fn gen_non_uniform_tree(max_depth: u32, max_branch_width: u32) -> Vec<usize> {474let mut tree = Vec::new();475add_children_non_uniform(&mut tree, 0, max_depth, max_branch_width);476tree477}478479/// parent map for a decently complex humanoid rig (based on mixamo rig)480const HUMANOID_RIG: [usize; 67] = [481// (0: root)4820, // 1: hips4831, // 2: spine4842, // 3: spine 14853, // 4: spine 24864, // 5: neck4875, // 6: head4886, // 7: head top4896, // 8: left eye4906, // 9: right eye4914, // 10: left shoulder49210, // 11: left arm49311, // 12: left forearm49412, // 13: left hand49513, // 14: left hand thumb 149614, // 15: left hand thumb 249715, // 16: left hand thumb 349816, // 17: left hand thumb 449913, // 18: left hand index 150018, // 19: left hand index 250119, // 20: left hand index 350220, // 21: left hand index 450313, // 22: left hand middle 150422, // 23: left hand middle 250523, // 24: left hand middle 350624, // 25: left hand middle 450713, // 26: left hand ring 150826, // 27: left hand ring 250927, // 28: left hand ring 351028, // 29: left hand ring 451113, // 30: left hand pinky 151230, // 31: left hand pinky 251331, // 32: left hand pinky 351432, // 33: left hand pinky 45154, // 34: right shoulder51634, // 35: right arm51735, // 36: right forearm51836, // 37: right hand51937, // 38: right hand thumb 152038, // 39: right hand thumb 252139, // 40: right hand thumb 352240, // 41: right hand thumb 452337, // 42: right hand index 152442, // 43: right hand index 252543, // 44: right hand index 352644, // 45: right hand index 452737, // 46: right hand middle 152846, // 47: right hand middle 252947, // 48: right hand middle 353048, // 49: right hand middle 453137, // 50: right hand ring 153250, // 51: right hand ring 253351, // 52: right hand ring 353452, // 53: right hand ring 453537, // 54: right hand pinky 153654, // 55: right hand pinky 253755, // 56: right hand pinky 353856, // 57: right hand pinky 45391, // 58: left upper leg54058, // 59: left leg54159, // 60: left foot54260, // 61: left toe base54361, // 62: left toe end5441, // 63: right upper leg54563, // 64: right leg54664, // 65: right foot54765, // 66: right toe base54866, // 67: right toe end549];550551552