use crate::components::{GlobalTransform, Transform, TransformTreeChanged};
use bevy_ecs::prelude::*;
#[cfg(feature = "std")]
pub use parallel::propagate_parent_transforms;
#[cfg(not(feature = "std"))]
pub use serial::propagate_parent_transforms;
pub fn sync_simple_transforms(
mut query: ParamSet<(
Query<
(&Transform, &mut GlobalTransform),
(
Or<(Changed<Transform>, Added<GlobalTransform>)>,
Without<ChildOf>,
Without<Children>,
),
>,
Query<(Ref<Transform>, &mut GlobalTransform), (Without<ChildOf>, Without<Children>)>,
)>,
mut orphaned: RemovedComponents<ChildOf>,
) {
query
.p0()
.par_iter_mut()
.for_each(|(transform, mut global_transform)| {
*global_transform = GlobalTransform::from(*transform);
});
let mut query = query.p1();
let mut iter = query.iter_many_mut(orphaned.read());
while let Some((transform, mut global_transform)) = iter.fetch_next() {
if !transform.is_changed() && !global_transform.is_added() {
*global_transform = GlobalTransform::from(*transform);
}
}
}
pub fn mark_dirty_trees(
changed_transforms: Query<
Entity,
Or<(Changed<Transform>, Changed<ChildOf>, Added<GlobalTransform>)>,
>,
mut orphaned: RemovedComponents<ChildOf>,
mut transforms: Query<(Option<&ChildOf>, &mut TransformTreeChanged)>,
) {
for entity in changed_transforms.iter().chain(orphaned.read()) {
let mut next = entity;
while let Ok((child_of, mut tree)) = transforms.get_mut(next) {
if tree.is_changed() && !tree.is_added() {
break;
}
tree.set_changed();
if let Some(parent) = child_of.map(ChildOf::parent) {
next = parent;
} else {
break;
};
}
}
}
#[cfg(not(feature = "std"))]
mod serial {
use crate::prelude::*;
use alloc::vec::Vec;
use bevy_ecs::prelude::*;
pub fn propagate_parent_transforms(
mut root_query: Query<
(Entity, &Children, Ref<Transform>, &mut GlobalTransform),
Without<ChildOf>,
>,
mut orphaned: RemovedComponents<ChildOf>,
transform_query: Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
With<ChildOf>,
>,
child_query: Query<(Entity, Ref<ChildOf>), With<GlobalTransform>>,
mut orphaned_entities: Local<Vec<Entity>>,
) {
orphaned_entities.clear();
orphaned_entities.extend(orphaned.read());
orphaned_entities.sort_unstable();
root_query.par_iter_mut().for_each(
|(entity, children, transform, mut global_transform)| {
let changed = transform.is_changed() || global_transform.is_added() || orphaned_entities.binary_search(&entity).is_ok();
if changed {
*global_transform = GlobalTransform::from(*transform);
}
for (child, child_of) in child_query.iter_many(children) {
assert_eq!(
child_of.parent(), entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
#[expect(unsafe_code, reason = "`propagate_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
unsafe {
propagate_recursive(
&global_transform,
&transform_query,
&child_query,
child,
changed || child_of.is_changed(),
);
}
}
},
);
}
#[expect(
unsafe_code,
reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
)]
unsafe fn propagate_recursive(
parent: &GlobalTransform,
transform_query: &Query<
(Ref<Transform>, &mut GlobalTransform, Option<&Children>),
With<ChildOf>,
>,
child_query: &Query<(Entity, Ref<ChildOf>), With<GlobalTransform>>,
entity: Entity,
mut changed: bool,
) {
let (global_matrix, children) = {
let Ok((transform, mut global_transform, children)) =
(unsafe { transform_query.get_unchecked(entity) }) else {
return;
};
changed |= transform.is_changed() || global_transform.is_added();
if changed {
*global_transform = parent.mul_transform(*transform);
}
(global_transform, children)
};
let Some(children) = children else { return };
for (child, child_of) in child_query.iter_many(children) {
assert_eq!(
child_of.parent(), entity,
"Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
);
unsafe {
propagate_recursive(
global_matrix.as_ref(),
transform_query,
child_query,
child,
changed || child_of.is_changed(),
);
}
}
}
}
#[cfg(feature = "std")]
mod parallel {
use crate::prelude::*;
use alloc::{sync::Arc, vec::Vec};
use bevy_ecs::{entity::UniqueEntityIter, prelude::*, system::lifetimeless::Read};
use bevy_tasks::{ComputeTaskPool, TaskPool};
use bevy_utils::Parallel;
use core::sync::atomic::{AtomicI32, Ordering};
use std::sync::{
mpsc::{Receiver, Sender},
Mutex,
};
pub fn propagate_parent_transforms(
mut queue: Local<WorkQueue>,
mut roots: Query<
(Entity, Ref<Transform>, &mut GlobalTransform, &Children),
(Without<ChildOf>, Changed<TransformTreeChanged>),
>,
nodes: NodeQuery,
) {
roots.par_iter_mut().for_each_init(
|| queue.local_queue.borrow_local_mut(),
|outbox, (parent, transform, mut parent_transform, children)| {
*parent_transform = GlobalTransform::from(*transform);
#[expect(unsafe_code, reason = "Mutating disjoint entities in parallel")]
unsafe {
propagate_descendants_unchecked(
parent,
parent_transform,
children,
&nodes,
outbox,
&queue,
1,
);
}
},
);
queue.send_batches();
if let Ok(rx) = queue.receiver.try_lock() {
if let Some(task) = rx.try_iter().next() {
queue.sender.send(task).ok();
} else {
return;
}
}
let task_pool = ComputeTaskPool::get_or_init(TaskPool::default);
task_pool.scope(|s| {
(1..task_pool.thread_num())
.for_each(|_| s.spawn(async { propagation_worker(&queue, &nodes) }));
propagation_worker(&queue, &nodes);
});
}
#[inline]
fn propagation_worker(queue: &WorkQueue, nodes: &NodeQuery) {
#[cfg(feature = "std")]
let _span = bevy_log::info_span!("transform propagation worker").entered();
let mut outbox = queue.local_queue.borrow_local_mut();
loop {
let Ok(rx) = queue.receiver.try_lock() else {
core::hint::spin_loop();
continue;
};
let Some(mut tasks) = rx.try_iter().next() else {
if queue.busy_threads.load(Ordering::Relaxed) == 0 {
break;
}
continue;
};
if tasks.is_empty() {
continue;
}
while tasks.len() < WorkQueue::CHUNK_SIZE / 2 {
let Some(mut extra_task) = rx.try_iter().next() else {
break;
};
tasks.append(&mut extra_task);
}
queue.busy_threads.fetch_add(1, Ordering::Relaxed);
drop(rx);
for parent in tasks.drain(..) {
#[expect(unsafe_code, reason = "Mutating disjoint entities in parallel")]
unsafe {
let (_, (_, p_global_transform, _), (p_children, _)) =
nodes.get_unchecked(parent).unwrap();
propagate_descendants_unchecked(
parent,
p_global_transform,
p_children.unwrap(),
nodes,
&mut outbox,
queue,
10_000,
);
}
}
WorkQueue::send_batches_with(&queue.sender, &mut outbox);
queue.busy_threads.fetch_add(-1, Ordering::Relaxed);
}
}
#[inline]
#[expect(unsafe_code, reason = "Mutating disjoint entities in parallel")]
unsafe fn propagate_descendants_unchecked(
parent: Entity,
p_global_transform: Mut<GlobalTransform>,
p_children: &Children,
nodes: &NodeQuery,
outbox: &mut Vec<Entity>,
queue: &WorkQueue,
max_depth: usize,
) {
let (mut parent, mut p_global_transform, mut p_children) =
(parent, p_global_transform, p_children);
for depth in 1..=max_depth {
#[expect(unsafe_code, reason = "Mutating disjoint entities in parallel")]
let children_iter = unsafe {
nodes.iter_many_unique_unsafe(UniqueEntityIter::from_iterator_unchecked(
p_children.iter(),
))
};
let mut last_child = None;
let new_children = children_iter.filter_map(
|(child, (transform, mut global_transform, tree), (children, child_of))| {
if !tree.is_changed() && !p_global_transform.is_changed() {
return None;
}
assert_eq!(child_of.parent(), parent);
global_transform.set_if_neq(p_global_transform.mul_transform(*transform));
children.map(|children| {
last_child = Some((child, global_transform, children));
child
})
},
);
outbox.extend(new_children);
if depth >= max_depth || last_child.is_none() {
break;
}
if let Some(last_child) = last_child {
(parent, p_global_transform, p_children) = last_child;
outbox.pop();
if outbox.len() >= WorkQueue::CHUNK_SIZE {
WorkQueue::send_batches_with(&queue.sender, outbox);
}
}
}
}
type NodeQuery<'w, 's> = Query<
'w,
's,
(
Entity,
(
Ref<'static, Transform>,
Mut<'static, GlobalTransform>,
Ref<'static, TransformTreeChanged>,
),
(Option<Read<Children>>, Read<ChildOf>),
),
>;
pub struct WorkQueue {
busy_threads: AtomicI32,
sender: Sender<Vec<Entity>>,
receiver: Arc<Mutex<Receiver<Vec<Entity>>>>,
local_queue: Parallel<Vec<Entity>>,
}
impl Default for WorkQueue {
fn default() -> Self {
let (tx, rx) = std::sync::mpsc::channel();
Self {
busy_threads: AtomicI32::default(),
sender: tx,
receiver: Arc::new(Mutex::new(rx)),
local_queue: Default::default(),
}
}
}
impl WorkQueue {
const CHUNK_SIZE: usize = 512;
#[inline]
fn send_batches_with(sender: &Sender<Vec<Entity>>, outbox: &mut Vec<Entity>) {
for chunk in outbox
.chunks(WorkQueue::CHUNK_SIZE)
.filter(|c| !c.is_empty())
{
sender.send(chunk.to_vec()).ok();
}
outbox.clear();
}
#[inline]
fn send_batches(&mut self) {
let Self {
sender,
local_queue,
..
} = self;
local_queue
.iter_mut()
.for_each(|outbox| Self::send_batches_with(sender, outbox));
}
}
}
#[cfg(test)]
mod test {
use alloc::{vec, vec::Vec};
use bevy_app::prelude::*;
use bevy_ecs::{prelude::*, world::CommandQueue};
use bevy_math::{vec3, Vec3};
use bevy_tasks::{ComputeTaskPool, TaskPool};
use crate::systems::*;
#[test]
fn correct_parent_removed() {
ComputeTaskPool::get_or_init(TaskPool::default);
let mut world = World::default();
let offset_global_transform =
|offset| GlobalTransform::from(Transform::from_xyz(offset, offset, offset));
let offset_transform = |offset| Transform::from_xyz(offset, offset, offset);
let mut schedule = Schedule::default();
schedule.add_systems(
(
mark_dirty_trees,
sync_simple_transforms,
propagate_parent_transforms,
)
.chain(),
);
let mut command_queue = CommandQueue::default();
let mut commands = Commands::new(&mut command_queue, &world);
let root = commands.spawn(offset_transform(3.3)).id();
let parent = commands.spawn(offset_transform(4.4)).id();
let child = commands.spawn(offset_transform(5.5)).id();
commands.entity(parent).insert(ChildOf(root));
commands.entity(child).insert(ChildOf(parent));
command_queue.apply(&mut world);
schedule.run(&mut world);
assert_eq!(
world.get::<GlobalTransform>(parent).unwrap(),
&offset_global_transform(4.4 + 3.3),
"The transform systems didn't run, ie: `GlobalTransform` wasn't updated",
);
let mut command_queue = CommandQueue::default();
let mut commands = Commands::new(&mut command_queue, &world);
commands.entity(parent).remove::<ChildOf>();
command_queue.apply(&mut world);
schedule.run(&mut world);
assert_eq!(
world.get::<GlobalTransform>(parent).unwrap(),
&offset_global_transform(4.4),
"The global transform of an orphaned entity wasn't updated properly",
);
let mut command_queue = CommandQueue::default();
let mut commands = Commands::new(&mut command_queue, &world);
commands.entity(child).remove::<ChildOf>();
command_queue.apply(&mut world);
schedule.run(&mut world);
assert_eq!(
world.get::<GlobalTransform>(child).unwrap(),
&offset_global_transform(5.5),
"The global transform of an orphaned entity wasn't updated properly",
);
}
#[test]
fn did_propagate() {
ComputeTaskPool::get_or_init(TaskPool::default);
let mut world = World::default();
let mut schedule = Schedule::default();
schedule.add_systems(
(
mark_dirty_trees,
sync_simple_transforms,
propagate_parent_transforms,
)
.chain(),
);
world.spawn(Transform::from_xyz(1.0, 0.0, 0.0));
let mut children = Vec::new();
world
.spawn(Transform::from_xyz(1.0, 0.0, 0.0))
.with_children(|parent| {
children.push(parent.spawn(Transform::from_xyz(0.0, 2.0, 0.)).id());
children.push(parent.spawn(Transform::from_xyz(0.0, 0.0, 3.)).id());
});
schedule.run(&mut world);
assert_eq!(
*world.get::<GlobalTransform>(children[0]).unwrap(),
GlobalTransform::from_xyz(1.0, 0.0, 0.0) * Transform::from_xyz(0.0, 2.0, 0.0)
);
assert_eq!(
*world.get::<GlobalTransform>(children[1]).unwrap(),
GlobalTransform::from_xyz(1.0, 0.0, 0.0) * Transform::from_xyz(0.0, 0.0, 3.0)
);
}
#[test]
fn did_propagate_command_buffer() {
let mut world = World::default();
let mut schedule = Schedule::default();
schedule.add_systems(
(
mark_dirty_trees,
sync_simple_transforms,
propagate_parent_transforms,
)
.chain(),
);
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &world);
let mut children = Vec::new();
commands
.spawn(Transform::from_xyz(1.0, 0.0, 0.0))
.with_children(|parent| {
children.push(parent.spawn(Transform::from_xyz(0.0, 2.0, 0.0)).id());
children.push(parent.spawn(Transform::from_xyz(0.0, 0.0, 3.0)).id());
});
queue.apply(&mut world);
schedule.run(&mut world);
assert_eq!(
*world.get::<GlobalTransform>(children[0]).unwrap(),
GlobalTransform::from_xyz(1.0, 0.0, 0.0) * Transform::from_xyz(0.0, 2.0, 0.0)
);
assert_eq!(
*world.get::<GlobalTransform>(children[1]).unwrap(),
GlobalTransform::from_xyz(1.0, 0.0, 0.0) * Transform::from_xyz(0.0, 0.0, 3.0)
);
}
#[test]
fn correct_children() {
ComputeTaskPool::get_or_init(TaskPool::default);
let mut world = World::default();
let mut schedule = Schedule::default();
schedule.add_systems(
(
mark_dirty_trees,
sync_simple_transforms,
propagate_parent_transforms,
)
.chain(),
);
let mut children = Vec::new();
let parent = {
let mut command_queue = CommandQueue::default();
let mut commands = Commands::new(&mut command_queue, &world);
let parent = commands.spawn(Transform::from_xyz(1.0, 0.0, 0.0)).id();
commands.entity(parent).with_children(|parent| {
children.push(parent.spawn(Transform::from_xyz(0.0, 2.0, 0.0)).id());
children.push(parent.spawn(Transform::from_xyz(0.0, 3.0, 0.0)).id());
});
command_queue.apply(&mut world);
schedule.run(&mut world);
parent
};
assert_eq!(
world
.get::<Children>(parent)
.unwrap()
.iter()
.collect::<Vec<_>>(),
children,
);
{
let mut command_queue = CommandQueue::default();
let mut commands = Commands::new(&mut command_queue, &world);
commands.entity(children[1]).add_child(children[0]);
command_queue.apply(&mut world);
schedule.run(&mut world);
}
assert_eq!(
world
.get::<Children>(parent)
.unwrap()
.iter()
.collect::<Vec<_>>(),
vec![children[1]]
);
assert_eq!(
world
.get::<Children>(children[1])
.unwrap()
.iter()
.collect::<Vec<_>>(),
vec![children[0]]
);
assert!(world.despawn(children[0]));
schedule.run(&mut world);
assert_eq!(
world
.get::<Children>(parent)
.unwrap()
.iter()
.collect::<Vec<_>>(),
vec![children[1]]
);
}
#[test]
fn correct_transforms_when_no_children() {
let mut app = App::new();
ComputeTaskPool::get_or_init(TaskPool::default);
app.add_systems(
Update,
(
mark_dirty_trees,
sync_simple_transforms,
propagate_parent_transforms,
)
.chain(),
);
let translation = vec3(1.0, 0.0, 0.0);
let mut child = Entity::from_raw_u32(0).unwrap();
let mut grandchild = Entity::from_raw_u32(1).unwrap();
let parent = app
.world_mut()
.spawn(Transform::from_translation(translation))
.with_children(|builder| {
child = builder
.spawn(Transform::IDENTITY)
.with_children(|builder| {
grandchild = builder.spawn(Transform::IDENTITY).id();
})
.id();
})
.id();
app.update();
assert_eq!(&**app.world().get::<Children>(parent).unwrap(), &[child]);
assert_eq!(
&**app.world().get::<Children>(child).unwrap(),
&[grandchild]
);
app.update();
let mut state = app.world_mut().query::<&GlobalTransform>();
for global in state.iter(app.world()) {
assert_eq!(global, &GlobalTransform::from_translation(translation));
}
}
#[test]
#[should_panic]
fn panic_when_hierarchy_cycle() {
ComputeTaskPool::get_or_init(TaskPool::default);
let mut temp = World::new();
let mut app = App::new();
app.add_systems(
Update,
propagate_parent_transforms,
);
fn setup_world(world: &mut World) -> (Entity, Entity) {
let mut grandchild = Entity::from_raw_u32(0).unwrap();
let child = world
.spawn(Transform::IDENTITY)
.with_children(|builder| {
grandchild = builder.spawn(Transform::IDENTITY).id();
})
.id();
(child, grandchild)
}
let (temp_child, temp_grandchild) = setup_world(&mut temp);
let (child, grandchild) = setup_world(app.world_mut());
assert_eq!(temp_child, child);
assert_eq!(temp_grandchild, grandchild);
app.world_mut()
.spawn(Transform::IDENTITY)
.add_children(&[child]);
let mut child_entity = app.world_mut().entity_mut(child);
let mut grandchild_entity = temp.entity_mut(grandchild);
#[expect(
unsafe_code,
reason = "ChildOf is not mutable but this is for a test to produce a scenario that cannot happen"
)]
let mut a = unsafe { child_entity.get_mut_assume_mutable::<ChildOf>().unwrap() };
#[expect(
unsafe_code,
reason = "ChildOf is not mutable but this is for a test to produce a scenario that cannot happen"
)]
let mut b = unsafe {
grandchild_entity
.get_mut_assume_mutable::<ChildOf>()
.unwrap()
};
core::mem::swap(a.as_mut(), b.as_mut());
app.update();
}
#[test]
fn global_transform_should_not_be_overwritten_after_reparenting() {
let translation = Vec3::ONE;
let mut world = World::new();
let mut schedule = Schedule::default();
schedule.add_systems(
(
mark_dirty_trees,
propagate_parent_transforms,
sync_simple_transforms,
)
.chain(),
);
let mut spawn_transform_bundle =
|| world.spawn(Transform::from_translation(translation)).id();
let parent = spawn_transform_bundle();
let child = spawn_transform_bundle();
world.entity_mut(parent).add_child(child);
schedule.run(&mut world);
let parent_global_transform = *world.entity(parent).get::<GlobalTransform>().unwrap();
let child_global_transform = *world.entity(child).get::<GlobalTransform>().unwrap();
assert!(parent_global_transform
.translation()
.abs_diff_eq(translation, 0.1));
assert!(child_global_transform
.translation()
.abs_diff_eq(2. * translation, 0.1));
world.entity_mut(child).remove::<ChildOf>();
world.entity_mut(parent).add_child(child);
schedule.run(&mut world);
assert_eq!(
parent_global_transform,
*world.entity(parent).get::<GlobalTransform>().unwrap()
);
assert_eq!(
child_global_transform,
*world.entity(child).get::<GlobalTransform>().unwrap()
);
}
}