Skip to content

Create OnDeferred to allow hooking into every ApplyDeferred action. #19818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions crates/bevy_ecs/src/schedule/executor/multi_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -787,6 +787,8 @@ fn apply_deferred(
systems: &[SyncUnsafeCell<SystemWithAccess>],
world: &mut World,
) -> Result<(), Box<dyn Any + Send>> {
OnDeferred::execute(world);
Copy link
Contributor

@urben1680 urben1680 Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you want to run this at the end of the sync point so the closures see the effects of commands/hooks/observers? Or do you want it to be the other way around, that these see the effect of OnDeferred?

Might be worth renaming this into either PreDeferred or PostDeferred.


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;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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");
Copy link
Contributor

@urben1680 urben1680 Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer that this test does not potentially block so a bug would not cause deadlocking the test here. Generally even try_lock is not the best way to ensure the order is correct as, in theory, both the system and the closure could run in parallel and then the test would only randomly fail.

I know, they cannot run in parallel because of the exclusive world access of the closure you add, but just the other day we realized we mistakenly allow &mut World in observers. So better the test is sturdy against other bugs.

Same at the other lock below and the single threaded test.

My suggestion is to put the vector here in a resource.

});
};
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",
]
);
}
}
59 changes: 58 additions & 1 deletion crates/bevy_ecs/src/schedule/executor/single_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -205,6 +205,8 @@ impl SingleThreadedExecutor {
}

fn apply_deferred(&mut self, schedule: &mut SystemSchedule, world: &mut World) {
OnDeferred::execute(world);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to update SimpleExecutor, too. It doesn't have a separate apply_deferred step because it uses System::run to apply buffers immediately when a system runs. So you probably want to call OnDeferred::execute(world); after each system is run.

On the other hand, SimpleExecutor is deprecated and will probably be removed in 0.18, so it might not be a big deal if it doesn't support this.


for system_index in self.unapplied_systems.ones() {
let system = &mut schedule.systems[system_index].system;
system.apply_deferred(world);
Expand Down Expand Up @@ -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",
]
);
}
}
2 changes: 2 additions & 0 deletions crates/bevy_ecs/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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::{
Expand Down
27 changes: 27 additions & 0 deletions crates/bevy_ecs/src/world/on_deferred.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn FnMut(&mut World) + Send + Sync + 'static>>);

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<Self>| {
for action in &mut this.0 {
action(world);
}
});
}
}
22 changes: 22 additions & 0 deletions release-content/release-notes/on_deferred.md
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
of as a command that executes at every sync point.
of as a command that executes once at every sync point.

In contrast to "proper" commands that can run multiple times at a single sync point if they get queued again by other commands/hooks/observers.


To use this, first init the `OnDeferred` resource (to ensure it exists), then add to it:

```rust
app.init_resource::<OnDeferred>();
app.world_mut().resource_mut::<OnDeferred>().add(|world: &mut World| {
Comment on lines +13 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The init and get could be combined with World::get_resource_or_init to make this a one-liner:

Suggested change
app.init_resource::<OnDeferred>();
app.world_mut().resource_mut::<OnDeferred>().add(|world: &mut World| {
app.world_mut().get_resource_or_init::<OnDeferred>().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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why won't the more standard approaches work for assets? I would have expected assets to be used at known points in each frame (maybe during extract?) and that scheduling a system to read a queue right before that would be sufficient.

This is provided for situations where users truly need to react at every sync point.