Mark changed subtrees dirty

This commit is contained in:
Aevyrie Roessler 2025-02-27 23:37:59 -08:00
parent df5e3a7b96
commit 64669c4fb3
2 changed files with 56 additions and 5 deletions

View File

@ -1,5 +1,6 @@
use crate::systems::{
compute_transform_leaves, propagate_parent_transforms, sync_simple_transforms,
compute_transform_leaves, mark_dirty_trees, propagate_parent_transforms,
sync_simple_transforms, ChangedTransforms,
};
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
use bevy_ecs::schedule::{IntoSystemConfigs, IntoSystemSetConfigs, SystemSet};
@ -30,10 +31,12 @@ impl Plugin for TransformPlugin {
PostStartup,
PropagateTransformsSet.in_set(TransformSystem::TransformPropagate),
)
.init_resource::<ChangedTransforms>()
// add transform systems to startup so the first update is "correct"
.add_systems(
PostStartup,
(
mark_dirty_trees,
propagate_parent_transforms,
(compute_transform_leaves, sync_simple_transforms)
.ambiguous_with(TransformSystem::TransformPropagate),
@ -48,6 +51,7 @@ impl Plugin for TransformPlugin {
.add_systems(
PostUpdate,
(
mark_dirty_trees,
propagate_parent_transforms,
(compute_transform_leaves, sync_simple_transforms) // TODO: Adjust the internal parallel queries to make these parallel systems more efficiently share and fill CPU time.
.ambiguous_with(TransformSystem::TransformPropagate),

View File

@ -1,5 +1,7 @@
use crate::components::{GlobalTransform, Transform};
use bevy_ecs::entity::hash_set::EntityHashSet;
use bevy_ecs::prelude::*;
use derive_more::Deref;
#[cfg(feature = "std")]
pub use parallel::propagate_parent_transforms;
@ -41,6 +43,31 @@ pub fn sync_simple_transforms(
}
}
#[derive(Default, Resource, Deref)]
pub struct ChangedTransforms(EntityHashSet);
pub fn mark_dirty_trees(
mut changed: ResMut<ChangedTransforms>,
transforms: Query<(Entity, Option<&ChildOf>, Ref<Transform>)>,
) {
changed.0.clear();
'outer: for (entity, parent, _) in transforms
.iter()
.filter(|(.., transform)| transform.is_changed())
{
if !changed.0.insert(entity) {
continue; // Tree has already been processed
}
let mut next = parent.map(ChildOf::get);
while let Some((entity, parent, _)) = next.and_then(|entity| transforms.get(entity).ok()) {
if !changed.0.insert(entity) {
break; // Tree has already been processed
}
next = parent.map(ChildOf::get);
}
}
}
/// Compute leaf [`GlobalTransform`]s in parallel.
///
/// This is run after [`propagate_parent_transforms`], to ensure the parents' [`GlobalTransform`]s
@ -245,12 +272,13 @@ mod serial {
#[cfg(feature = "std")]
mod parallel {
use crate::prelude::*;
// TODO: this implementation could be used in no_std if there are equivalents of these.
use crate::systems::ChangedTransforms;
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};
// TODO: this implementation could be used in no_std if there are equivalents of these.
use std::sync::{
mpsc::{Receiver, Sender},
Mutex,
@ -263,6 +291,7 @@ mod parallel {
/// [`sync_simple_transforms`](super::sync_simple_transforms) and
/// [`compute_transform_leaves`](super::compute_transform_leaves).
pub fn propagate_parent_transforms(
changed_transforms: Res<ChangedTransforms>,
mut queue: Local<WorkQueue>,
mut orphaned: RemovedComponents<ChildOf>,
mut orphans: Local<Vec<Entity>>,
@ -281,6 +310,11 @@ mod parallel {
roots.par_iter_mut().for_each_init(
|| queue.local_queue.borrow_local_mut(),
|outbox, (parent, transform, mut parent_transform, children)| {
// If the parent isn't in this resource, no transforms in the subtree have changed.
if !changed_transforms.contains(&parent) {
return;
}
if transform.is_changed()
|| parent_transform.is_added()
|| orphans.binary_search(&parent).is_ok()
@ -298,6 +332,7 @@ mod parallel {
parent_transform,
children,
&nodes,
&changed_transforms,
outbox,
&queue,
// Need to revisit this single-max-depth by profiling more representative
@ -319,15 +354,21 @@ mod parallel {
let task_pool = ComputeTaskPool::get_or_init(TaskPool::default);
task_pool.scope(|s| {
(1..task_pool.thread_num()) // First worker is run locally instead of the task pool.
.for_each(|_| s.spawn(async { propagation_worker(&queue, &nodes) }));
propagation_worker(&queue, &nodes);
.for_each(|_| {
s.spawn(async { propagation_worker(&queue, &nodes, &changed_transforms) })
});
propagation_worker(&queue, &nodes, &changed_transforms);
});
}
/// A parallel worker that will consume processed parent entities from the queue, and push
/// children to the queue once it has propagated their [`GlobalTransform`].
#[inline]
fn propagation_worker(queue: &WorkQueue, nodes: &NodeQuery) {
fn propagation_worker(
queue: &WorkQueue,
nodes: &NodeQuery,
changed_transforms: &ChangedTransforms,
) {
#[cfg(feature = "std")]
let _span = bevy_log::info_span!("transform propagation worker").entered();
@ -380,6 +421,7 @@ mod parallel {
p_global_transform,
p_children,
nodes,
changed_transforms,
&mut outbox,
queue,
// Only affects performance. Trees deeper than this will still be fully
@ -424,6 +466,7 @@ mod parallel {
p_global_transform: Mut<GlobalTransform>,
p_children: &Children,
nodes: &NodeQuery,
changed_transforms: &ChangedTransforms,
outbox: &mut Vec<Entity>,
queue: &WorkQueue,
max_depth: usize,
@ -434,6 +477,10 @@ mod parallel {
// See the optimization note at the end to understand why this loop is here.
for depth in 1..=max_depth {
if !changed_transforms.contains(&parent) {
break;
}
// Safety: traversing the entity tree from the roots, we assert that the childof and
// children pointers match in both directions (see assert below) to ensure the hierarchy
// does not have any cycles. Because the hierarchy does not have cycles, we know we are