Skip to content
5 changes: 4 additions & 1 deletion crates/bevy_transform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,12 @@ pub mod prelude {
commands::BuildChildrenTransformExt,
helper::TransformHelper,
plugins::{TransformPlugin, TransformSystems},
systems::StaticTransformOptimizations,
traits::TransformPoint,
};
}

#[cfg(feature = "bevy-support")]
pub use prelude::{TransformPlugin, TransformPoint, TransformSystems};
pub use prelude::{
StaticTransformOptimizations, TransformPlugin, TransformPoint, TransformSystems,
};
7 changes: 5 additions & 2 deletions crates/bevy_transform/src/plugins.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::systems::{mark_dirty_trees, propagate_parent_transforms, sync_simple_transforms};
use crate::systems::{
mark_dirty_trees, propagate_parent_transforms, sync_simple_transforms,
StaticTransformOptimizations,
};
use bevy_app::{App, Plugin, PostStartup, PostUpdate};
use bevy_ecs::schedule::{IntoScheduleConfigs, SystemSet};

Expand All @@ -15,7 +18,7 @@ pub struct TransformPlugin;

impl Plugin for TransformPlugin {
fn build(&self, app: &mut App) {
app
app.init_resource::<StaticTransformOptimizations>()
// add transform systems to startup so the first update is "correct"
.add_systems(
PostStartup,
Expand Down
141 changes: 127 additions & 14 deletions crates/bevy_transform/src/systems.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::components::{GlobalTransform, Transform, TransformTreeChanged};
use bevy_ecs::prelude::*;

#[cfg(feature = "std")]
pub use parallel::propagate_parent_transforms;
#[cfg(not(feature = "std"))]
Expand Down Expand Up @@ -40,27 +41,108 @@ pub fn sync_simple_transforms(
}
}

/// Optimization for static scenes. Propagates a "dirty bit" up the hierarchy towards ancestors.
/// Transform propagation can ignore entire subtrees of the hierarchy if it encounters an entity
/// without the dirty bit.
/// Configure the behavior of static scene optimizations for [`Transform`] propagation.
///
/// For scenes with many static entities, it is much faster to track trees of unchanged
/// [`Transform`]s and skip these during the expensive transform propagation step. If your scene is
/// very dynamic, the cost of tracking these trees can exceed the performance benefits. By default,
/// static scene optimization is disabled for worlds with more than 30% of its entities moving.
///
/// This resource allows you to configure that threshold at runtime.
#[derive(Resource, Debug)]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
pub struct StaticTransformOptimizations {
/// If the percentage of moving objects exceeds this value, skip dirty tree marking.
threshold: f32,
/// Updated every frame by [`mark_dirty_trees`].
enabled: bool,
}

impl StaticTransformOptimizations {
/// If the percentage of moving objects exceeds this threshold, disable static [`Transform`]
/// optimizations. This is done because the scene is so dynamic that the cost of tracking static
/// trees exceeds the performance benefit of skipping propagation for these trees.
///
/// - Setting this to `0.0` will result in never running static scene tracking.
/// - Setting this to `1.0` will result in always tracking static transform trees.
pub fn from_threshold(threshold: f32) -> Self {
Self {
threshold,
enabled: true,
}
}

/// Unconditionally disable static scene optimizations.
pub fn disabled() -> Self {
Self {
threshold: 0.0,
enabled: false,
}
}

/// Unconditionally enable static scene optimizations.
pub fn enabled() -> Self {
Self {
threshold: 1.0,
enabled: true,
}
}
}

impl Default for StaticTransformOptimizations {
fn default() -> Self {
Self {
// Scenes with more than 30% moving objects are considered dynamic enough to skip static
// optimizations.
threshold: 0.3,
enabled: true,
}
}
}

/// Optimization for static scenes.
///
/// Propagates a "dirty bit" up the hierarchy towards ancestors. Transform propagation can ignore
/// entire subtrees of the hierarchy if it encounters an entity without the dirty bit.
///
/// Configure behavior with [`StaticTransformOptimizations`].
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)>,
mut transforms: Query<&mut TransformTreeChanged>,
parents: Query<&ChildOf>,
mut static_optimizations: ResMut<StaticTransformOptimizations>,
) {
let threshold = static_optimizations.threshold.clamp(0.0, 1.0);
match threshold {
0.0 => static_optimizations.enabled = false,
1.0 => static_optimizations.enabled = true,
_ => {
static_optimizations.enabled = true;
let n_dyn = changed_transforms.count() as f32;
let total = transforms.count() as f32;
if n_dyn / total > threshold {
static_optimizations.enabled = false;
}
}
}
if !static_optimizations.enabled {
return;
}

for entity in changed_transforms.iter().chain(orphaned.read()) {
let mut next = entity;
while let Ok((child_of, mut tree)) = transforms.get_mut(next) {
while let Ok(mut tree) = transforms.get_mut(next) {
if tree.is_changed() && !tree.is_added() {
// If the component was changed, this part of the tree has already been processed.
// Ignore this if the change was caused by the component being added.
break;
}
tree.set_changed();
if let Some(parent) = child_of.map(ChildOf::parent) {
if let Ok(parent) = parents.get(next).map(ChildOf::parent) {
next = parent;
} else {
break;
Expand Down Expand Up @@ -250,6 +332,7 @@ mod serial {
mod parallel {
use crate::prelude::*;
// TODO: this implementation could be used in no_std if there are equivalents of these.
use crate::systems::StaticTransformOptimizations;
use alloc::{sync::Arc, vec::Vec};
use bevy_ecs::{entity::UniqueEntityIter, prelude::*, system::lifetimeless::Read};
use bevy_tasks::{ComputeTaskPool, TaskPool};
Expand All @@ -269,15 +352,27 @@ mod parallel {
pub fn propagate_parent_transforms(
mut queue: Local<WorkQueue>,
mut roots: Query<
(Entity, Ref<Transform>, &mut GlobalTransform, &Children),
(Without<ChildOf>, Changed<TransformTreeChanged>),
(
Entity,
Ref<Transform>,
&mut GlobalTransform,
&Children,
Ref<TransformTreeChanged>,
),
Without<ChildOf>,
>,
nodes: NodeQuery,
static_optimizations: Res<StaticTransformOptimizations>,
) {
// Process roots in parallel, seeding the work queue
roots.par_iter_mut().for_each_init(
|| queue.local_queue.borrow_local_mut(),
|outbox, (parent, transform, mut parent_transform, children)| {
|outbox, (parent, transform, mut parent_transform, children, transform_tree)| {
if static_optimizations.enabled && !transform_tree.is_changed() {
// Early exit if the subtree is static and the optimization is enabled.
return;
}

*parent_transform = GlobalTransform::from(*transform);

// SAFETY: the parent entities passed into this function are taken from iterating
Expand All @@ -292,6 +387,7 @@ mod parallel {
&nodes,
outbox,
&queue,
&static_optimizations,
// Need to revisit this single-max-depth by profiling more representative
// scenes. It's possible that it is actually beneficial to go deep into the
// hierarchy to build up a good task queue before starting the workers.
Expand Down Expand Up @@ -323,15 +419,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, &static_optimizations) });
});
propagation_worker(&queue, &nodes, &static_optimizations);
});
}

/// 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,
static_optimizations: &StaticTransformOptimizations,
) {
#[cfg(feature = "std")]
let _span = bevy_log::info_span!("transform propagation worker").entered();

Expand Down Expand Up @@ -386,6 +488,7 @@ mod parallel {
nodes,
&mut outbox,
queue,
static_optimizations,
// Only affects performance. Trees deeper than this will still be fully
// propagated, but the work will be broken into multiple tasks. This number
// was chosen to be larger than any reasonable tree depth, while not being
Expand Down Expand Up @@ -426,6 +529,7 @@ mod parallel {
nodes: &NodeQuery,
outbox: &mut Vec<Entity>,
queue: &WorkQueue,
static_optimizations: &StaticTransformOptimizations,
max_depth: usize,
) {
// Create mutable copies of the input variables, used for iterative depth-first traversal.
Expand All @@ -448,7 +552,10 @@ mod parallel {
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() {
if static_optimizations.enabled
&& !tree.is_changed()
&& !p_global_transform.is_changed()
{
// Static scene optimization
return None;
}
Expand Down Expand Up @@ -580,6 +687,7 @@ mod test {
)
.chain(),
);
world.insert_resource(StaticTransformOptimizations::default());

let mut command_queue = CommandQueue::default();
let mut commands = Commands::new(&mut command_queue, &world);
Expand Down Expand Up @@ -638,6 +746,7 @@ mod test {
)
.chain(),
);
world.insert_resource(StaticTransformOptimizations::default());

// Root entity
world.spawn(Transform::from_xyz(1.0, 0.0, 0.0));
Expand Down Expand Up @@ -675,6 +784,7 @@ mod test {
)
.chain(),
);
world.insert_resource(StaticTransformOptimizations::default());

// Root entity
let mut queue = CommandQueue::default();
Expand Down Expand Up @@ -714,6 +824,7 @@ mod test {
)
.chain(),
);
world.insert_resource(StaticTransformOptimizations::default());

// Add parent entities
let mut children = Vec::new();
Expand Down Expand Up @@ -793,7 +904,8 @@ mod test {
propagate_parent_transforms,
)
.chain(),
);
)
.insert_resource(StaticTransformOptimizations::default());

let translation = vec3(1.0, 0.0, 0.0);

Expand Down Expand Up @@ -913,6 +1025,7 @@ mod test {
)
.chain(),
);
world.insert_resource(StaticTransformOptimizations::default());

// Spawn a `Transform` entity with a local translation of `Vec3::ONE`
let mut spawn_transform_bundle =
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ui/src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ mod tests {
app.init_resource::<bevy_text::TextPipeline>();
app.init_resource::<bevy_text::CosmicFontSystem>();
app.init_resource::<bevy_text::SwashCache>();
app.init_resource::<bevy_transform::StaticTransformOptimizations>();

app.add_systems(
PostUpdate,
Expand Down
1 change: 1 addition & 0 deletions examples/stress_tests/bevymark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ fn main() {
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(StaticTransformOptimizations::disabled())
.insert_resource(WinitSettings::continuous())
.insert_resource(args)
.insert_resource(BevyCounter {
Expand Down
1 change: 1 addition & 0 deletions examples/stress_tests/many_foxes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ fn main() {
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
))
.insert_resource(StaticTransformOptimizations::disabled())
.insert_resource(WinitSettings::continuous())
.insert_resource(Foxes {
count: args.count,
Expand Down
Loading