-
-
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 7 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,74 +2,93 @@ 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>), | ||
>, | ||
) { | ||
for (transform, mut global_transform) in query.iter_mut() { | ||
*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 { | ||
changed |= children_changed; | ||
|
||
for child in children.iter() { | ||
let _ = 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 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) | ||
if let Ok(actual_parent) = parent_query.get(entity) { | ||
assert_eq!( | ||
child_parent.get(), expected_parent, | ||
actual_parent.get(), expected_parent, | ||
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" | ||
); | ||
} else { | ||
panic!("Propagated child for {:?} has no Parent component!", entity); | ||
} | ||
|
||
// SAFE: With the check that each child has one and only one parent, each child must be globally unique within the | ||
// hierarchy. Because of this, it is impossible for this query to have aliased mutable access to the same GlobalTransform. | ||
james7132 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Any malformed hierarchy will cause a panic due to the above check. | ||
let global_matrix = unsafe { | ||
let (transform, transform_changed, mut global_transform) = | ||
unsafe_transform_query.get_unchecked(entity).map_err(drop)?; | ||
james7132 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
changed |= transform_changed; | ||
if changed { | ||
*global_transform = parent.mul_transform(*transform); | ||
|
@@ -83,10 +102,11 @@ fn propagate_recursive( | |
for child in children { | ||
let _ = propagate_recursive( | ||
&global_matrix, | ||
transform_query, | ||
unsafe_transform_query, | ||
parent_query, | ||
children_query, | ||
*child, | ||
entity, | ||
*child, | ||
changed, | ||
); | ||
} | ||
|
@@ -99,9 +119,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 +131,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 +175,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 +217,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 +299,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 +342,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 +351,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.