Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_transform/src/systems.rs
9351 views
1
use crate::components::{GlobalTransform, Transform, TransformTreeChanged};
2
use bevy_ecs::prelude::*;
3
4
#[cfg(feature = "std")]
5
pub use parallel::propagate_parent_transforms;
6
#[cfg(not(feature = "std"))]
7
pub 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`].
13
pub 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))]
54
pub 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
61
impl 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
92
impl 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`].
109
pub 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"))]
170
mod 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")]
332
mod 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)]
664
mod 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
}
1070
1071