From b36d5055e305238114f3218883b346498a8b091e Mon Sep 17 00:00:00 2001 From: Katt <51190960+coolcatcoder@users.noreply.github.com> Date: Sat, 29 Nov 2025 06:50:17 +0800 Subject: [PATCH 1/4] Add register_system_cached to DeferredWorld. --- crates/bevy_ecs/src/world/deferred_world.rs | 40 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 64f1fa409f9aa..3542297f5314f 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -14,7 +14,7 @@ use crate::{ query::{QueryData, QueryFilter}, relationship::RelationshipHookMode, resource::Resource, - system::{Commands, Query}, + system::{CachedSystemId, Commands, IntoSystem, Query, SystemId, SystemInput}, traversal::Traversal, world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, }; @@ -890,4 +890,42 @@ impl<'w> DeferredWorld<'w> { pub(crate) fn as_unsafe_world_cell(&mut self) -> UnsafeWorldCell { self.world } + + /// Registers a system or returns its cached [`SystemId`]. + /// + /// If you want to run the system immediately and you don't need its `SystemId`, see + /// [`Commands::run_system_cached`]. + /// + /// The first time this function is called for a particular system, it will register it and + /// store its [`SystemId`] in a [`CachedSystemId`] resource for later. If you would rather + /// manage the `SystemId` yourself, or register multiple copies of the same system, use + /// [`Commands::register_system`] instead. + /// + /// # Limitations + /// + /// If this function is called twice for the same system before commands are executed, then it will register the system twice. + /// + /// This function only accepts ZST (zero-sized) systems to guarantee that any two systems of + /// the same type must be equal. This means that closures that capture the environment, and + /// function pointers, are not accepted. + /// + /// If you want to access values from the environment within a system, consider passing them in + /// as inputs via [`Commands::run_system_cached_with`]. If that's not an option, consider + /// [`Commands::register_system`] instead. + pub fn register_system_cached(&mut self, system: S) -> SystemId + where + I: SystemInput + Send + 'static, + O: Send + 'static, + S: IntoSystem + 'static, + { + match self.get_resource::>() { + Some(cached_system) => SystemId::from_entity(cached_system.entity), + None => { + let mut commands = self.commands(); + let system_id = commands.register_system(system); + commands.insert_resource(CachedSystemId::::new(system_id)); + system_id + } + } + } } From 0ff1f39ee078000a0e30a7591596b4d67c137b56 Mon Sep 17 00:00:00 2001 From: Katt <51190960+coolcatcoder@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:57:33 +0800 Subject: [PATCH 2/4] Attempt to test whether register_system_cached on DeferredWorld is caching systems. --- Cargo.toml | 8 ++++ ...egister_system_cached_on_deferred_world.rs | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tests/ecs/register_system_cached_on_deferred_world.rs diff --git a/Cargo.toml b/Cargo.toml index f047040bdc9f7..9c303d1cc4295 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3786,6 +3786,14 @@ doc-scrape-examples = true [package.metadata.example.ambiguity_detection] hidden = true +[[example]] +name = "register_system_cached_on_deferred_world" +path = "tests/ecs/register_system_cached_on_deferred_world.rs" +doc-scrape-examples = true + +[package.metadata.example.register_system_cached_on_deferred_world] +hidden = true + [[example]] name = "resizing" path = "tests/window/resizing.rs" diff --git a/tests/ecs/register_system_cached_on_deferred_world.rs b/tests/ecs/register_system_cached_on_deferred_world.rs new file mode 100644 index 0000000000000..1b674c02ece81 --- /dev/null +++ b/tests/ecs/register_system_cached_on_deferred_world.rs @@ -0,0 +1,38 @@ +//! A test to confirm that [`bevy::ecs::world::DeferredWorld::register_system_cached`] is caching systems. +//! This is run in CI. + +use bevy::{prelude::*, ecs::{system::SystemId, lifecycle::HookContext, world::DeferredWorld}}; + +fn main() { + let mut app = App::new(); + + let world = app.world_mut(); + + let first_entity = world.spawn(ComponentWithOnAddHook).id(); + let second_entity = world.spawn(ComponentWithOnAddHook).id(); + + app.update(); + + let world = app.world_mut(); + + let first_system = world.entity(first_entity).get::().unwrap().0; + let second_system = world.entity(second_entity).get::().unwrap().0; + + assert_eq!(first_system, second_system); +} + +#[derive(Component)] +#[component(on_add = Self::on_add)] +struct ComponentWithOnAddHook; + +impl ComponentWithOnAddHook { + fn on_add(mut world: DeferredWorld, context: HookContext) { + let system_id = world.register_system_cached(Self::the_system_that_will_be_registered); + world.commands().entity(context.entity).insert(RegisteredSystem(system_id)); + } + + fn the_system_that_will_be_registered() {} +} + +#[derive(Component)] +struct RegisteredSystem(SystemId); From b7bc21e0d6a3a902a3d9f11114c0816ba93dd3e1 Mon Sep 17 00:00:00 2001 From: Katt <51190960+coolcatcoder@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:59:34 +0800 Subject: [PATCH 3/4] Remember to rustfmt the code pertaining to register_system_cached on DeferredWorld. --- crates/bevy_ecs/src/world/deferred_world.rs | 2 +- ...egister_system_cached_on_deferred_world.rs | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 3542297f5314f..df9e6cd67fe43 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -902,7 +902,7 @@ impl<'w> DeferredWorld<'w> { /// [`Commands::register_system`] instead. /// /// # Limitations - /// + /// /// If this function is called twice for the same system before commands are executed, then it will register the system twice. /// /// This function only accepts ZST (zero-sized) systems to guarantee that any two systems of diff --git a/tests/ecs/register_system_cached_on_deferred_world.rs b/tests/ecs/register_system_cached_on_deferred_world.rs index 1b674c02ece81..4ae40f5413227 100644 --- a/tests/ecs/register_system_cached_on_deferred_world.rs +++ b/tests/ecs/register_system_cached_on_deferred_world.rs @@ -1,7 +1,10 @@ //! A test to confirm that [`bevy::ecs::world::DeferredWorld::register_system_cached`] is caching systems. //! This is run in CI. -use bevy::{prelude::*, ecs::{system::SystemId, lifecycle::HookContext, world::DeferredWorld}}; +use bevy::{ + ecs::{lifecycle::HookContext, system::SystemId, world::DeferredWorld}, + prelude::*, +}; fn main() { let mut app = App::new(); @@ -15,8 +18,16 @@ fn main() { let world = app.world_mut(); - let first_system = world.entity(first_entity).get::().unwrap().0; - let second_system = world.entity(second_entity).get::().unwrap().0; + let first_system = world + .entity(first_entity) + .get::() + .unwrap() + .0; + let second_system = world + .entity(second_entity) + .get::() + .unwrap() + .0; assert_eq!(first_system, second_system); } @@ -28,7 +39,10 @@ struct ComponentWithOnAddHook; impl ComponentWithOnAddHook { fn on_add(mut world: DeferredWorld, context: HookContext) { let system_id = world.register_system_cached(Self::the_system_that_will_be_registered); - world.commands().entity(context.entity).insert(RegisteredSystem(system_id)); + world + .commands() + .entity(context.entity) + .insert(RegisteredSystem(system_id)); } fn the_system_that_will_be_registered() {} From 1e0dc7b198ec10df0c2e8791b0254fcd942a5cd8 Mon Sep 17 00:00:00 2001 From: Katt <51190960+coolcatcoder@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:13:09 +0800 Subject: [PATCH 4/4] move the test register_system_cached_on_deferred_world to the correct location --- Cargo.toml | 8 --- crates/bevy_ecs/src/lib.rs | 45 +++++++++++++++- ...egister_system_cached_on_deferred_world.rs | 52 ------------------- 3 files changed, 44 insertions(+), 61 deletions(-) delete mode 100644 tests/ecs/register_system_cached_on_deferred_world.rs diff --git a/Cargo.toml b/Cargo.toml index 9c303d1cc4295..f047040bdc9f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3786,14 +3786,6 @@ doc-scrape-examples = true [package.metadata.example.ambiguity_detection] hidden = true -[[example]] -name = "register_system_cached_on_deferred_world" -path = "tests/ecs/register_system_cached_on_deferred_world.rs" -doc-scrape-examples = true - -[package.metadata.example.register_system_cached_on_deferred_world] -hidden = true - [[example]] name = "resizing" path = "tests/window/resizing.rs" diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 8a07cdc8e1b92..25b4ec229153e 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -151,10 +151,12 @@ mod tests { component::{Component, ComponentId, RequiredComponents, RequiredComponentsError}, entity::{Entity, EntityMapper}, entity_disabling::DefaultQueryFilters, + lifecycle::HookContext, prelude::Or, query::{Added, Changed, FilteredAccess, QueryFilter, With, Without}, resource::Resource, - world::{EntityMut, EntityRef, Mut, World}, + system::SystemId, + world::{DeferredWorld, EntityMut, EntityRef, Mut, World}, }; use alloc::{ string::{String, ToString}, @@ -2776,4 +2778,45 @@ mod tests { fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {} } + + #[test] + fn register_system_cached_on_deferred_world() { + let mut world = World::new(); + + let first_entity = world.spawn(ComponentWithOnAddHook).id(); + let second_entity = world.spawn(ComponentWithOnAddHook).id(); + + let first_system = world + .entity(first_entity) + .get::() + .unwrap() + .0; + let second_system = world + .entity(second_entity) + .get::() + .unwrap() + .0; + + assert_eq!(first_system, second_system); + + #[derive(Component)] + #[component(on_add = Self::on_add)] + struct ComponentWithOnAddHook; + + impl ComponentWithOnAddHook { + fn on_add(mut world: DeferredWorld, context: HookContext) { + let system_id = + world.register_system_cached(Self::the_system_that_will_be_registered); + world + .commands() + .entity(context.entity) + .insert(RegisteredSystem(system_id)); + } + + fn the_system_that_will_be_registered() {} + } + + #[derive(Component)] + struct RegisteredSystem(SystemId); + } } diff --git a/tests/ecs/register_system_cached_on_deferred_world.rs b/tests/ecs/register_system_cached_on_deferred_world.rs deleted file mode 100644 index 4ae40f5413227..0000000000000 --- a/tests/ecs/register_system_cached_on_deferred_world.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! A test to confirm that [`bevy::ecs::world::DeferredWorld::register_system_cached`] is caching systems. -//! This is run in CI. - -use bevy::{ - ecs::{lifecycle::HookContext, system::SystemId, world::DeferredWorld}, - prelude::*, -}; - -fn main() { - let mut app = App::new(); - - let world = app.world_mut(); - - let first_entity = world.spawn(ComponentWithOnAddHook).id(); - let second_entity = world.spawn(ComponentWithOnAddHook).id(); - - app.update(); - - let world = app.world_mut(); - - let first_system = world - .entity(first_entity) - .get::() - .unwrap() - .0; - let second_system = world - .entity(second_entity) - .get::() - .unwrap() - .0; - - assert_eq!(first_system, second_system); -} - -#[derive(Component)] -#[component(on_add = Self::on_add)] -struct ComponentWithOnAddHook; - -impl ComponentWithOnAddHook { - fn on_add(mut world: DeferredWorld, context: HookContext) { - let system_id = world.register_system_cached(Self::the_system_that_will_be_registered); - world - .commands() - .entity(context.entity) - .insert(RegisteredSystem(system_id)); - } - - fn the_system_that_will_be_registered() {} -} - -#[derive(Component)] -struct RegisteredSystem(SystemId);