
# Objective - Fixes #17960 ## Solution - Followed the [edition upgrade guide](https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html) ## Testing - CI --- ## Summary of Changes ### Documentation Indentation When using lists in documentation, proper indentation is now linted for. This means subsequent lines within the same list item must start at the same indentation level as the item. ```rust /* Valid */ /// - Item 1 /// Run-on sentence. /// - Item 2 struct Foo; /* Invalid */ /// - Item 1 /// Run-on sentence. /// - Item 2 struct Foo; ``` ### Implicit `!` to `()` Conversion `!` (the never return type, returned by `panic!`, etc.) no longer implicitly converts to `()`. This is particularly painful for systems with `todo!` or `panic!` statements, as they will no longer be functions returning `()` (or `Result<()>`), making them invalid systems for functions like `add_systems`. The ideal fix would be to accept functions returning `!` (or rather, _not_ returning), but this is blocked on the [stabilisation of the `!` type itself](https://doc.rust-lang.org/std/primitive.never.html), which is not done. The "simple" fix would be to add an explicit `-> ()` to system signatures (e.g., `|| { todo!() }` becomes `|| -> () { todo!() }`). However, this is _also_ banned, as there is an existing lint which (IMO, incorrectly) marks this as an unnecessary annotation. So, the "fix" (read: workaround) is to put these kinds of `|| -> ! { ... }` closuers into variables and give the variable an explicit type (e.g., `fn()`). ```rust // Valid let system: fn() = || todo!("Not implemented yet!"); app.add_systems(..., system); // Invalid app.add_systems(..., || todo!("Not implemented yet!")); ``` ### Temporary Variable Lifetimes The order in which temporary variables are dropped has changed. The simple fix here is _usually_ to just assign temporaries to a named variable before use. ### `gen` is a keyword We can no longer use the name `gen` as it is reserved for a future generator syntax. This involved replacing uses of the name `gen` with `r#gen` (the raw-identifier syntax). ### Formatting has changed Use statements have had the order of imports changed, causing a substantial +/-3,000 diff when applied. For now, I have opted-out of this change by amending `rustfmt.toml` ```toml style_edition = "2021" ``` This preserves the original formatting for now, reducing the size of this PR. It would be a simple followup to update this to 2024 and run `cargo fmt`. ### New `use<>` Opt-Out Syntax Lifetimes are now implicitly included in RPIT types. There was a handful of instances where it needed to be added to satisfy the borrow checker, but there may be more cases where it _should_ be added to avoid breakages in user code. ### `MyUnitStruct { .. }` is an invalid pattern Previously, you could match against unit structs (and unit enum variants) with a `{ .. }` destructuring. This is no longer valid. ### Pretty much every use of `ref` and `mut` are gone Pattern binding has changed to the point where these terms are largely unused now. They still serve a purpose, but it is far more niche now. ### `iter::repeat(...).take(...)` is bad New lint recommends using the more explicit `iter::repeat_n(..., ...)` instead. ## Migration Guide The lifetimes of functions using return-position impl-trait (RPIT) are likely _more_ conservative than they had been previously. If you encounter lifetime issues with such a function, please create an issue to investigate the addition of `+ use<...>`. ## Notes - Check the individual commits for a clearer breakdown for what _actually_ changed. --------- Co-authored-by: François Mockers <francois.mockers@vleue.com>
551 lines
16 KiB
Rust
551 lines
16 KiB
Rust
//! 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
|
|
];
|