bevy_transform/systems.rs
1use crate::components::{GlobalTransform, Transform, TransformTreeChanged};
2use bevy_ecs::prelude::*;
3
4#[cfg(feature = "std")]
5pub use parallel::propagate_parent_transforms;
6#[cfg(not(feature = "std"))]
7pub use serial::propagate_parent_transforms;
8
9/// Update [`GlobalTransform`] component of entities that aren't in the hierarchy
10///
11/// Third party plugins should ensure that this is used in concert with
12/// [`propagate_parent_transforms`] and [`mark_dirty_trees`].
13pub fn sync_simple_transforms(
14 mut query: ParamSet<(
15 Query<
16 (&Transform, &mut GlobalTransform),
17 (
18 Or<(Changed<Transform>, Added<GlobalTransform>)>,
19 Without<ChildOf>,
20 Without<Children>,
21 ),
22 >,
23 Query<(Ref<Transform>, &mut GlobalTransform), (Without<ChildOf>, Without<Children>)>,
24 )>,
25 mut orphaned: RemovedComponents<ChildOf>,
26) {
27 // Update changed entities.
28 query
29 .p0()
30 .par_iter_mut()
31 .for_each(|(transform, mut global_transform)| {
32 *global_transform = GlobalTransform::from(*transform);
33 });
34 // Update orphaned entities.
35 let mut query = query.p1();
36 let mut iter = query.iter_many_mut(orphaned.read());
37 while let Some((transform, mut global_transform)) = iter.fetch_next() {
38 if !transform.is_changed() && !global_transform.is_added() {
39 *global_transform = GlobalTransform::from(*transform);
40 }
41 }
42}
43
44/// Configure the behavior of static scene optimizations for [`Transform`] propagation.
45///
46/// For scenes with many static entities, it is much faster to track trees of unchanged
47/// [`Transform`]s and skip these during the expensive transform propagation step. If your scene is
48/// very dynamic, the cost of tracking these trees can exceed the performance benefits. By default,
49/// static scene optimization is disabled for worlds with more than 30% of its entities moving.
50///
51/// This resource allows you to configure that threshold at runtime.
52#[derive(Resource, Debug)]
53#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
54pub struct StaticTransformOptimizations {
55 /// If the percentage of moving objects exceeds this value, skip dirty tree marking.
56 threshold: f32,
57 /// Updated every frame by [`mark_dirty_trees`].
58 enabled: bool,
59}
60
61impl StaticTransformOptimizations {
62 /// If the percentage of moving objects exceeds this threshold, disable static [`Transform`]
63 /// optimizations. This is done because the scene is so dynamic that the cost of tracking static
64 /// trees exceeds the performance benefit of skipping propagation for these trees.
65 ///
66 /// - Setting this to `0.0` will result in never running static scene tracking.
67 /// - Setting this to `1.0` will result in always tracking static transform trees.
68 pub fn from_threshold(threshold: f32) -> Self {
69 Self {
70 threshold,
71 enabled: true,
72 }
73 }
74
75 /// Unconditionally disable static scene optimizations.
76 pub fn disabled() -> Self {
77 Self {
78 threshold: 0.0,
79 enabled: false,
80 }
81 }
82
83 /// Unconditionally enable static scene optimizations.
84 pub fn enabled() -> Self {
85 Self {
86 threshold: 1.0,
87 enabled: true,
88 }
89 }
90}
91
92impl Default for StaticTransformOptimizations {
93 fn default() -> Self {
94 Self {
95 // Scenes with more than 30% moving objects are considered dynamic enough to skip static
96 // optimizations.
97 threshold: 0.3,
98 enabled: true,
99 }
100 }
101}
102
103/// Optimization for static scenes.
104///
105/// Propagates a "dirty bit" up the hierarchy towards ancestors. Transform propagation can ignore
106/// entire subtrees of the hierarchy if it encounters an entity without the dirty bit.
107///
108/// Configure behavior with [`StaticTransformOptimizations`].
109pub fn mark_dirty_trees(
110 changed_transforms: Query<
111 Entity,
112 Or<(Changed<Transform>, Changed<ChildOf>, Added<GlobalTransform>)>,
113 >,
114 mut orphaned: RemovedComponents<ChildOf>,
115 mut transforms: Query<&mut TransformTreeChanged>,
116 parents: Query<&ChildOf>,
117 mut static_optimizations: ResMut<StaticTransformOptimizations>,
118) {
119 let threshold = static_optimizations.threshold.clamp(0.0, 1.0);
120 match threshold {
121 0.0 => static_optimizations.enabled = false,
122 1.0 => static_optimizations.enabled = true,
123 _ => {
124 static_optimizations.enabled = true;
125 let n_dyn = changed_transforms.count() as f32;
126 let total = transforms.count() as f32;
127 if n_dyn / total > threshold {
128 static_optimizations.enabled = false;
129 }
130 }
131 }
132 if !static_optimizations.enabled {
133 return;
134 }
135
136 for entity in changed_transforms.iter().chain(orphaned.read()) {
137 let mut next = entity;
138 while let Ok(mut tree) = transforms.get_mut(next) {
139 if tree.is_changed() && !tree.is_added() {
140 // If the component was changed, this part of the tree has already been processed.
141 // Ignore this if the change was caused by the component being added.
142 break;
143 }
144 tree.set_changed();
145 if let Ok(parent) = parents.get(next).map(ChildOf::parent) {
146 next = parent;
147 } else {
148 break;
149 };
150 }
151 }
152}
153
154// TODO: This serial implementation isn't actually serial, it parallelizes across the roots.
155// Additionally, this couples "no_std" with "single_threaded" when these two features should be
156// independent.
157//
158// What we want to do in a future refactor is take the current "single threaded" implementation, and
159// actually make it single threaded. This will remove any overhead associated with working on a task
160// pool when you only have a single thread, and will have the benefit of removing the need for any
161// unsafe. We would then make the multithreaded implementation work across std and no_std, but this
162// is blocked a no_std compatible Channel, which is why this TODO is not yet implemented.
163//
164// This complexity might also not be needed. If the multithreaded implementation on a single thread
165// is as fast as the single threaded implementation, we could simply remove the entire serial
166// module, and make the multithreaded module no_std compatible.
167//
168/// Serial hierarchy traversal. Useful in `no_std` or single threaded contexts.
169#[cfg(not(feature = "std"))]
170mod serial {
171 use crate::prelude::*;
172 use alloc::vec::Vec;
173 use bevy_ecs::prelude::*;
174
175 /// Update [`GlobalTransform`] component of entities based on entity hierarchy and [`Transform`]
176 /// component.
177 ///
178 /// Third party plugins should ensure that this is used in concert with
179 /// [`sync_simple_transforms`](super::sync_simple_transforms) and
180 /// [`mark_dirty_trees`](super::mark_dirty_trees).
181 pub fn propagate_parent_transforms(
182 mut root_query: Query<
183 (Entity, &Children, Ref<Transform>, &mut GlobalTransform),
184 Without<ChildOf>,
185 >,
186 mut orphaned: RemovedComponents<ChildOf>,
187 transform_query: Query<
188 (Ref<Transform>, &mut GlobalTransform, Option<&Children>),
189 With<ChildOf>,
190 >,
191 child_query: Query<(Entity, Ref<ChildOf>), With<GlobalTransform>>,
192 mut orphaned_entities: Local<Vec<Entity>>,
193 ) {
194 orphaned_entities.clear();
195 orphaned_entities.extend(orphaned.read());
196 orphaned_entities.sort_unstable();
197 root_query.par_iter_mut().for_each(
198 |(entity, children, transform, mut global_transform)| {
199 let changed = transform.is_changed() || global_transform.is_added() || orphaned_entities.binary_search(&entity).is_ok();
200 if changed {
201 *global_transform = GlobalTransform::from(*transform);
202 }
203
204 for (child, child_of) in child_query.iter_many(children) {
205 assert_eq!(
206 child_of.parent(), entity,
207 "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
208 );
209 // SAFETY:
210 // - `child` must have consistent parentage, or the above assertion would panic.
211 // Since `child` is parented to a root entity, the entire hierarchy leading to it
212 // is consistent.
213 // - We may operate as if all descendants are consistent, since
214 // `propagate_recursive` will panic before continuing to propagate if it
215 // encounters an entity with inconsistent parentage.
216 // - Since each root entity is unique and the hierarchy is consistent and
217 // forest-like, other root entities' `propagate_recursive` calls will not conflict
218 // with this one.
219 // - Since this is the only place where `transform_query` gets used, there will be
220 // no conflicting fetches elsewhere.
221 #[expect(unsafe_code, reason = "`propagate_recursive()` is unsafe due to its use of `Query::get_unchecked()`.")]
222 unsafe {
223 propagate_recursive(
224 &global_transform,
225 &transform_query,
226 &child_query,
227 child,
228 changed || child_of.is_changed(),
229 );
230 }
231 }
232 },
233 );
234 }
235
236 /// Recursively propagates the transforms for `entity` and all of its descendants.
237 ///
238 /// # Panics
239 ///
240 /// If `entity`'s descendants have a malformed hierarchy, this function will panic occur before
241 /// propagating the transforms of any malformed entities and their descendants.
242 ///
243 /// # Safety
244 ///
245 /// - While this function is running, `transform_query` must not have any fetches for `entity`,
246 /// nor any of its descendants.
247 /// - The caller must ensure that the hierarchy leading to `entity` is well-formed and must
248 /// remain as a tree or a forest. Each entity must have at most one parent.
249 #[expect(
250 unsafe_code,
251 reason = "This function uses `Query::get_unchecked()`, which can result in multiple mutable references if the preconditions are not met."
252 )]
253 unsafe fn propagate_recursive(
254 parent: &GlobalTransform,
255 transform_query: &Query<
256 (Ref<Transform>, &mut GlobalTransform, Option<&Children>),
257 With<ChildOf>,
258 >,
259 child_query: &Query<(Entity, Ref<ChildOf>), With<GlobalTransform>>,
260 entity: Entity,
261 mut changed: bool,
262 ) {
263 let (global_matrix, children) = {
264 let Ok((transform, mut global_transform, children)) =
265 // SAFETY: This call cannot create aliased mutable references.
266 // - The top level iteration parallelizes on the roots of the hierarchy.
267 // - The caller ensures that each child has one and only one unique parent throughout
268 // the entire hierarchy.
269 //
270 // For example, consider the following malformed hierarchy:
271 //
272 // A
273 // / \
274 // B C
275 // \ /
276 // D
277 //
278 // D has two parents, B and C. If the propagation passes through C, but the ChildOf
279 // component on D points to B, the above check will panic as the origin parent does
280 // match the recorded parent.
281 //
282 // Also consider the following case, where A and B are roots:
283 //
284 // A B
285 // \ /
286 // C D
287 // \ /
288 // E
289 //
290 // Even if these A and B start two separate tasks running in parallel, one of them will
291 // panic before attempting to mutably access E.
292 (unsafe { transform_query.get_unchecked(entity) }) else {
293 return;
294 };
295
296 changed |= transform.is_changed() || global_transform.is_added();
297 if changed {
298 *global_transform = parent.mul_transform(*transform);
299 }
300 (global_transform, children)
301 };
302
303 let Some(children) = children else { return };
304 for (child, child_of) in child_query.iter_many(children) {
305 assert_eq!(
306 child_of.parent(), entity,
307 "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle"
308 );
309 // SAFETY: The caller guarantees that `transform_query` will not be fetched for any
310 // descendants of `entity`, so it is safe to call `propagate_recursive` for each child.
311 //
312 // The above assertion ensures that each child has one and only one unique parent
313 // throughout the entire hierarchy.
314 unsafe {
315 propagate_recursive(
316 global_matrix.as_ref(),
317 transform_query,
318 child_query,
319 child,
320 changed || child_of.is_changed(),
321 );
322 }
323 }
324 }
325}
326
327// TODO: Relies on `std` until a `no_std` `mpsc` channel is available.
328//
329/// Parallel hierarchy traversal with a batched work sharing scheduler. Often 2-5 times faster than
330/// the serial version.
331#[cfg(feature = "std")]
332mod parallel {
333 use crate::prelude::*;
334 // TODO: this implementation could be used in no_std if there are equivalents of these.
335 use crate::systems::StaticTransformOptimizations;
336 use alloc::{sync::Arc, vec::Vec};
337 use bevy_ecs::{entity::UniqueEntityIter, prelude::*, system::lifetimeless::Read};
338 use bevy_tasks::{ComputeTaskPool, TaskPool};
339 use bevy_utils::Parallel;
340 use core::sync::atomic::{AtomicI32, Ordering};
341 use std::sync::{
342 mpsc::{Receiver, Sender},
343 Mutex,
344 };
345
346 /// Update [`GlobalTransform`] component of entities based on entity hierarchy and [`Transform`]
347 /// component.
348 ///
349 /// Third party plugins should ensure that this is used in concert with
350 /// [`sync_simple_transforms`](super::sync_simple_transforms) and
351 /// [`mark_dirty_trees`](super::mark_dirty_trees).
352 pub fn propagate_parent_transforms(
353 mut queue: Local<WorkQueue>,
354 mut roots: Query<
355 (
356 Entity,
357 Ref<Transform>,
358 &mut GlobalTransform,
359 &Children,
360 Ref<TransformTreeChanged>,
361 ),
362 Without<ChildOf>,
363 >,
364 nodes: NodeQuery,
365 static_optimizations: Res<StaticTransformOptimizations>,
366 ) {
367 // Process roots in parallel, seeding the work queue
368 roots.par_iter_mut().for_each_init(
369 || queue.local_queue.borrow_local_mut(),
370 |outbox, (parent, transform, mut parent_transform, children, transform_tree)| {
371 if static_optimizations.enabled && !transform_tree.is_changed() {
372 // Early exit if the subtree is static and the optimization is enabled.
373 return;
374 }
375
376 *parent_transform = GlobalTransform::from(*transform);
377
378 // SAFETY: the parent entities passed into this function are taken from iterating
379 // over the root entity query. Queries iterate over disjoint entities, preventing
380 // mutable aliasing, and making this call safe.
381 #[expect(unsafe_code, reason = "Mutating disjoint entities in parallel")]
382 unsafe {
383 propagate_descendants_unchecked(
384 parent,
385 parent_transform,
386 children,
387 &nodes,
388 outbox,
389 &queue,
390 &static_optimizations,
391 // Need to revisit this single-max-depth by profiling more representative
392 // scenes. It's possible that it is actually beneficial to go deep into the
393 // hierarchy to build up a good task queue before starting the workers.
394 // However, we avoid this for now to prevent cases where only a single
395 // thread is going deep into the hierarchy while the others sit idle, which
396 // is the problem that the tasks sharing workers already solve.
397 1,
398 );
399 }
400 },
401 );
402 // Send all tasks in thread local outboxes *after* roots are processed to reduce the total
403 // number of channel sends by avoiding sending partial batches.
404 queue.send_batches();
405
406 if let Ok(rx) = queue.receiver.try_lock() {
407 if let Some(task) = rx.try_iter().next() {
408 // This is a bit silly, but the only way to see if there is any work is to grab a
409 // task. Peeking will remove the task even if you don't call `next`, resulting in
410 // dropping a task. What we do here is grab the first task if there is one, then
411 // immediately send it to the back of the queue.
412 queue.sender.send(task).ok();
413 } else {
414 return; // No work, don't bother spawning any tasks
415 }
416 }
417
418 // Spawn workers on the task pool to recursively propagate the hierarchy in parallel.
419 let task_pool = ComputeTaskPool::get_or_init(TaskPool::default);
420 task_pool.scope(|s| {
421 (1..task_pool.thread_num()) // First worker is run locally instead of the task pool.
422 .for_each(|_| {
423 s.spawn(async { propagation_worker(&queue, &nodes, &static_optimizations) });
424 });
425 propagation_worker(&queue, &nodes, &static_optimizations);
426 });
427 }
428
429 /// A parallel worker that will consume processed parent entities from the queue, and push
430 /// children to the queue once it has propagated their [`GlobalTransform`].
431 #[inline]
432 fn propagation_worker(
433 queue: &WorkQueue,
434 nodes: &NodeQuery,
435 static_optimizations: &StaticTransformOptimizations,
436 ) {
437 #[cfg(feature = "std")]
438 let _span = bevy_log::info_span!("transform propagation worker").entered();
439
440 let mut outbox = queue.local_queue.borrow_local_mut();
441 loop {
442 // Try to acquire a lock on the work queue in a tight loop. Profiling shows this is much
443 // more efficient than relying on `.lock()`, which causes gaps to form between tasks.
444 let Ok(rx) = queue.receiver.try_lock() else {
445 core::hint::spin_loop(); // No apparent impact on profiles, but best practice.
446 continue;
447 };
448 // If the queue is empty and no other threads are busy processing work, we can conclude
449 // there is no more work to do, and end the task by exiting the loop.
450 let Some(mut tasks) = rx.try_iter().next() else {
451 if queue.busy_threads.load(Ordering::Relaxed) == 0 {
452 break; // All work is complete, kill the worker
453 }
454 continue; // No work to do now, but another thread is busy creating more work.
455 };
456 if tasks.is_empty() {
457 continue; // This shouldn't happen, but if it does, we might as well stop early.
458 }
459
460 // If the task queue is extremely short, it's worthwhile to gather a few more tasks to
461 // reduce the amount of thread synchronization needed once this very short task is
462 // complete.
463 while tasks.len() < WorkQueue::CHUNK_SIZE / 2 {
464 let Some(mut extra_task) = rx.try_iter().next() else {
465 break;
466 };
467 tasks.append(&mut extra_task);
468 }
469
470 // At this point, we know there is work to do, so we increment the busy thread counter,
471 // and drop the mutex guard *after* we have incremented the counter. This ensures that
472 // if another thread is able to acquire a lock, the busy thread counter will already be
473 // incremented.
474 queue.busy_threads.fetch_add(1, Ordering::Relaxed);
475 drop(rx); // Important: drop after atomic and before work starts.
476
477 for parent in tasks.drain(..) {
478 // SAFETY: each task pushed to the worker queue represents an unprocessed subtree of
479 // the hierarchy, guaranteeing unique access.
480 #[expect(unsafe_code, reason = "Mutating disjoint entities in parallel")]
481 unsafe {
482 let (_, (_, p_global_transform, _), (p_children, _)) =
483 nodes.get_unchecked(parent).unwrap();
484 propagate_descendants_unchecked(
485 parent,
486 p_global_transform,
487 p_children.unwrap(), // All entities in the queue should have children
488 nodes,
489 &mut outbox,
490 queue,
491 static_optimizations,
492 // Only affects performance. Trees deeper than this will still be fully
493 // propagated, but the work will be broken into multiple tasks. This number
494 // was chosen to be larger than any reasonable tree depth, while not being
495 // so large the function could hang on a deep hierarchy.
496 10_000,
497 );
498 }
499 }
500 WorkQueue::send_batches_with(&queue.sender, &mut outbox);
501 queue.busy_threads.fetch_add(-1, Ordering::Relaxed);
502 }
503 }
504
505 /// Propagate transforms from `parent` to its `children`, pushing updated child entities to the
506 /// `outbox`. This function will continue propagating transforms to descendants in a depth-first
507 /// traversal, while simultaneously pushing unvisited branches to the outbox, for other threads
508 /// to take when idle.
509 ///
510 /// # Safety
511 ///
512 /// Callers must ensure that concurrent calls to this function are given unique `parent`
513 /// entities. Calling this function concurrently with the same `parent` is unsound. This
514 /// function will validate that the entity hierarchy does not contain cycles to prevent mutable
515 /// aliasing during propagation, but it is unable to verify that it isn't being used to mutably
516 /// alias the same entity.
517 ///
518 /// ## Panics
519 ///
520 /// Panics if the parent of a child node is not the same as the supplied `parent`. This
521 /// assertion ensures that the hierarchy is acyclic, which in turn ensures that if the caller is
522 /// following the supplied safety rules, multi-threaded propagation is sound.
523 #[inline]
524 #[expect(unsafe_code, reason = "Mutating disjoint entities in parallel")]
525 unsafe fn propagate_descendants_unchecked(
526 parent: Entity,
527 p_global_transform: Mut<GlobalTransform>,
528 p_children: &Children,
529 nodes: &NodeQuery,
530 outbox: &mut Vec<Entity>,
531 queue: &WorkQueue,
532 static_optimizations: &StaticTransformOptimizations,
533 max_depth: usize,
534 ) {
535 // Create mutable copies of the input variables, used for iterative depth-first traversal.
536 let (mut parent, mut p_global_transform, mut p_children) =
537 (parent, p_global_transform, p_children);
538
539 // See the optimization note at the end to understand why this loop is here.
540 for depth in 1..=max_depth {
541 // Safety: traversing the entity tree from the roots, we assert that the childof and
542 // children pointers match in both directions (see assert below) to ensure the hierarchy
543 // does not have any cycles. Because the hierarchy does not have cycles, we know we are
544 // visiting disjoint entities in parallel, which is safe.
545 #[expect(unsafe_code, reason = "Mutating disjoint entities in parallel")]
546 let children_iter = unsafe {
547 nodes.iter_many_unique_unsafe(UniqueEntityIter::from_iterator_unchecked(
548 p_children.iter(),
549 ))
550 };
551
552 let mut last_child = None;
553 let new_children = children_iter.filter_map(
554 |(child, (transform, mut global_transform, tree), (children, child_of))| {
555 if static_optimizations.enabled
556 && !tree.is_changed()
557 && !p_global_transform.is_changed()
558 {
559 // Static scene optimization
560 return None;
561 }
562 assert_eq!(child_of.parent(), parent);
563
564 // Transform prop is expensive - this helps avoid updating entire subtrees if
565 // the GlobalTransform is unchanged, at the cost of an added equality check.
566 global_transform.set_if_neq(p_global_transform.mul_transform(*transform));
567
568 children.map(|children| {
569 // Only continue propagation if the entity has children.
570 last_child = Some((child, global_transform, children));
571 child
572 })
573 },
574 );
575 outbox.extend(new_children);
576
577 if depth >= max_depth || last_child.is_none() {
578 break; // Don't remove anything from the outbox or send any chunks, just exit.
579 }
580
581 // Optimization: tasks should consume work locally as long as they can to avoid
582 // thread synchronization for as long as possible.
583 if let Some(last_child) = last_child {
584 // Overwrite parent data with children, and loop to iterate through descendants.
585 (parent, p_global_transform, p_children) = last_child;
586 outbox.pop();
587
588 // Send chunks during traversal. This allows sharing tasks with other threads before
589 // fully completing the traversal.
590 if outbox.len() >= WorkQueue::CHUNK_SIZE {
591 WorkQueue::send_batches_with(&queue.sender, outbox);
592 }
593 }
594 }
595 }
596
597 /// Alias for a large, repeatedly used query. Queries for transform entities that have both a
598 /// parent and possibly children, thus they are not roots.
599 type NodeQuery<'w, 's> = Query<
600 'w,
601 's,
602 (
603 Entity,
604 (
605 Ref<'static, Transform>,
606 Mut<'static, GlobalTransform>,
607 Ref<'static, TransformTreeChanged>,
608 ),
609 (Option<Read<Children>>, Read<ChildOf>),
610 ),
611 >;
612
613 /// A queue shared between threads for transform propagation.
614 pub struct WorkQueue {
615 /// A semaphore that tracks how many threads are busy doing work. Used to determine when
616 /// there is no more work to do.
617 busy_threads: AtomicI32,
618 sender: Sender<Vec<Entity>>,
619 receiver: Arc<Mutex<Receiver<Vec<Entity>>>>,
620 local_queue: Parallel<Vec<Entity>>,
621 }
622 impl Default for WorkQueue {
623 fn default() -> Self {
624 let (tx, rx) = std::sync::mpsc::channel();
625 Self {
626 busy_threads: AtomicI32::default(),
627 sender: tx,
628 receiver: Arc::new(Mutex::new(rx)),
629 local_queue: Default::default(),
630 }
631 }
632 }
633 impl WorkQueue {
634 const CHUNK_SIZE: usize = 512;
635
636 #[inline]
637 fn send_batches_with(sender: &Sender<Vec<Entity>>, outbox: &mut Vec<Entity>) {
638 for chunk in outbox
639 .chunks(WorkQueue::CHUNK_SIZE)
640 .filter(|c| !c.is_empty())
641 {
642 sender.send(chunk.to_vec()).ok();
643 }
644 outbox.clear();
645 }
646
647 #[inline]
648 fn send_batches(&mut self) {
649 let Self {
650 sender,
651 local_queue,
652 ..
653 } = self;
654 // Iterate over the locals to send batched tasks, avoiding the need to drain the locals
655 // into a larger allocation.
656 local_queue
657 .iter_mut()
658 .for_each(|outbox| Self::send_batches_with(sender, outbox));
659 }
660 }
661}
662
663#[cfg(test)]
664mod test {
665 use alloc::{vec, vec::Vec};
666 use bevy_app::prelude::*;
667 use bevy_ecs::{prelude::*, world::CommandQueue};
668 use bevy_math::{vec3, Vec3};
669 use bevy_tasks::{ComputeTaskPool, TaskPool};
670
671 use crate::systems::*;
672
673 #[test]
674 fn correct_parent_removed() {
675 ComputeTaskPool::get_or_init(TaskPool::default);
676 let mut world = World::default();
677 let offset_global_transform =
678 |offset| GlobalTransform::from(Transform::from_xyz(offset, offset, offset));
679 let offset_transform = |offset| Transform::from_xyz(offset, offset, offset);
680
681 let mut schedule = Schedule::default();
682 schedule.add_systems(
683 (
684 mark_dirty_trees,
685 sync_simple_transforms,
686 propagate_parent_transforms,
687 )
688 .chain(),
689 );
690 world.insert_resource(StaticTransformOptimizations::default());
691
692 let mut command_queue = CommandQueue::default();
693 let mut commands = Commands::new(&mut command_queue, &world);
694 let root = commands.spawn(offset_transform(3.3)).id();
695 let parent = commands.spawn(offset_transform(4.4)).id();
696 let child = commands.spawn(offset_transform(5.5)).id();
697 commands.entity(parent).insert(ChildOf(root));
698 commands.entity(child).insert(ChildOf(parent));
699 command_queue.apply(&mut world);
700 schedule.run(&mut world);
701
702 assert_eq!(
703 world.get::<GlobalTransform>(parent).unwrap(),
704 &offset_global_transform(4.4 + 3.3),
705 "The transform systems didn't run, ie: `GlobalTransform` wasn't updated",
706 );
707
708 // Remove parent of `parent`
709 let mut command_queue = CommandQueue::default();
710 let mut commands = Commands::new(&mut command_queue, &world);
711 commands.entity(parent).remove::<ChildOf>();
712 command_queue.apply(&mut world);
713 schedule.run(&mut world);
714
715 assert_eq!(
716 world.get::<GlobalTransform>(parent).unwrap(),
717 &offset_global_transform(4.4),
718 "The global transform of an orphaned entity wasn't updated properly",
719 );
720
721 // Remove parent of `child`
722 let mut command_queue = CommandQueue::default();
723 let mut commands = Commands::new(&mut command_queue, &world);
724 commands.entity(child).remove::<ChildOf>();
725 command_queue.apply(&mut world);
726 schedule.run(&mut world);
727
728 assert_eq!(
729 world.get::<GlobalTransform>(child).unwrap(),
730 &offset_global_transform(5.5),
731 "The global transform of an orphaned entity wasn't updated properly",
732 );
733 }
734
735 #[test]
736 fn did_propagate() {
737 ComputeTaskPool::get_or_init(TaskPool::default);
738 let mut world = World::default();
739
740 let mut schedule = Schedule::default();
741 schedule.add_systems(
742 (
743 mark_dirty_trees,
744 sync_simple_transforms,
745 propagate_parent_transforms,
746 )
747 .chain(),
748 );
749 world.insert_resource(StaticTransformOptimizations::default());
750
751 // Root entity
752 world.spawn(Transform::from_xyz(1.0, 0.0, 0.0));
753
754 let mut children = Vec::new();
755 world
756 .spawn(Transform::from_xyz(1.0, 0.0, 0.0))
757 .with_children(|parent| {
758 children.push(parent.spawn(Transform::from_xyz(0.0, 2.0, 0.)).id());
759 children.push(parent.spawn(Transform::from_xyz(0.0, 0.0, 3.)).id());
760 });
761 schedule.run(&mut world);
762
763 assert_eq!(
764 *world.get::<GlobalTransform>(children[0]).unwrap(),
765 GlobalTransform::from_xyz(1.0, 0.0, 0.0) * Transform::from_xyz(0.0, 2.0, 0.0)
766 );
767
768 assert_eq!(
769 *world.get::<GlobalTransform>(children[1]).unwrap(),
770 GlobalTransform::from_xyz(1.0, 0.0, 0.0) * Transform::from_xyz(0.0, 0.0, 3.0)
771 );
772 }
773
774 #[test]
775 fn did_propagate_command_buffer() {
776 let mut world = World::default();
777
778 let mut schedule = Schedule::default();
779 schedule.add_systems(
780 (
781 mark_dirty_trees,
782 sync_simple_transforms,
783 propagate_parent_transforms,
784 )
785 .chain(),
786 );
787 world.insert_resource(StaticTransformOptimizations::default());
788
789 // Root entity
790 let mut queue = CommandQueue::default();
791 let mut commands = Commands::new(&mut queue, &world);
792 let mut children = Vec::new();
793 commands
794 .spawn(Transform::from_xyz(1.0, 0.0, 0.0))
795 .with_children(|parent| {
796 children.push(parent.spawn(Transform::from_xyz(0.0, 2.0, 0.0)).id());
797 children.push(parent.spawn(Transform::from_xyz(0.0, 0.0, 3.0)).id());
798 });
799 queue.apply(&mut world);
800 schedule.run(&mut world);
801
802 assert_eq!(
803 *world.get::<GlobalTransform>(children[0]).unwrap(),
804 GlobalTransform::from_xyz(1.0, 0.0, 0.0) * Transform::from_xyz(0.0, 2.0, 0.0)
805 );
806
807 assert_eq!(
808 *world.get::<GlobalTransform>(children[1]).unwrap(),
809 GlobalTransform::from_xyz(1.0, 0.0, 0.0) * Transform::from_xyz(0.0, 0.0, 3.0)
810 );
811 }
812
813 #[test]
814 fn correct_children() {
815 ComputeTaskPool::get_or_init(TaskPool::default);
816 let mut world = World::default();
817
818 let mut schedule = Schedule::default();
819 schedule.add_systems(
820 (
821 mark_dirty_trees,
822 sync_simple_transforms,
823 propagate_parent_transforms,
824 )
825 .chain(),
826 );
827 world.insert_resource(StaticTransformOptimizations::default());
828
829 // Add parent entities
830 let mut children = Vec::new();
831 let parent = {
832 let mut command_queue = CommandQueue::default();
833 let mut commands = Commands::new(&mut command_queue, &world);
834 let parent = commands.spawn(Transform::from_xyz(1.0, 0.0, 0.0)).id();
835 commands.entity(parent).with_children(|parent| {
836 children.push(parent.spawn(Transform::from_xyz(0.0, 2.0, 0.0)).id());
837 children.push(parent.spawn(Transform::from_xyz(0.0, 3.0, 0.0)).id());
838 });
839 command_queue.apply(&mut world);
840 schedule.run(&mut world);
841 parent
842 };
843
844 assert_eq!(
845 world
846 .get::<Children>(parent)
847 .unwrap()
848 .iter()
849 .collect::<Vec<_>>(),
850 children,
851 );
852
853 // Parent `e1` to `e2`.
854 {
855 let mut command_queue = CommandQueue::default();
856 let mut commands = Commands::new(&mut command_queue, &world);
857 commands.entity(children[1]).add_child(children[0]);
858 command_queue.apply(&mut world);
859 schedule.run(&mut world);
860 }
861
862 assert_eq!(
863 world
864 .get::<Children>(parent)
865 .unwrap()
866 .iter()
867 .collect::<Vec<_>>(),
868 vec![children[1]]
869 );
870
871 assert_eq!(
872 world
873 .get::<Children>(children[1])
874 .unwrap()
875 .iter()
876 .collect::<Vec<_>>(),
877 vec![children[0]]
878 );
879
880 assert!(world.despawn(children[0]));
881
882 schedule.run(&mut world);
883
884 assert_eq!(
885 world
886 .get::<Children>(parent)
887 .unwrap()
888 .iter()
889 .collect::<Vec<_>>(),
890 vec![children[1]]
891 );
892 }
893
894 #[test]
895 fn correct_transforms_when_no_children() {
896 let mut app = App::new();
897 ComputeTaskPool::get_or_init(TaskPool::default);
898
899 app.add_systems(
900 Update,
901 (
902 mark_dirty_trees,
903 sync_simple_transforms,
904 propagate_parent_transforms,
905 )
906 .chain(),
907 )
908 .insert_resource(StaticTransformOptimizations::default());
909
910 let translation = vec3(1.0, 0.0, 0.0);
911
912 // These will be overwritten.
913 let mut child = Entity::from_raw_u32(0).unwrap();
914 let mut grandchild = Entity::from_raw_u32(1).unwrap();
915 let parent = app
916 .world_mut()
917 .spawn(Transform::from_translation(translation))
918 .with_children(|builder| {
919 child = builder
920 .spawn(Transform::IDENTITY)
921 .with_children(|builder| {
922 grandchild = builder.spawn(Transform::IDENTITY).id();
923 })
924 .id();
925 })
926 .id();
927
928 app.update();
929
930 // check the `Children` structure is spawned
931 assert_eq!(&**app.world().get::<Children>(parent).unwrap(), &[child]);
932 assert_eq!(
933 &**app.world().get::<Children>(child).unwrap(),
934 &[grandchild]
935 );
936 // Note that at this point, the `GlobalTransform`s will not have updated yet, due to
937 // `Commands` delay
938 app.update();
939
940 let mut state = app.world_mut().query::<&GlobalTransform>();
941 for global in state.iter(app.world()) {
942 assert_eq!(global, &GlobalTransform::from_translation(translation));
943 }
944 }
945
946 #[test]
947 #[should_panic]
948 fn panic_when_hierarchy_cycle() {
949 ComputeTaskPool::get_or_init(TaskPool::default);
950 // We cannot directly edit ChildOf and Children, so we use a temp world to break the
951 // hierarchy's invariants.
952 let mut temp = World::new();
953 let mut app = App::new();
954
955 app.add_systems(
956 Update,
957 // It is unsound for this unsafe system to encounter a cycle without panicking. This
958 // requirement only applies to systems with unsafe parallel traversal that result in
959 // aliased mutability during a cycle.
960 propagate_parent_transforms,
961 );
962
963 fn setup_world(world: &mut World) -> (Entity, Entity) {
964 let mut grandchild = Entity::from_raw_u32(0).unwrap();
965 let child = world
966 .spawn(Transform::IDENTITY)
967 .with_children(|builder| {
968 grandchild = builder.spawn(Transform::IDENTITY).id();
969 })
970 .id();
971 (child, grandchild)
972 }
973
974 let (temp_child, temp_grandchild) = setup_world(&mut temp);
975 let (child, grandchild) = setup_world(app.world_mut());
976
977 assert_eq!(temp_child, child);
978 assert_eq!(temp_grandchild, grandchild);
979
980 app.world_mut()
981 .spawn(Transform::IDENTITY)
982 .add_children(&[child]);
983
984 let mut child_entity = app.world_mut().entity_mut(child);
985
986 let mut grandchild_entity = temp.entity_mut(grandchild);
987
988 #[expect(
989 unsafe_code,
990 reason = "ChildOf is not mutable but this is for a test to produce a scenario that cannot happen"
991 )]
992 // SAFETY: ChildOf is not mutable but this is for a test to produce a scenario that
993 // cannot happen
994 let mut a = unsafe { child_entity.get_mut_assume_mutable::<ChildOf>().unwrap() };
995
996 #[expect(
997 unsafe_code,
998 reason = "ChildOf is not mutable but this is for a test to produce a scenario that cannot happen"
999 )]
1000 // SAFETY: ChildOf is not mutable but this is for a test to produce a scenario that
1001 // cannot happen
1002 let mut b = unsafe {
1003 grandchild_entity
1004 .get_mut_assume_mutable::<ChildOf>()
1005 .unwrap()
1006 };
1007
1008 core::mem::swap(a.as_mut(), b.as_mut());
1009
1010 app.update();
1011 }
1012
1013 #[test]
1014 fn global_transform_should_not_be_overwritten_after_reparenting() {
1015 let translation = Vec3::ONE;
1016 let mut world = World::new();
1017
1018 // Create transform propagation schedule
1019 let mut schedule = Schedule::default();
1020 schedule.add_systems(
1021 (
1022 mark_dirty_trees,
1023 propagate_parent_transforms,
1024 sync_simple_transforms,
1025 )
1026 .chain(),
1027 );
1028 world.insert_resource(StaticTransformOptimizations::default());
1029
1030 // Spawn a `Transform` entity with a local translation of `Vec3::ONE`
1031 let mut spawn_transform_bundle =
1032 || world.spawn(Transform::from_translation(translation)).id();
1033
1034 // Spawn parent and child with identical transform bundles
1035 let parent = spawn_transform_bundle();
1036 let child = spawn_transform_bundle();
1037 world.entity_mut(parent).add_child(child);
1038
1039 // Run schedule to propagate transforms
1040 schedule.run(&mut world);
1041
1042 // Child should be positioned relative to its parent
1043 let parent_global_transform = *world.entity(parent).get::<GlobalTransform>().unwrap();
1044 let child_global_transform = *world.entity(child).get::<GlobalTransform>().unwrap();
1045 assert!(parent_global_transform
1046 .translation()
1047 .abs_diff_eq(translation, 0.1));
1048 assert!(child_global_transform
1049 .translation()
1050 .abs_diff_eq(2. * translation, 0.1));
1051
1052 // Reparent child
1053 world.entity_mut(child).remove::<ChildOf>();
1054 world.entity_mut(parent).add_child(child);
1055
1056 // Run schedule to propagate transforms
1057 schedule.run(&mut world);
1058
1059 // Translations should be unchanged after update
1060 assert_eq!(
1061 parent_global_transform,
1062 *world.entity(parent).get::<GlobalTransform>().unwrap()
1063 );
1064 assert_eq!(
1065 child_global_transform,
1066 *world.entity(child).get::<GlobalTransform>().unwrap()
1067 );
1068 }
1069}