Skip to content

Commit 69e20b9

Browse files
committed
Parallelized transform propagation (#4775)
# Objective Fixes #4697. Hierarchical propagation of properties, currently only Transform -> GlobalTransform, can be a very expensive operation. Transform propagation is a strict dependency for anything positioned in world-space. In large worlds, this can take quite a bit of time, so limiting it to a single thread can result in poor CPU utilization as it bottlenecks the rest of the frame's systems. ## Solution - Move transforms without a parent or a child (free-floating (Global)Transform) entities into a separate parallel system. - Chunk the hierarchy based on the root entities and process it in parallel with `Query::par_for_each_mut`. - Utilize the hierarchy's specific properties introduced in #4717 to allow for safe use of `Query::get_unchecked` on multiple threads. Assuming each child is unique in the hierarchy, it is impossible to have an aliased `&mut GlobalTransform` so long as we verify that the parent for a child is the same one propagated from. --- ## Changelog Removed: `transform_propagate_system` is no longer `pub`.
1 parent e0c3c6d commit 69e20b9

File tree

5 files changed

+125
-59
lines changed

5 files changed

+125
-59
lines changed

crates/bevy_transform/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,8 @@ bevy_math = { path = "../bevy_math", version = "0.9.0" }
1717
bevy_reflect = { path = "../bevy_reflect", version = "0.9.0", features = ["bevy"] }
1818
serde = { version = "1", features = ["derive"], optional = true }
1919

20+
[dev_dependencies]
21+
bevy_tasks = { path = "../bevy_tasks", version = "0.9.0-dev" }
22+
2023
[features]
2124
serialize = ["dep:serde", "bevy_math/serialize"]

crates/bevy_transform/src/components/global_transform.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect};
1919
///
2020
/// [`GlobalTransform`] is the position of an entity relative to the reference frame.
2121
///
22-
/// [`GlobalTransform`] is updated from [`Transform`] in the system
23-
/// [`transform_propagate_system`](crate::transform_propagate_system).
22+
/// [`GlobalTransform`] is updated from [`Transform`] in the systems labeled
23+
/// [`TransformPropagate`](crate::TransformSystem::TransformPropagate).
2424
///
2525
/// This system runs in stage [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate). If you
2626
/// update the [`Transform`] of an entity in this stage or after, you will notice a 1 frame lag

crates/bevy_transform/src/components/transform.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ use std::ops::Mul;
2020
///
2121
/// [`GlobalTransform`] is the position of an entity relative to the reference frame.
2222
///
23-
/// [`GlobalTransform`] is updated from [`Transform`] in the system
24-
/// [`transform_propagate_system`](crate::transform_propagate_system).
23+
/// [`GlobalTransform`] is updated from [`Transform`] in the systems labeled
24+
/// [`TransformPropagate`](crate::TransformSystem::TransformPropagate).
2525
///
2626
/// This system runs in stage [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate). If you
2727
/// update the [`Transform`] of an entity in this stage or after, you will notice a 1 frame lag

crates/bevy_transform/src/lib.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
/// The basic components of the transform crate
55
pub mod components;
66
mod systems;
7-
pub use crate::systems::transform_propagate_system;
87

98
#[doc(hidden)]
109
pub mod prelude {
@@ -32,8 +31,8 @@ use prelude::{GlobalTransform, Transform};
3231
///
3332
/// [`GlobalTransform`] is the position of an entity relative to the reference frame.
3433
///
35-
/// [`GlobalTransform`] is updated from [`Transform`] in the system
36-
/// [`transform_propagate_system`].
34+
/// [`GlobalTransform`] is updated from [`Transform`] in the systems labeled
35+
/// [`TransformPropagate`](crate::TransformSystem::TransformPropagate).
3736
///
3837
/// This system runs in stage [`CoreStage::PostUpdate`](crate::CoreStage::PostUpdate). If you
3938
/// update the [`Transform`] of an entity in this stage or after, you will notice a 1 frame lag
@@ -91,11 +90,19 @@ impl Plugin for TransformPlugin {
9190
// add transform systems to startup so the first update is "correct"
9291
.add_startup_system_to_stage(
9392
StartupStage::PostStartup,
94-
systems::transform_propagate_system.label(TransformSystem::TransformPropagate),
93+
systems::sync_simple_transforms.label(TransformSystem::TransformPropagate),
94+
)
95+
.add_startup_system_to_stage(
96+
StartupStage::PostStartup,
97+
systems::propagate_transforms.label(TransformSystem::TransformPropagate),
98+
)
99+
.add_system_to_stage(
100+
CoreStage::PostUpdate,
101+
systems::sync_simple_transforms.label(TransformSystem::TransformPropagate),
95102
)
96103
.add_system_to_stage(
97104
CoreStage::PostUpdate,
98-
systems::transform_propagate_system.label(TransformSystem::TransformPropagate),
105+
systems::propagate_transforms.label(TransformSystem::TransformPropagate),
99106
);
100107
}
101108
}

crates/bevy_transform/src/systems.rs

Lines changed: 106 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,95 +2,140 @@ use crate::components::{GlobalTransform, Transform};
22
use bevy_ecs::prelude::{Changed, Entity, Query, With, Without};
33
use bevy_hierarchy::{Children, Parent};
44

5+
/// Update [`GlobalTransform`] component of entities that aren't in the hierarchy
6+
pub fn sync_simple_transforms(
7+
mut query: Query<
8+
(&Transform, &mut GlobalTransform),
9+
(Changed<Transform>, Without<Parent>, Without<Children>),
10+
>,
11+
) {
12+
query.par_for_each_mut(1024, |(transform, mut global_transform)| {
13+
*global_transform = GlobalTransform::from(*transform);
14+
});
15+
}
16+
517
/// Update [`GlobalTransform`] component of entities based on entity hierarchy and
618
/// [`Transform`] component.
7-
pub fn transform_propagate_system(
19+
pub fn propagate_transforms(
820
mut root_query: Query<
921
(
10-
Option<(&Children, Changed<Children>)>,
22+
Entity,
23+
&Children,
1124
&Transform,
1225
Changed<Transform>,
26+
Changed<Children>,
1327
&mut GlobalTransform,
14-
Entity,
1528
),
1629
Without<Parent>,
1730
>,
18-
mut transform_query: Query<(
19-
&Transform,
20-
Changed<Transform>,
21-
&mut GlobalTransform,
22-
&Parent,
23-
)>,
31+
transform_query: Query<(&Transform, Changed<Transform>, &mut GlobalTransform), With<Parent>>,
32+
parent_query: Query<&Parent>,
2433
children_query: Query<(&Children, Changed<Children>), (With<Parent>, With<GlobalTransform>)>,
2534
) {
26-
for (children, transform, transform_changed, mut global_transform, entity) in
27-
root_query.iter_mut()
28-
{
29-
let mut changed = transform_changed;
30-
if transform_changed {
31-
*global_transform = GlobalTransform::from(*transform);
32-
}
35+
root_query.par_for_each_mut(
36+
// The differing depths and sizes of hierarchy trees causes the work for each root to be
37+
// different. A batch size of 1 ensures that each tree gets it's own task and multiple
38+
// large trees are not clumped together.
39+
1,
40+
|(entity, children, transform, mut changed, children_changed, mut global_transform)| {
41+
if changed {
42+
*global_transform = GlobalTransform::from(*transform);
43+
}
3344

34-
if let Some((children, changed_children)) = children {
3545
// If our `Children` has changed, we need to recalculate everything below us
36-
changed |= changed_children;
37-
for child in children {
38-
let _ = propagate_recursive(
46+
changed |= children_changed;
47+
48+
for child in children.iter() {
49+
propagate_recursive(
3950
&global_transform,
40-
&mut transform_query,
51+
&transform_query,
52+
&parent_query,
4153
&children_query,
42-
*child,
4354
entity,
55+
*child,
4456
changed,
4557
);
4658
}
47-
}
48-
}
59+
},
60+
);
4961
}
5062

5163
fn propagate_recursive(
5264
parent: &GlobalTransform,
53-
transform_query: &mut Query<(
54-
&Transform,
55-
Changed<Transform>,
56-
&mut GlobalTransform,
57-
&Parent,
58-
)>,
65+
unsafe_transform_query: &Query<
66+
(&Transform, Changed<Transform>, &mut GlobalTransform),
67+
With<Parent>,
68+
>,
69+
parent_query: &Query<&Parent>,
5970
children_query: &Query<(&Children, Changed<Children>), (With<Parent>, With<GlobalTransform>)>,
60-
entity: Entity,
6171
expected_parent: Entity,
72+
entity: Entity,
6273
mut changed: bool,
6374
// We use a result here to use the `?` operator. Ideally we'd use a try block instead
64-
) -> Result<(), ()> {
75+
) {
76+
let Ok(actual_parent) = parent_query.get(entity) else {
77+
panic!("Propagated child for {:?} has no Parent component!", entity);
78+
};
79+
assert_eq!(
80+
actual_parent.get(), expected_parent,
81+
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
82+
);
83+
6584
let global_matrix = {
66-
let (transform, transform_changed, mut global_transform, child_parent) =
67-
transform_query.get_mut(entity).map_err(drop)?;
68-
// Note that for parallelising, this check cannot occur here, since there is an `&mut GlobalTransform` (in global_transform)
69-
assert_eq!(
70-
child_parent.get(), expected_parent,
71-
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
72-
);
85+
let Ok((transform, transform_changed, mut global_transform)) =
86+
// SAFETY: This call cannot create aliased mutable references.
87+
// - The top level iteration parallelizes on the roots of the hierarchy.
88+
// - The above assertion ensures that each child has one and only one unique parent throughout the entire
89+
// hierarchy.
90+
//
91+
// For example, consider the following malformed hierarchy:
92+
//
93+
// A
94+
// / \
95+
// B C
96+
// \ /
97+
// D
98+
//
99+
// D has two parents, B and C. If the propagation passes through C, but the Parent component on D points to B,
100+
// the above check will panic as the origin parent does match the recorded parent.
101+
//
102+
// Also consider the following case, where A and B are roots:
103+
//
104+
// A B
105+
// \ /
106+
// C D
107+
// \ /
108+
// E
109+
//
110+
// Even if these A and B start two separate tasks running in parallel, one of them will panic before attempting
111+
// to mutably access E.
112+
(unsafe { unsafe_transform_query.get_unchecked(entity) }) else {
113+
return;
114+
};
115+
73116
changed |= transform_changed;
74117
if changed {
75118
*global_transform = parent.mul_transform(*transform);
76119
}
77120
*global_transform
78121
};
79122

80-
let (children, changed_children) = children_query.get(entity).map_err(drop)?;
123+
let Ok((children, changed_children)) = children_query.get(entity) else {
124+
return
125+
};
81126
// If our `Children` has changed, we need to recalculate everything below us
82127
changed |= changed_children;
83128
for child in children {
84-
let _ = propagate_recursive(
129+
propagate_recursive(
85130
&global_matrix,
86-
transform_query,
131+
unsafe_transform_query,
132+
parent_query,
87133
children_query,
88-
*child,
89134
entity,
135+
*child,
90136
changed,
91137
);
92138
}
93-
Ok(())
94139
}
95140

96141
#[cfg(test)]
@@ -99,9 +144,10 @@ mod test {
99144
use bevy_ecs::prelude::*;
100145
use bevy_ecs::system::CommandQueue;
101146
use bevy_math::vec3;
147+
use bevy_tasks::{ComputeTaskPool, TaskPool};
102148

103149
use crate::components::{GlobalTransform, Transform};
104-
use crate::systems::transform_propagate_system;
150+
use crate::systems::*;
105151
use crate::TransformBundle;
106152
use bevy_hierarchy::{BuildChildren, BuildWorldChildren, Children, Parent};
107153

@@ -110,10 +156,12 @@ mod test {
110156

111157
#[test]
112158
fn did_propagate() {
159+
ComputeTaskPool::init(TaskPool::default);
113160
let mut world = World::default();
114161

115162
let mut update_stage = SystemStage::parallel();
116-
update_stage.add_system(transform_propagate_system);
163+
update_stage.add_system(sync_simple_transforms);
164+
update_stage.add_system(propagate_transforms);
117165

118166
let mut schedule = Schedule::default();
119167
schedule.add_stage(Update, update_stage);
@@ -152,8 +200,10 @@ mod test {
152200
#[test]
153201
fn did_propagate_command_buffer() {
154202
let mut world = World::default();
203+
155204
let mut update_stage = SystemStage::parallel();
156-
update_stage.add_system(transform_propagate_system);
205+
update_stage.add_system(sync_simple_transforms);
206+
update_stage.add_system(propagate_transforms);
157207

158208
let mut schedule = Schedule::default();
159209
schedule.add_stage(Update, update_stage);
@@ -192,10 +242,12 @@ mod test {
192242

193243
#[test]
194244
fn correct_children() {
245+
ComputeTaskPool::init(TaskPool::default);
195246
let mut world = World::default();
196247

197248
let mut update_stage = SystemStage::parallel();
198-
update_stage.add_system(transform_propagate_system);
249+
update_stage.add_system(sync_simple_transforms);
250+
update_stage.add_system(propagate_transforms);
199251

200252
let mut schedule = Schedule::default();
201253
schedule.add_stage(Update, update_stage);
@@ -272,8 +324,10 @@ mod test {
272324
#[test]
273325
fn correct_transforms_when_no_children() {
274326
let mut app = App::new();
327+
ComputeTaskPool::init(TaskPool::default);
275328

276-
app.add_system(transform_propagate_system);
329+
app.add_system(sync_simple_transforms);
330+
app.add_system(propagate_transforms);
277331

278332
let translation = vec3(1.0, 0.0, 0.0);
279333

@@ -313,6 +367,7 @@ mod test {
313367
#[test]
314368
#[should_panic]
315369
fn panic_when_hierarchy_cycle() {
370+
ComputeTaskPool::init(TaskPool::default);
316371
// We cannot directly edit Parent and Children, so we use a temp world to break
317372
// the hierarchy's invariants.
318373
let mut temp = World::new();
@@ -321,7 +376,8 @@ mod test {
321376
// FIXME: Parallel executors seem to have some odd interaction with the other
322377
// tests in this crate. Using single_threaded until a root cause can be found.
323378
app.add_stage("single", SystemStage::single_threaded())
324-
.add_system_to_stage("single", transform_propagate_system);
379+
.add_system_to_stage("single", sync_simple_transforms)
380+
.add_system_to_stage("single", propagate_transforms);
325381

326382
fn setup_world(world: &mut World) -> (Entity, Entity) {
327383
let mut grandchild = Entity::from_raw(0);

0 commit comments

Comments
 (0)