diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index b6036ee76b744..6fa88560d592a 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -20,7 +20,7 @@ use crate::{ SystemWithAccess, }, system::ScheduleSystem, - world::{unsafe_world_cell::UnsafeWorldCell, World}, + world::{unsafe_world_cell::UnsafeWorldCell, OnDeferred, World}, }; #[cfg(feature = "hotpatching")] use crate::{event::Events, HotPatched}; @@ -787,6 +787,8 @@ fn apply_deferred( systems: &[SyncUnsafeCell], world: &mut World, ) -> Result<(), Box> { + OnDeferred::execute(world); + for system_index in unapplied_systems.ones() { // SAFETY: none of these systems are running, no other references exist let system = &mut unsafe { &mut *systems[system_index].get() }.system; @@ -868,11 +870,15 @@ impl MainThreadExecutor { #[cfg(test)] mod tests { + use std::sync::Mutex; + + use alloc::{sync::Arc, vec}; + use crate::{ prelude::Resource, schedule::{ExecutorKind, IntoScheduleConfigs, Schedule}, system::Commands, - world::World, + world::{OnDeferred, World}, }; #[derive(Resource)] @@ -908,4 +914,44 @@ mod tests { schedule.add_systems(((|_: Commands| {}), |_: Commands| {}).chain()); schedule.run(&mut world); } + + #[test] + fn executes_on_deferred() { + let mut world = World::new(); + let mut schedule = Schedule::default(); + schedule.set_executor_kind(ExecutorKind::MultiThreaded); + + let result = Arc::new(Mutex::new(vec![])); + + let result_clone = result.clone(); + let my_system = move |mut commands: Commands| { + let result_clone = result_clone.clone(); + commands.queue(move |_: &mut World| { + result_clone.lock().unwrap().push("my_system"); + }); + }; + schedule.add_systems((my_system.clone(), my_system.clone(), my_system).chain()); + + world.insert_resource({ + let mut on_deferred = OnDeferred::default(); + let result_clone = result.clone(); + on_deferred.add(move |_: &mut World| { + result_clone.lock().unwrap().push("on_deferred"); + }); + on_deferred + }); + + schedule.run(&mut world); + assert_eq!( + &*result.lock().unwrap(), + &[ + "on_deferred", + "my_system", + "on_deferred", + "my_system", + "on_deferred", + "my_system", + ] + ); + } } diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 21b8d2289dd0e..a785855844aff 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -12,7 +12,7 @@ use crate::{ schedule::{ is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, }, - world::World, + world::{OnDeferred, World}, }; #[cfg(feature = "hotpatching")] use crate::{event::Events, HotPatched}; @@ -205,6 +205,8 @@ impl SingleThreadedExecutor { } fn apply_deferred(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + OnDeferred::execute(world); + for system_index in self.unapplied_systems.ones() { let system = &mut schedule.systems[system_index].system; system.apply_deferred(world); @@ -255,3 +257,58 @@ fn evaluate_and_fold_conditions( }) .fold(true, |acc, res| acc && res) } + +#[cfg(test)] +#[cfg(feature = "std")] +mod tests { + use std::sync::Mutex; + + use alloc::{sync::Arc, vec}; + + use crate::{ + prelude::{IntoScheduleConfigs, Schedule}, + schedule::ExecutorKind, + system::Commands, + world::{OnDeferred, World}, + }; + + #[test] + fn executes_on_deferred() { + let mut world = World::new(); + let mut schedule = Schedule::default(); + schedule.set_executor_kind(ExecutorKind::MultiThreaded); + + let result = Arc::new(Mutex::new(vec![])); + + let result_clone = result.clone(); + let my_system = move |mut commands: Commands| { + let result_clone = result_clone.clone(); + commands.queue(move |_: &mut World| { + result_clone.lock().unwrap().push("my_system"); + }); + }; + schedule.add_systems((my_system.clone(), my_system.clone(), my_system).chain()); + + world.insert_resource({ + let mut on_deferred = OnDeferred::default(); + let result_clone = result.clone(); + on_deferred.add(move |_: &mut World| { + result_clone.lock().unwrap().push("on_deferred"); + }); + on_deferred + }); + + schedule.run(&mut world); + assert_eq!( + &*result.lock().unwrap(), + &[ + "on_deferred", + "my_system", + "on_deferred", + "my_system", + "on_deferred", + "my_system", + ] + ); + } +} diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 714c5e1eaec8c..46b07003e64b8 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -7,6 +7,7 @@ mod entity_ref; pub mod error; mod filtered_resource; mod identifier; +mod on_deferred; mod spawn_batch; pub mod unsafe_world_cell; @@ -34,6 +35,7 @@ pub use entity_ref::{ }; pub use filtered_resource::*; pub use identifier::WorldId; +pub use on_deferred::OnDeferred; pub use spawn_batch::*; use crate::{ diff --git a/crates/bevy_ecs/src/world/on_deferred.rs b/crates/bevy_ecs/src/world/on_deferred.rs new file mode 100644 index 0000000000000..7bfb06e46ebf7 --- /dev/null +++ b/crates/bevy_ecs/src/world/on_deferred.rs @@ -0,0 +1,27 @@ +use alloc::{boxed::Box, vec::Vec}; + +use crate::{ + resource::Resource, + world::{Mut, World}, +}; + +/// A resource holding the set of all actions to take just before applying deferred actions. +#[derive(Resource, Default)] +pub struct OnDeferred(Vec>); + +impl OnDeferred { + /// Adds an action to be executed before applying deferred actions. + pub fn add(&mut self, action: impl FnMut(&mut World) + Send + Sync + 'static) { + self.0.push(Box::new(action)); + } + + /// Executes the actions in [`OnDeferred`] from `world`. Does nothing if [`OnDeferred`] does not + /// exist in the world. + pub(crate) fn execute(world: &mut World) { + world.try_resource_scope(|world, mut this: Mut| { + for action in &mut this.0 { + action(world); + } + }); + } +} diff --git a/release-content/release-notes/on_deferred.md b/release-content/release-notes/on_deferred.md new file mode 100644 index 0000000000000..84b0c0fb334b0 --- /dev/null +++ b/release-content/release-notes/on_deferred.md @@ -0,0 +1,22 @@ +--- +title: Hook into every `ApplyDeferred` using `OnDeferred` +authors: ["@andriyDev"] +pull_requests: [] +--- + +Bevy now allows you to execute some code whenever `ApplyDeferred` is executed. This can be thought +of as a command that executes at every sync point. + +To use this, first init the `OnDeferred` resource (to ensure it exists), then add to it: + +```rust +app.init_resource::(); +app.world_mut().resource_mut::().add(|world: &mut World| { + // Do command stuff. +}); +``` + +For one potential example, you could send commands through a channel to your `OnDeferred` command. +Since it has access to `&mut World` it can then apply any commands in the channel. While this is now +supported, more standard approaches are preferable (e.g., creating a system that polls the channel). +This is provided for situations where users truly need to react at every sync point.