-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Merged by Bors] - Parallelized transform propagation #4775
Changes from 11 commits
4e5ae27
ff0f0bc
925c360
80b2a33
8f28b3d
40c9a4e
fd483d0
73fee07
73ec563
efbbae2
4cec581
691f99a
e032f9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,95 +2,140 @@ use crate::components::{GlobalTransform, Transform}; | |
use bevy_ecs::prelude::{Changed, Entity, Query, With, Without}; | ||
use bevy_hierarchy::{Children, Parent}; | ||
|
||
/// Update [`GlobalTransform`] component of entities that aren't in the hierarchy | ||
pub fn sync_simple_transforms( | ||
mut query: Query< | ||
(&Transform, &mut GlobalTransform), | ||
(Changed<Transform>, Without<Parent>, Without<Children>), | ||
>, | ||
) { | ||
query.par_for_each_mut(1024, |(transform, mut global_transform)| { | ||
*global_transform = GlobalTransform::from(*transform); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't a problem with this PR, but it's an interesting conundrum: Currently, if the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Simple solution: make |
||
}); | ||
} | ||
|
||
/// Update [`GlobalTransform`] component of entities based on entity hierarchy and | ||
/// [`Transform`] component. | ||
pub fn transform_propagate_system( | ||
pub fn propagate_transforms( | ||
mut root_query: Query< | ||
( | ||
Option<(&Children, Changed<Children>)>, | ||
Entity, | ||
&Children, | ||
&Transform, | ||
Changed<Transform>, | ||
Changed<Children>, | ||
&mut GlobalTransform, | ||
Entity, | ||
), | ||
Without<Parent>, | ||
>, | ||
mut transform_query: Query<( | ||
&Transform, | ||
Changed<Transform>, | ||
&mut GlobalTransform, | ||
&Parent, | ||
)>, | ||
transform_query: Query<(&Transform, Changed<Transform>, &mut GlobalTransform), With<Parent>>, | ||
parent_query: Query<&Parent>, | ||
children_query: Query<(&Children, Changed<Children>), (With<Parent>, With<GlobalTransform>)>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this query need those filters? I guess completeness of recording requirements to limit possible collisions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should allow this system to run in parallel with |
||
) { | ||
for (children, transform, transform_changed, mut global_transform, entity) in | ||
root_query.iter_mut() | ||
{ | ||
let mut changed = transform_changed; | ||
if transform_changed { | ||
*global_transform = GlobalTransform::from(*transform); | ||
} | ||
root_query.par_for_each_mut( | ||
// The differing depths and sizes of hierarchy trees causes the work for each root to be | ||
// different. A batch size of 1 ensures that each tree gets it's own task and multiple | ||
// large trees are not clumped together. | ||
1, | ||
james7132 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|(entity, children, transform, mut changed, children_changed, mut global_transform)| { | ||
if changed { | ||
*global_transform = GlobalTransform::from(*transform); | ||
} | ||
|
||
if let Some((children, changed_children)) = children { | ||
// If our `Children` has changed, we need to recalculate everything below us | ||
changed |= changed_children; | ||
james7132 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for child in children { | ||
let _ = propagate_recursive( | ||
changed |= children_changed; | ||
|
||
for child in children.iter() { | ||
propagate_recursive( | ||
&global_transform, | ||
&mut transform_query, | ||
&transform_query, | ||
&parent_query, | ||
&children_query, | ||
*child, | ||
entity, | ||
*child, | ||
changed, | ||
); | ||
} | ||
} | ||
} | ||
}, | ||
); | ||
} | ||
|
||
fn propagate_recursive( | ||
parent: &GlobalTransform, | ||
transform_query: &mut Query<( | ||
&Transform, | ||
Changed<Transform>, | ||
&mut GlobalTransform, | ||
&Parent, | ||
)>, | ||
unsafe_transform_query: &Query< | ||
(&Transform, Changed<Transform>, &mut GlobalTransform), | ||
With<Parent>, | ||
>, | ||
parent_query: &Query<&Parent>, | ||
children_query: &Query<(&Children, Changed<Children>), (With<Parent>, With<GlobalTransform>)>, | ||
entity: Entity, | ||
expected_parent: Entity, | ||
entity: Entity, | ||
mut changed: bool, | ||
// We use a result here to use the `?` operator. Ideally we'd use a try block instead | ||
) -> Result<(), ()> { | ||
) { | ||
let Ok(actual_parent) = parent_query.get(entity) else { | ||
panic!("Propagated child for {:?} has no Parent component!", entity); | ||
}; | ||
assert_eq!( | ||
actual_parent.get(), expected_parent, | ||
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" | ||
); | ||
|
||
let global_matrix = { | ||
let (transform, transform_changed, mut global_transform, child_parent) = | ||
transform_query.get_mut(entity).map_err(drop)?; | ||
// Note that for parallelising, this check cannot occur here, since there is an `&mut GlobalTransform` (in global_transform) | ||
assert_eq!( | ||
child_parent.get(), expected_parent, | ||
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" | ||
); | ||
let Ok((transform, transform_changed, mut global_transform)) = | ||
// SAFETY: This call cannot create aliased mutable references. | ||
// - The top level iteration parallelizes on the roots of the hierarchy. | ||
// - The above assertion ensures that each child has one and only one unique parent throughout the entire | ||
// hierarchy. | ||
// | ||
// For example, consider the following malformed hierarchy: | ||
// | ||
// A | ||
// / \ | ||
// B C | ||
// \ / | ||
// D | ||
// | ||
// D has two parents, B and C. If the propagation passes through C, but the Parent component on D points to B, | ||
// the above check will panic as the origin parent does match the recorded parent. | ||
// | ||
// Also consider the following case, where A and B are roots: | ||
// | ||
// A B | ||
// \ / | ||
// C D | ||
// \ / | ||
// E | ||
// | ||
// Even if these A and B start two separate tasks running in parallel, one of them will panic before attempting | ||
// to mutably access E. | ||
(unsafe { unsafe_transform_query.get_unchecked(entity) }) else { | ||
return; | ||
}; | ||
|
||
changed |= transform_changed; | ||
if changed { | ||
*global_transform = parent.mul_transform(*transform); | ||
} | ||
*global_transform | ||
}; | ||
|
||
let (children, changed_children) = children_query.get(entity).map_err(drop)?; | ||
let Ok((children, changed_children)) = children_query.get(entity) else { | ||
return | ||
}; | ||
// If our `Children` has changed, we need to recalculate everything below us | ||
changed |= changed_children; | ||
for child in children { | ||
let _ = propagate_recursive( | ||
propagate_recursive( | ||
&global_matrix, | ||
transform_query, | ||
unsafe_transform_query, | ||
parent_query, | ||
children_query, | ||
*child, | ||
entity, | ||
*child, | ||
changed, | ||
); | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[cfg(test)] | ||
|
@@ -99,9 +144,10 @@ mod test { | |
use bevy_ecs::prelude::*; | ||
use bevy_ecs::system::CommandQueue; | ||
use bevy_math::vec3; | ||
use bevy_tasks::{ComputeTaskPool, TaskPool}; | ||
|
||
use crate::components::{GlobalTransform, Transform}; | ||
use crate::systems::transform_propagate_system; | ||
use crate::systems::*; | ||
use crate::TransformBundle; | ||
use bevy_hierarchy::{BuildChildren, BuildWorldChildren, Children, Parent}; | ||
|
||
|
@@ -110,10 +156,12 @@ mod test { | |
|
||
#[test] | ||
fn did_propagate() { | ||
ComputeTaskPool::init(TaskPool::default); | ||
let mut world = World::default(); | ||
|
||
let mut update_stage = SystemStage::parallel(); | ||
update_stage.add_system(transform_propagate_system); | ||
update_stage.add_system(sync_simple_transforms); | ||
update_stage.add_system(propagate_transforms); | ||
|
||
let mut schedule = Schedule::default(); | ||
schedule.add_stage(Update, update_stage); | ||
|
@@ -152,8 +200,10 @@ mod test { | |
#[test] | ||
fn did_propagate_command_buffer() { | ||
let mut world = World::default(); | ||
|
||
let mut update_stage = SystemStage::parallel(); | ||
update_stage.add_system(transform_propagate_system); | ||
update_stage.add_system(sync_simple_transforms); | ||
update_stage.add_system(propagate_transforms); | ||
|
||
let mut schedule = Schedule::default(); | ||
schedule.add_stage(Update, update_stage); | ||
|
@@ -192,10 +242,12 @@ mod test { | |
|
||
#[test] | ||
fn correct_children() { | ||
ComputeTaskPool::init(TaskPool::default); | ||
let mut world = World::default(); | ||
|
||
let mut update_stage = SystemStage::parallel(); | ||
update_stage.add_system(transform_propagate_system); | ||
update_stage.add_system(sync_simple_transforms); | ||
update_stage.add_system(propagate_transforms); | ||
|
||
let mut schedule = Schedule::default(); | ||
schedule.add_stage(Update, update_stage); | ||
|
@@ -272,8 +324,10 @@ mod test { | |
#[test] | ||
fn correct_transforms_when_no_children() { | ||
let mut app = App::new(); | ||
ComputeTaskPool::init(TaskPool::default); | ||
|
||
app.add_system(transform_propagate_system); | ||
app.add_system(sync_simple_transforms); | ||
app.add_system(propagate_transforms); | ||
|
||
let translation = vec3(1.0, 0.0, 0.0); | ||
|
||
|
@@ -313,6 +367,7 @@ mod test { | |
#[test] | ||
#[should_panic] | ||
fn panic_when_hierarchy_cycle() { | ||
ComputeTaskPool::init(TaskPool::default); | ||
// We cannot directly edit Parent and Children, so we use a temp world to break | ||
// the hierarchy's invariants. | ||
let mut temp = World::new(); | ||
|
@@ -321,7 +376,8 @@ mod test { | |
// FIXME: Parallel executors seem to have some odd interaction with the other | ||
// tests in this crate. Using single_threaded until a root cause can be found. | ||
app.add_stage("single", SystemStage::single_threaded()) | ||
.add_system_to_stage("single", transform_propagate_system); | ||
.add_system_to_stage("single", sync_simple_transforms) | ||
.add_system_to_stage("single", propagate_transforms); | ||
|
||
fn setup_world(world: &mut World) -> (Entity, Entity) { | ||
let mut grandchild = Entity::from_raw(0); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm skeptical that change detection actually helps here. Branching and checking only saves us a very simple operation, while introducing the potential for weird behavior.
Avoids false positives on change detection for GlobalTransform, but IMO we should consider just opting out of change detection for that anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing change detection bumps me from 180k to 190k birds at 60FPS on BevyMark. So, a modest improvement when everything is changed. Probably worth doing then, as in most scenes the overwhelming majority of things with transforms are static.
This PR seems to help that benchmark significantly in general, even comparing to latest main I'm getting +20k birbs.