From eb7467a3bd0e4410e8d7473555f41433f9931a1a Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 12 Jun 2025 13:05:24 -0700 Subject: [PATCH 01/34] add apply and queue methods copied directly from `SystemParam` lol --- crates/bevy_ecs/src/query/world_query.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index a6bcbf58bd..03f6a7cfed 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -3,7 +3,8 @@ use crate::{ component::{ComponentId, Components, Tick}, query::FilteredAccess, storage::Table, - world::{unsafe_world_cell::UnsafeWorldCell, World}, + system::SystemMeta, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; use variadics_please::all_tuples; @@ -117,6 +118,25 @@ pub unsafe trait WorldQuery { /// access to [`Components`]. fn get_state(components: &Components) -> Option; + /// Applies any deferred mutations stored in this [`QueryData`]'s state. + /// This is used to apply [`Commands`] during [`ApplyDeferred`](crate::prelude::ApplyDeferred). + /// + /// [`Commands`]: crate::prelude::Commands + #[inline] + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {} + + /// Queues any deferred mutations to be applied at the next [`ApplyDeferred`](crate::prelude::ApplyDeferred). + #[inline] + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {} + /// Returns `true` if this query matches a set of components. Otherwise, returns `false`. /// /// Used to check which [`Archetype`]s can be skipped by the query From ddbe222cd90a8f356a61a401d07b3ff048910c89 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 12 Jun 2025 13:39:35 -0700 Subject: [PATCH 02/34] pipe through calls from Query --- crates/bevy_ecs/src/query/world_query.rs | 14 ++++++++++++++ crates/bevy_ecs/src/system/system_param.rs | 12 +++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index 03f6a7cfed..c2de0bf0d9 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -227,6 +227,20 @@ macro_rules! impl_tuple_world_query { Some(($($name::get_state(components)?,)*)) } + #[inline] + fn apply(($($name,)*): &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + $($name::apply($name, system_meta, world);)* + } + + #[inline] + #[allow( + unused_mut, + reason = "The `world` parameter is unused for zero-length tuples; however, it must be mutable for other lengths of tuples." + )] + fn queue(($($name,)*): &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + $($name::queue($name, system_meta, world.reborrow());)* + } + fn matches_component_set(state: &Self::State, set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { let ($($name,)*) = state; true $(&& $name::matches_component_set($name, set_contains_id))* diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index fc795abf59..4f59b72151 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -7,7 +7,7 @@ use crate::{ entity::Entities, query::{ Access, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, QuerySingleError, - QueryState, ReadOnlyQueryData, + QueryState, ReadOnlyQueryData, WorldQuery, }, resource::Resource, storage::ResourceData, @@ -343,6 +343,16 @@ unsafe impl SystemParam for Qu // The caller ensures the world matches the one used in init_state. unsafe { state.query_unchecked_with_ticks(world, system_meta.last_run, change_tick) } } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + ::apply(&mut state.fetch_state, system_meta, world); + ::apply(&mut state.filter_state, system_meta, world); + } + + fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + ::queue(&mut state.fetch_state, system_meta, world.reborrow()); + ::queue(&mut state.filter_state, system_meta, world); + } } pub(crate) fn init_query_param( From eedb053ce5dd18c34b5a066ba7db6fe8d158c2dd Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 12 Jun 2025 13:53:49 -0700 Subject: [PATCH 03/34] fix imports --- crates/bevy_ecs/src/system/system_param.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 4f59b72151..93fb6c83d0 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -7,7 +7,7 @@ use crate::{ entity::Entities, query::{ Access, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, QuerySingleError, - QueryState, ReadOnlyQueryData, WorldQuery, + QueryState, ReadOnlyQueryData, }, resource::Resource, storage::ResourceData, @@ -345,13 +345,13 @@ unsafe impl SystemParam for Qu } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { - ::apply(&mut state.fetch_state, system_meta, world); - ::apply(&mut state.filter_state, system_meta, world); + D::apply(&mut state.fetch_state, system_meta, world); + F::apply(&mut state.filter_state, system_meta, world); } fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { - ::queue(&mut state.fetch_state, system_meta, world.reborrow()); - ::queue(&mut state.filter_state, system_meta, world); + D::queue(&mut state.fetch_state, system_meta, world.reborrow()); + F::queue(&mut state.filter_state, system_meta, world); } } From f8e4a5edc48f0156d73221158c79343b5c5bd99b Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 12 Jun 2025 13:54:46 -0700 Subject: [PATCH 04/34] inline methods --- crates/bevy_ecs/src/system/system_param.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 93fb6c83d0..e7daf5ca7e 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -344,11 +344,13 @@ unsafe impl SystemParam for Qu unsafe { state.query_unchecked_with_ticks(world, system_meta.last_run, change_tick) } } + #[inline] fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { D::apply(&mut state.fetch_state, system_meta, world); F::apply(&mut state.filter_state, system_meta, world); } + #[inline] fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { D::queue(&mut state.fetch_state, system_meta, world.reborrow()); F::queue(&mut state.filter_state, system_meta, world); From c675ccf25cf5dd7e432d5190afa2a368503659a3 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 12 Jun 2025 14:20:33 -0700 Subject: [PATCH 05/34] fix ci --- crates/bevy_ecs/src/query/world_query.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index c2de0bf0d9..09cf7d00bd 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -118,7 +118,7 @@ pub unsafe trait WorldQuery { /// access to [`Components`]. fn get_state(components: &Components) -> Option; - /// Applies any deferred mutations stored in this [`QueryData`]'s state. + /// Applies any deferred mutations stored in this [`WorldQuery`]'s state. /// This is used to apply [`Commands`] during [`ApplyDeferred`](crate::prelude::ApplyDeferred). /// /// [`Commands`]: crate::prelude::Commands From cd599e511f3679df88bb8fb02a727d95bb8e3c46 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 12 Jun 2025 22:31:22 -0700 Subject: [PATCH 06/34] fix query_data derive --- crates/bevy_ecs/macros/src/world_query.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 5c4c0bff01..40a874aaa6 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -168,6 +168,17 @@ pub(crate) fn world_query_impl( }) } + #[inline] + fn apply(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: &mut #path::world::World) { + #(<#field_types>::apply(&mut state.#named_field_idents, system_meta, world);)* + } + + + #[inline] + fn queue(state: &mut Self::State, system_meta: &#path::system::SystemMeta, mut world: #path::world::DeferredWorld) { + #(<#field_types>::queue(&mut state.#named_field_idents, system_meta, world.reborrow());)* + } + fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(#path::component::ComponentId) -> bool) -> bool { true #(&& <#field_types>::matches_component_set(&state.#named_field_idents, _set_contains_id))* } From 469954f770555c4a37e7e7168eb864f812ec49d4 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 12 Jun 2025 22:53:00 -0700 Subject: [PATCH 07/34] fix macros --- crates/bevy_ecs/macros/src/world_query.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 40a874aaa6..fec7387994 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -170,13 +170,13 @@ pub(crate) fn world_query_impl( #[inline] fn apply(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: &mut #path::world::World) { - #(<#field_types>::apply(&mut state.#named_field_idents, system_meta, world);)* + #(<#field_types as #path::query::WorldQuery>::apply(&mut state.#named_field_idents, system_meta, world);)* } #[inline] fn queue(state: &mut Self::State, system_meta: &#path::system::SystemMeta, mut world: #path::world::DeferredWorld) { - #(<#field_types>::queue(&mut state.#named_field_idents, system_meta, world.reborrow());)* + #(<#field_types as #path::query::WorldQuery>::queue(&mut state.#named_field_idents, system_meta, world.reborrow());)* } fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(#path::component::ComponentId) -> bool) -> bool { From a0bcb63ab9e5bd79d65664a1dbf81c8e1d4dd6fd Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sat, 14 Jun 2025 14:50:37 -0700 Subject: [PATCH 08/34] working, maybe?? --- crates/bevy_ecs/src/query/fetch.rs | 194 ++++++++++++++++++++++++++- examples/ecs/immutable_components.rs | 20 ++- 2 files changed, 211 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 55fa42c41e..d19335888a 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -3,19 +3,24 @@ use crate::{ bundle::Bundle, change_detection::{MaybeLocation, Ticks, TicksMut}, component::{Component, ComponentId, Components, Mutable, StorageType, Tick}, - entity::{Entities, Entity, EntityLocation}, + entity::{Entities, Entity, EntityHashMap, EntityLocation}, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, + system::{lifetimeless::Read, SystemMeta}, world::{ unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, FilteredEntityMut, FilteredEntityRef, Mut, Ref, World, }, }; +use bevy_platform::sync::Arc; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; +use bevy_utils::Parallel; use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; use smallvec::SmallVec; use variadics_please::all_tuples; +use core::ops::{Deref, DerefMut}; + /// Types that can be fetched from a [`World`] using a [`Query`]. /// /// There are many types that natively implement this trait: @@ -2319,6 +2324,193 @@ unsafe impl QueryData for Has { /// SAFETY: [`Has`] is read only unsafe impl ReadOnlyQueryData for Has {} +pub struct Tracked<'w, T: Component> { + entity: Entity, + old: &'w T, + new: Option, + record: TrackedMutationRecord, +} + +impl<'w, T: Component> Drop for Tracked<'w, T> { + fn drop(&mut self) { + if let Some(new) = self.new.take() { + self.record.insert(self.entity, new); + } + } +} + +impl<'w, T: Component> Deref for Tracked<'w, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.new.as_ref().unwrap_or(self.old) + } +} + +impl<'w, T: Component + Clone> DerefMut for Tracked<'w, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.new.get_or_insert(self.old.clone()) + } +} + +pub struct TrackedFetch<'w, T: Component> { + fetch: ReadFetch<'w, T>, + record: TrackedMutationRecord, +} + +impl<'w, T: Component> Clone for TrackedFetch<'w, T> { + fn clone(&self) -> Self { + Self { + fetch: self.fetch, + record: self.record.clone(), + } + } +} + +pub struct TrackedState { + component_id: ComponentId, + record: TrackedMutationRecord, +} + +struct TrackedMutationRecord(Arc>>); + +impl Default for TrackedMutationRecord { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Clone for TrackedMutationRecord { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl TrackedMutationRecord { + fn insert(&self, entity: Entity, component: T) { + self.0.scope(|map| map.insert(entity, component)); + } + + fn drain(&mut self) -> impl Iterator { + Arc::get_mut(&mut self.0) + .expect("drain() called while query fetches are still active.") + .drain() + } +} + +// SAFETY: defer to `<&T as WorldQuery>` for all methods +unsafe impl<'__w, T: Component> WorldQuery for Tracked<'__w, T> { + type Fetch<'a> = TrackedFetch<'a, T>; + + type State = TrackedState; + + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + state: &Self::State, + last_run: Tick, + this_run: Tick, + ) -> Self::Fetch<'w> { + // SAFETY: invariants are upheld by the caller + let fetch = unsafe { + <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) + }; + TrackedFetch { + fetch, + record: state.record.clone(), + } + } + + const IS_DENSE: bool = true; + + unsafe fn set_archetype<'w>( + fetch: &mut Self::Fetch<'w>, + state: &Self::State, + archetype: &'w Archetype, + table: &'w Table, + ) { + <&T as WorldQuery>::set_archetype(&mut fetch.fetch, &state.component_id, archetype, table); + } + + unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + <&T as WorldQuery>::set_table(&mut fetch.fetch, &state.component_id, table); + } + + fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { + <&T as WorldQuery>::update_component_access(&state.component_id, access); + } + + fn init_state(world: &mut World) -> Self::State { + TrackedState { + component_id: world.register_component::(), + record: Default::default(), + } + } + + fn get_state(components: &Components) -> Option { + Some(TrackedState { + component_id: components.component_id::()?, + record: Default::default(), + }) + } + + fn matches_component_set( + state: &Self::State, + set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + <&T as WorldQuery>::matches_component_set(&state.component_id, set_contains_id) + } + + fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { + world.insert_batch(state.record.drain()); + } + + fn queue( + _state: &mut Self::State, + _system_meta: &SystemMeta, + _world: crate::world::DeferredWorld, + ) { + } +} + +// SAFETY: Tracked defers to &T internally, so it must be readonly and Self::ReadOnly = Self. +unsafe impl<'__w, T: Component> QueryData for Tracked<'__w, T> { + const IS_READ_ONLY: bool = true; + + type ReadOnly = Self; + + type Item<'a> = Tracked<'a, T>; + + fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + item + } + + unsafe fn fetch<'w>( + fetch: &mut Self::Fetch<'w>, + entity: Entity, + table_row: TableRow, + ) -> Self::Item<'w> { + // SAFETY: invariants are upheld by the caller + let old = unsafe { as QueryData>::fetch(&mut fetch.fetch, entity, table_row) }; + Tracked { + entity, + old, + // note: we could try to get an existing updated component from the record, + // but we can't reliably do that across all threads. Better to say that all + // newly-created Tracked values will match what's in the ECS. + new: None, + record: fetch.record.clone(), + } + } +} + +// SAFETY: Tracked only accesses &T from the world. Though it provides mutable access, +// it does so wrapped in `Clone` and commands. +unsafe impl<'__w, T: Component> ReadOnlyQueryData for Tracked<'__w, T> {} + /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// /// `Query>` is equivalent to `Query<(Option<&A>, Option<&B>, Option<&mut C>), Or<(With, With, With)>>`. diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index c0ee241923..db733f4802 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -10,7 +10,8 @@ use bevy::{ prelude::*, ptr::OwningPtr, }; -use core::alloc::Layout; +use bevy_ecs::{query::Tracked, system::RunSystemOnce}; +use core::{alloc::Layout, iter}; /// This component is mutable, the default case. This is indicated by components /// implementing [`Component`] where [`Component::Mutability`] is [`Mutable`](bevy::ecs::component::Mutable). @@ -171,7 +172,7 @@ fn demo_3(world: &mut World) { for (_name, size, component_id) in &my_registered_components { // We're just storing some zeroes for the sake of demonstration. - let data = core::iter::repeat_n(0, *size).collect::>(); + let data = iter::repeat_n(0, *size).collect::>(); OwningPtr::make(data, |ptr| { // SAFETY: @@ -194,10 +195,25 @@ fn demo_3(world: &mut World) { } } +#[derive(Component, Clone, Copy)] +#[component(immutable)] +pub struct MyImmutableCounter(u32); + +fn demo_4(world: &mut World) { + world.spawn_batch(iter::repeat_n(MyImmutableCounter(0), 10)); + + let _ = world.run_system_once(|counters: Query>| { + for mut counter in counters { + counter.0 += 1; + } + }); +} + fn main() { App::new() .add_systems(Startup, demo_1) .add_systems(Startup, demo_2) .add_systems(Startup, demo_3) + .add_systems(Startup, demo_4) .run(); } From 2a6fb145f9f68816f818c83d1f849d43569620a1 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Mon, 16 Jun 2025 14:41:49 -0700 Subject: [PATCH 09/34] remove the arcane --- crates/bevy_ecs/src/query/fetch.rs | 68 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 140596d9a5..2fb0fc33e3 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2514,14 +2514,14 @@ impl ReleaseStateQueryData for Has { } } -pub struct Tracked<'w, T: Component> { +pub struct Tracked<'w, 's, T: Component> { entity: Entity, old: &'w T, new: Option, - record: TrackedMutationRecord, + record: &'s TrackedMutationRecord, } -impl<'w, T: Component> Drop for Tracked<'w, T> { +impl<'w, 's, T: Component> Drop for Tracked<'w, 's, T> { fn drop(&mut self) { if let Some(new) = self.new.take() { self.record.insert(self.entity, new); @@ -2529,7 +2529,7 @@ impl<'w, T: Component> Drop for Tracked<'w, T> { } } -impl<'w, T: Component> Deref for Tracked<'w, T> { +impl<'w, 's, T: Component> Deref for Tracked<'w, 's, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -2537,18 +2537,18 @@ impl<'w, T: Component> Deref for Tracked<'w, T> { } } -impl<'w, T: Component + Clone> DerefMut for Tracked<'w, T> { +impl<'w, 's, T: Component + Clone> DerefMut for Tracked<'w, 's, T> { fn deref_mut(&mut self) -> &mut Self::Target { self.new.get_or_insert(self.old.clone()) } } -pub struct TrackedFetch<'w, T: Component> { +pub struct TrackedFetch<'w, 's, T: Component> { fetch: ReadFetch<'w, T>, - record: TrackedMutationRecord, + record: &'s TrackedMutationRecord, } -impl<'w, T: Component> Clone for TrackedFetch<'w, T> { +impl<'w, 's, T: Component> Clone for TrackedFetch<'w, 's, T> { fn clone(&self) -> Self { Self { fetch: self.fetch, @@ -2562,7 +2562,7 @@ pub struct TrackedState { record: TrackedMutationRecord, } -struct TrackedMutationRecord(Arc>>); +struct TrackedMutationRecord(Parallel>); impl Default for TrackedMutationRecord { fn default() -> Self { @@ -2570,62 +2570,60 @@ impl Default for TrackedMutationRecord { } } -impl Clone for TrackedMutationRecord { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - impl TrackedMutationRecord { fn insert(&self, entity: Entity, component: T) { self.0.scope(|map| map.insert(entity, component)); } fn drain(&mut self) -> impl Iterator { - Arc::get_mut(&mut self.0) - .expect("drain() called while query fetches are still active.") - .drain() + self.0.drain() } } // SAFETY: defer to `<&T as WorldQuery>` for all methods -unsafe impl<'__w, T: Component> WorldQuery for Tracked<'__w, T> { - type Fetch<'a> = TrackedFetch<'a, T>; +unsafe impl<'__w, '__s, T: Component> WorldQuery for Tracked<'__w, '__s, T> { + type Fetch<'w, 's> = TrackedFetch<'w, 's, T>; type State = TrackedState; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( + fetch: Self::Fetch<'wlong, 's>, + ) -> Self::Fetch<'wshort, 's> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &Self::State, + state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w> { + ) -> Self::Fetch<'w, 's> { // SAFETY: invariants are upheld by the caller let fetch = unsafe { <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) }; TrackedFetch { fetch, - record: state.record.clone(), + record: &state.record, } } const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w, 's>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ) { <&T as WorldQuery>::set_archetype(&mut fetch.fetch, &state.component_id, archetype, table); } - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w, 's>, + state: &Self::State, + table: &'w Table, + ) { <&T as WorldQuery>::set_table(&mut fetch.fetch, &state.component_id, table); } @@ -2667,12 +2665,12 @@ unsafe impl<'__w, T: Component> WorldQuery for Tracked<'__w, T> { } // SAFETY: Tracked defers to &T internally, so it must be readonly and Self::ReadOnly = Self. -unsafe impl<'__w, T: Component> QueryData for Tracked<'__w, T> { +unsafe impl<'__w, '__s, T: Component> QueryData for Tracked<'__w, '__s, T> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w, 's> = Tracked<'w, T>; + type Item<'w, 's> = Tracked<'w, 's, T>; fn shrink<'wlong: 'wshort, 'wshort, 's>( item: Self::Item<'wlong, 's>, @@ -2690,18 +2688,18 @@ unsafe impl<'__w, T: Component> QueryData for Tracked<'__w, T> { Tracked { entity, old, - // note: we could try to get an existing updated component from the record, + // NOTE: we could try to get an existing updated component from the record, // but we can't reliably do that across all threads. Better to say that all // newly-created Tracked values will match what's in the ECS. new: None, - record: fetch.record.clone(), + record: fetch.record, } } } -// SAFETY: Tracked only accesses &T from the world. Though it provides mutable access, -// it does so wrapped in `Clone` and commands. -unsafe impl<'__w, T: Component> ReadOnlyQueryData for Tracked<'__w, T> {} +// SAFETY: Tracked only accesses &T from the world. Though it provides mutable access, it only +// applies those changes through commands. +unsafe impl<'__w, '__s, T: Component> ReadOnlyQueryData for Tracked<'__w, '__s, T> {} /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// From d8ad523e4cf21eb264f26f5d31e886c91d93a3f8 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Mon, 16 Jun 2025 15:54:12 -0700 Subject: [PATCH 10/34] rename TrackedMutations --- crates/bevy_ecs/src/query/fetch.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 2fb0fc33e3..2303d4e85f 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -8,11 +8,10 @@ use crate::{ storage::{ComponentSparseSet, Table, TableRow}, system::{lifetimeless::Read, SystemMeta}, world::{ - unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, - FilteredEntityMut, FilteredEntityRef, Mut, Ref, World, + unsafe_world_cell::UnsafeWorldCell, DeferredWorld, EntityMut, EntityMutExcept, EntityRef, + EntityRefExcept, FilteredEntityMut, FilteredEntityRef, Mut, Ref, World, }, }; -use bevy_platform::sync::Arc; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; use bevy_utils::Parallel; use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; @@ -2518,7 +2517,7 @@ pub struct Tracked<'w, 's, T: Component> { entity: Entity, old: &'w T, new: Option, - record: &'s TrackedMutationRecord, + record: &'s TrackedMutations, } impl<'w, 's, T: Component> Drop for Tracked<'w, 's, T> { @@ -2545,7 +2544,7 @@ impl<'w, 's, T: Component + Clone> DerefMut for Tracked<'w, 's, T> { pub struct TrackedFetch<'w, 's, T: Component> { fetch: ReadFetch<'w, T>, - record: &'s TrackedMutationRecord, + record: &'s TrackedMutations, } impl<'w, 's, T: Component> Clone for TrackedFetch<'w, 's, T> { @@ -2559,18 +2558,18 @@ impl<'w, 's, T: Component> Clone for TrackedFetch<'w, 's, T> { pub struct TrackedState { component_id: ComponentId, - record: TrackedMutationRecord, + record: TrackedMutations, } -struct TrackedMutationRecord(Parallel>); +struct TrackedMutations(Parallel>); -impl Default for TrackedMutationRecord { +impl Default for TrackedMutations { fn default() -> Self { Self(Default::default()) } } -impl TrackedMutationRecord { +impl TrackedMutations { fn insert(&self, entity: Entity, component: T) { self.0.scope(|map| map.insert(entity, component)); } @@ -2656,11 +2655,8 @@ unsafe impl<'__w, '__s, T: Component> WorldQuery for Tracked<'__w, '__s, T> { world.insert_batch(state.record.drain()); } - fn queue( - _state: &mut Self::State, - _system_meta: &SystemMeta, - _world: crate::world::DeferredWorld, - ) { + fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + todo!() } } From f3653a107f597cc5a1ea8c92a5358832575a3c71 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 17 Jun 2025 09:35:01 -0700 Subject: [PATCH 11/34] fix methods --- crates/bevy_ecs/src/query/fetch.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 2303d4e85f..17acb8574f 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2551,7 +2551,7 @@ impl<'w, 's, T: Component> Clone for TrackedFetch<'w, 's, T> { fn clone(&self) -> Self { Self { fetch: self.fetch, - record: self.record.clone(), + record: self.record, } } } @@ -2656,7 +2656,9 @@ unsafe impl<'__w, '__s, T: Component> WorldQuery for Tracked<'__w, '__s, T> { } fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { - todo!() + world + .commands() + .insert_batch(state.record.drain().collect::>()); } } From b892f0f0ebcce439671e8fbff69bfa9001300d66 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 17 Jun 2025 09:45:18 -0700 Subject: [PATCH 12/34] rename to `DeferredMut` --- crates/bevy_ecs/src/query/fetch.rs | 38 ++++++++++++++-------------- examples/ecs/immutable_components.rs | 6 +++-- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 17acb8574f..186e639e5e 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2513,14 +2513,14 @@ impl ReleaseStateQueryData for Has { } } -pub struct Tracked<'w, 's, T: Component> { +pub struct DeferredMut<'w, 's, T: Component> { entity: Entity, old: &'w T, new: Option, - record: &'s TrackedMutations, + record: &'s DeferredMutations, } -impl<'w, 's, T: Component> Drop for Tracked<'w, 's, T> { +impl<'w, 's, T: Component> Drop for DeferredMut<'w, 's, T> { fn drop(&mut self) { if let Some(new) = self.new.take() { self.record.insert(self.entity, new); @@ -2528,7 +2528,7 @@ impl<'w, 's, T: Component> Drop for Tracked<'w, 's, T> { } } -impl<'w, 's, T: Component> Deref for Tracked<'w, 's, T> { +impl<'w, 's, T: Component> Deref for DeferredMut<'w, 's, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -2536,18 +2536,18 @@ impl<'w, 's, T: Component> Deref for Tracked<'w, 's, T> { } } -impl<'w, 's, T: Component + Clone> DerefMut for Tracked<'w, 's, T> { +impl<'w, 's, T: Component + Clone> DerefMut for DeferredMut<'w, 's, T> { fn deref_mut(&mut self) -> &mut Self::Target { self.new.get_or_insert(self.old.clone()) } } -pub struct TrackedFetch<'w, 's, T: Component> { +pub struct DeferredMutFetch<'w, 's, T: Component> { fetch: ReadFetch<'w, T>, - record: &'s TrackedMutations, + record: &'s DeferredMutations, } -impl<'w, 's, T: Component> Clone for TrackedFetch<'w, 's, T> { +impl<'w, 's, T: Component> Clone for DeferredMutFetch<'w, 's, T> { fn clone(&self) -> Self { Self { fetch: self.fetch, @@ -2558,18 +2558,18 @@ impl<'w, 's, T: Component> Clone for TrackedFetch<'w, 's, T> { pub struct TrackedState { component_id: ComponentId, - record: TrackedMutations, + record: DeferredMutations, } -struct TrackedMutations(Parallel>); +struct DeferredMutations(Parallel>); -impl Default for TrackedMutations { +impl Default for DeferredMutations { fn default() -> Self { Self(Default::default()) } } -impl TrackedMutations { +impl DeferredMutations { fn insert(&self, entity: Entity, component: T) { self.0.scope(|map| map.insert(entity, component)); } @@ -2580,8 +2580,8 @@ impl TrackedMutations { } // SAFETY: defer to `<&T as WorldQuery>` for all methods -unsafe impl<'__w, '__s, T: Component> WorldQuery for Tracked<'__w, '__s, T> { - type Fetch<'w, 's> = TrackedFetch<'w, 's, T>; +unsafe impl<'__w, '__s, T: Component> WorldQuery for DeferredMut<'__w, '__s, T> { + type Fetch<'w, 's> = DeferredMutFetch<'w, 's, T>; type State = TrackedState; @@ -2601,7 +2601,7 @@ unsafe impl<'__w, '__s, T: Component> WorldQuery for Tracked<'__w, '__s, T> { let fetch = unsafe { <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) }; - TrackedFetch { + DeferredMutFetch { fetch, record: &state.record, } @@ -2663,12 +2663,12 @@ unsafe impl<'__w, '__s, T: Component> WorldQuery for Tracked<'__w, '__s, T> { } // SAFETY: Tracked defers to &T internally, so it must be readonly and Self::ReadOnly = Self. -unsafe impl<'__w, '__s, T: Component> QueryData for Tracked<'__w, '__s, T> { +unsafe impl<'__w, '__s, T: Component> QueryData for DeferredMut<'__w, '__s, T> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w, 's> = Tracked<'w, 's, T>; + type Item<'w, 's> = DeferredMut<'w, 's, T>; fn shrink<'wlong: 'wshort, 'wshort, 's>( item: Self::Item<'wlong, 's>, @@ -2683,7 +2683,7 @@ unsafe impl<'__w, '__s, T: Component> QueryData for Tracked<'__w, '__s, T> { ) -> Self::Item<'w, 's> { // SAFETY: invariants are upheld by the caller let old = unsafe { as QueryData>::fetch(&mut fetch.fetch, entity, table_row) }; - Tracked { + DeferredMut { entity, old, // NOTE: we could try to get an existing updated component from the record, @@ -2697,7 +2697,7 @@ unsafe impl<'__w, '__s, T: Component> QueryData for Tracked<'__w, '__s, T> { // SAFETY: Tracked only accesses &T from the world. Though it provides mutable access, it only // applies those changes through commands. -unsafe impl<'__w, '__s, T: Component> ReadOnlyQueryData for Tracked<'__w, '__s, T> {} +unsafe impl<'__w, '__s, T: Component> ReadOnlyQueryData for DeferredMut<'__w, '__s, T> {} /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index db733f4802..edc3bf3650 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -10,7 +10,7 @@ use bevy::{ prelude::*, ptr::OwningPtr, }; -use bevy_ecs::{query::Tracked, system::RunSystemOnce}; +use bevy_ecs::{query::DeferredMut, system::RunSystemOnce}; use core::{alloc::Layout, iter}; /// This component is mutable, the default case. This is indicated by components @@ -202,11 +202,13 @@ pub struct MyImmutableCounter(u32); fn demo_4(world: &mut World) { world.spawn_batch(iter::repeat_n(MyImmutableCounter(0), 10)); - let _ = world.run_system_once(|counters: Query>| { + let _ = world.run_system_once(|counters: Query>| { for mut counter in counters { counter.0 += 1; } }); + + assert!() } fn main() { From ca7436cdc0d76e78e7a6c07381805c449261f312 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 17 Jun 2025 10:02:39 -0700 Subject: [PATCH 13/34] docs --- crates/bevy_ecs/src/query/fetch.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 186e639e5e..69db4877cc 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2513,6 +2513,7 @@ impl ReleaseStateQueryData for Has { } } +/// Provides mutable access to a component, wrapped through commands. pub struct DeferredMut<'w, 's, T: Component> { entity: Entity, old: &'w T, @@ -2542,6 +2543,7 @@ impl<'w, 's, T: Component + Clone> DerefMut for DeferredMut<'w, 's, T> { } } +/// The [`WorldQuery::Fetch`] type for [`DeferredMut`] pub struct DeferredMutFetch<'w, 's, T: Component> { fetch: ReadFetch<'w, T>, record: &'s DeferredMutations, @@ -2556,7 +2558,8 @@ impl<'w, 's, T: Component> Clone for DeferredMutFetch<'w, 's, T> { } } -pub struct TrackedState { +/// The [`WorldQuery::State`] type for [`DeferredMut`] +pub struct DeferredMutState { component_id: ComponentId, record: DeferredMutations, } @@ -2579,11 +2582,11 @@ impl DeferredMutations { } } -// SAFETY: defer to `<&T as WorldQuery>` for all methods +// SAFETY: impl defers to `<&T as WorldQuery>` for all methods unsafe impl<'__w, '__s, T: Component> WorldQuery for DeferredMut<'__w, '__s, T> { type Fetch<'w, 's> = DeferredMutFetch<'w, 's, T>; - type State = TrackedState; + type State = DeferredMutState; fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( fetch: Self::Fetch<'wlong, 's>, @@ -2631,14 +2634,14 @@ unsafe impl<'__w, '__s, T: Component> WorldQuery for DeferredMut<'__w, '__s, T> } fn init_state(world: &mut World) -> Self::State { - TrackedState { + DeferredMutState { component_id: world.register_component::(), record: Default::default(), } } fn get_state(components: &Components) -> Option { - Some(TrackedState { + Some(DeferredMutState { component_id: components.component_id::()?, record: Default::default(), }) @@ -2655,7 +2658,7 @@ unsafe impl<'__w, '__s, T: Component> WorldQuery for DeferredMut<'__w, '__s, T> world.insert_batch(state.record.drain()); } - fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + fn queue(state: &mut Self::State, _system_meta: &SystemMeta, mut world: DeferredWorld) { world .commands() .insert_batch(state.record.drain().collect::>()); From a10b23a6ec5a3ed3a577d3d9cad02e5c722ee5d9 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 17 Jun 2025 10:41:09 -0700 Subject: [PATCH 14/34] more docs progress --- crates/bevy_ecs/src/query/fetch.rs | 16 +++++++++++++++- examples/ecs/immutable_components.rs | 4 +--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 69db4877cc..abbcec13c8 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2513,7 +2513,21 @@ impl ReleaseStateQueryData for Has { } } -/// Provides mutable access to a component, wrapped through commands. +/// Provides "fake" mutable access to the component `T` +/// +/// `DeferredMut` only accesses `&T` from the world, but when mutably +/// dereferenced will clone that value and return the cloned value as a +/// mutable reference. Once the `DeferredMut` is dropped, the query keeps +/// track of the new value and inserts it into the world at the next sync point. +/// +/// This can be used to "mutably" access immutable components! +/// However, this will still be slower than direct mutation, so +/// +/// # Footguns +/// +/// # Examples +/// +/// pub struct DeferredMut<'w, 's, T: Component> { entity: Entity, old: &'w T, diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index edc3bf3650..b597c10f40 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -195,7 +195,7 @@ fn demo_3(world: &mut World) { } } -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, PartialEq, Eq)] #[component(immutable)] pub struct MyImmutableCounter(u32); @@ -207,8 +207,6 @@ fn demo_4(world: &mut World) { counter.0 += 1; } }); - - assert!() } fn main() { From 18a5a387ae0200db1d2fda2c9fd008afd12ac5f4 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 13:35:09 -0700 Subject: [PATCH 15/34] add clone to everything blegh, don't like this solution. --- crates/bevy_ecs/src/query/fetch.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index abbcec13c8..3d381a2184 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2528,14 +2528,14 @@ impl ReleaseStateQueryData for Has { /// # Examples /// /// -pub struct DeferredMut<'w, 's, T: Component> { +pub struct DeferredMut<'w, 's, T: Component + Clone> { entity: Entity, old: &'w T, new: Option, record: &'s DeferredMutations, } -impl<'w, 's, T: Component> Drop for DeferredMut<'w, 's, T> { +impl<'w, 's, T: Component + Clone> Drop for DeferredMut<'w, 's, T> { fn drop(&mut self) { if let Some(new) = self.new.take() { self.record.insert(self.entity, new); @@ -2543,7 +2543,7 @@ impl<'w, 's, T: Component> Drop for DeferredMut<'w, 's, T> { } } -impl<'w, 's, T: Component> Deref for DeferredMut<'w, 's, T> { +impl<'w, 's, T: Component + Clone> Deref for DeferredMut<'w, 's, T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -2558,12 +2558,12 @@ impl<'w, 's, T: Component + Clone> DerefMut for DeferredMut<'w, 's, T> { } /// The [`WorldQuery::Fetch`] type for [`DeferredMut`] -pub struct DeferredMutFetch<'w, 's, T: Component> { +pub struct DeferredMutFetch<'w, 's, T: Component + Clone> { fetch: ReadFetch<'w, T>, record: &'s DeferredMutations, } -impl<'w, 's, T: Component> Clone for DeferredMutFetch<'w, 's, T> { +impl<'w, 's, T: Component + Clone> Clone for DeferredMutFetch<'w, 's, T> { fn clone(&self) -> Self { Self { fetch: self.fetch, @@ -2573,20 +2573,20 @@ impl<'w, 's, T: Component> Clone for DeferredMutFetch<'w, 's, T> { } /// The [`WorldQuery::State`] type for [`DeferredMut`] -pub struct DeferredMutState { +pub struct DeferredMutState { component_id: ComponentId, record: DeferredMutations, } struct DeferredMutations(Parallel>); -impl Default for DeferredMutations { +impl Default for DeferredMutations { fn default() -> Self { Self(Default::default()) } } -impl DeferredMutations { +impl DeferredMutations { fn insert(&self, entity: Entity, component: T) { self.0.scope(|map| map.insert(entity, component)); } @@ -2597,7 +2597,7 @@ impl DeferredMutations { } // SAFETY: impl defers to `<&T as WorldQuery>` for all methods -unsafe impl<'__w, '__s, T: Component> WorldQuery for DeferredMut<'__w, '__s, T> { +unsafe impl<'__w, '__s, T: Component + Clone> WorldQuery for DeferredMut<'__w, '__s, T> { type Fetch<'w, 's> = DeferredMutFetch<'w, 's, T>; type State = DeferredMutState; @@ -2680,7 +2680,7 @@ unsafe impl<'__w, '__s, T: Component> WorldQuery for DeferredMut<'__w, '__s, T> } // SAFETY: Tracked defers to &T internally, so it must be readonly and Self::ReadOnly = Self. -unsafe impl<'__w, '__s, T: Component> QueryData for DeferredMut<'__w, '__s, T> { +unsafe impl<'__w, '__s, T: Component + Clone> QueryData for DeferredMut<'__w, '__s, T> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; @@ -2714,7 +2714,7 @@ unsafe impl<'__w, '__s, T: Component> QueryData for DeferredMut<'__w, '__s, T> { // SAFETY: Tracked only accesses &T from the world. Though it provides mutable access, it only // applies those changes through commands. -unsafe impl<'__w, '__s, T: Component> ReadOnlyQueryData for DeferredMut<'__w, '__s, T> {} +unsafe impl<'__w, '__s, T: Component + Clone> ReadOnlyQueryData for DeferredMut<'__w, '__s, T> {} /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// From 50ed0847fa3409387b81f5c273a60ad2cedc8958 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 14:46:12 -0700 Subject: [PATCH 16/34] update docs and examples --- crates/bevy_ecs/src/query/fetch.rs | 123 ++++++++++++++++----------- examples/ecs/immutable_components.rs | 15 ---- 2 files changed, 73 insertions(+), 65 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 60cca2a0ed..36ce216def 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -13,8 +13,8 @@ use crate::{ }, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; -use bevy_utils::Parallel; use bevy_utils::prelude::DebugName; +use bevy_utils::Parallel; use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; use smallvec::SmallVec; use variadics_please::all_tuples; @@ -2493,18 +2493,48 @@ impl ReleaseStateQueryData for Has { /// Provides "fake" mutable access to the component `T` /// /// `DeferredMut` only accesses `&T` from the world, but when mutably -/// dereferenced will clone that value and return the cloned value as a -/// mutable reference. Once the `DeferredMut` is dropped, the query keeps -/// track of the new value and inserts it into the world at the next sync point. +/// dereferenced will clone it and return a reference to that cloned value. +/// Once the `DeferredMut` is dropped, the query keeps track of the new value +/// and inserts it into the world at the next sync point. /// /// This can be used to "mutably" access immutable components! -/// However, this will still be slower than direct mutation, so -/// -/// # Footguns +/// However, this will still be slower than direct mutation, so this should +/// mainly be used for its ergonomics. /// /// # Examples /// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::query::DeferredMut; +/// # use bevy_ecs::system::IntoSystem; +/// # use bevy_ecs::system::Query; +/// # +// # #[derive(Component)] +/// # struct Poisoned; +/// #[derive(Component)] +/// #[component(immutable)] +/// struct Health(u32); /// +/// fn tick_poison(mut health_query: Query<(DeferredMut, Has)>) { +/// for (mut health, is_poisoned) in health_query { +/// health.0 -= 1; +/// } +/// } +/// # bevy_ecs::system::assert_is_system(tick_counters); +/// ``` +/// +/// # Footguns +/// +/// It's possible to query multiple `DeferredMut` values from the same entity. +/// However, since mutations are deferred, each new value won't see the changes +/// applied to previous iterations. +/// +/// Normally, the final iteration will be the one that "wins" and gets inserted +/// onto the entity, but parallelism can mess with that, too. Since `DeferredMut` +/// internally uses a thread-local [`EntityHashMap`] to keep track of mutations, +/// if two `DeferredMut` values for the same entity are created in the same system +/// on different threads, then they'll each be inserted into the entity in an +/// undetermined order. pub struct DeferredMut<'w, 's, T: Component + Clone> { entity: Entity, old: &'w T, @@ -2512,6 +2542,24 @@ pub struct DeferredMut<'w, 's, T: Component + Clone> { record: &'s DeferredMutations, } +impl<'w, 's, T: Component + Clone> DeferredMut<'w, 's, T> { + /// Returns a reference to the `T` value still present in the ECS + pub fn stale(&self) -> &'w T { + self.old + } + + /// Returns a reference to the `T` value currently being updated. + /// If none is present yet, this method will clone from `Self::stale` + pub fn fresh(&mut self) -> &mut T { + self.new.get_or_insert(self.old.clone()) + } + + /// Returns a (possibly absent) reference to the `T` value currently being updated. + pub fn try_get_fresh(&mut self) -> Option<&mut T> { + self.new.as_mut() + } +} + impl<'w, 's, T: Component + Clone> Drop for DeferredMut<'w, 's, T> { fn drop(&mut self) { if let Some(new) = self.new.take() { @@ -2530,22 +2578,7 @@ impl<'w, 's, T: Component + Clone> Deref for DeferredMut<'w, 's, T> { impl<'w, 's, T: Component + Clone> DerefMut for DeferredMut<'w, 's, T> { fn deref_mut(&mut self) -> &mut Self::Target { - self.new.get_or_insert(self.old.clone()) - } -} - -/// The [`WorldQuery::Fetch`] type for [`DeferredMut`] -pub struct DeferredMutFetch<'w, 's, T: Component + Clone> { - fetch: ReadFetch<'w, T>, - record: &'s DeferredMutations, -} - -impl<'w, 's, T: Component + Clone> Clone for DeferredMutFetch<'w, 's, T> { - fn clone(&self) -> Self { - Self { - fetch: self.fetch, - record: self.record, - } + self.fresh() } } @@ -2575,13 +2608,11 @@ impl DeferredMutations { // SAFETY: impl defers to `<&T as WorldQuery>` for all methods unsafe impl<'__w, '__s, T: Component + Clone> WorldQuery for DeferredMut<'__w, '__s, T> { - type Fetch<'w, 's> = DeferredMutFetch<'w, 's, T>; + type Fetch<'w> = ReadFetch<'w, T>; type State = DeferredMutState; - fn shrink_fetch<'wlong: 'wshort, 'wshort, 's>( - fetch: Self::Fetch<'wlong, 's>, - ) -> Self::Fetch<'wshort, 's> { + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } @@ -2590,34 +2621,24 @@ unsafe impl<'__w, '__s, T: Component + Clone> WorldQuery for DeferredMut<'__w, ' state: &'s Self::State, last_run: Tick, this_run: Tick, - ) -> Self::Fetch<'w, 's> { + ) -> Self::Fetch<'w> { // SAFETY: invariants are upheld by the caller - let fetch = unsafe { - <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) - }; - DeferredMutFetch { - fetch, - record: &state.record, - } + unsafe { <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) } } const IS_DENSE: bool = true; - unsafe fn set_archetype<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, - state: &'s Self::State, + unsafe fn set_archetype<'w>( + fetch: &mut Self::Fetch<'w>, + state: &Self::State, archetype: &'w Archetype, table: &'w Table, ) { - <&T as WorldQuery>::set_archetype(&mut fetch.fetch, &state.component_id, archetype, table); + <&T as WorldQuery>::set_archetype(fetch, &state.component_id, archetype, table); } - unsafe fn set_table<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, - state: &Self::State, - table: &'w Table, - ) { - <&T as WorldQuery>::set_table(&mut fetch.fetch, &state.component_id, table); + unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + <&T as WorldQuery>::set_table(fetch, &state.component_id, table); } fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { @@ -2656,7 +2677,7 @@ unsafe impl<'__w, '__s, T: Component + Clone> WorldQuery for DeferredMut<'__w, ' } } -// SAFETY: Tracked defers to &T internally, so it must be readonly and Self::ReadOnly = Self. +// SAFETY: DeferredMut defers to &T internally, so it must be readonly and Self::ReadOnly = Self. unsafe impl<'__w, '__s, T: Component + Clone> QueryData for DeferredMut<'__w, '__s, T> { const IS_READ_ONLY: bool = true; @@ -2671,20 +2692,22 @@ unsafe impl<'__w, '__s, T: Component + Clone> QueryData for DeferredMut<'__w, '_ } unsafe fn fetch<'w, 's>( - fetch: &mut Self::Fetch<'w, 's>, + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, ) -> Self::Item<'w, 's> { // SAFETY: invariants are upheld by the caller - let old = unsafe { as QueryData>::fetch(&mut fetch.fetch, entity, table_row) }; + let old = + unsafe { as QueryData>::fetch(&state.component_id, fetch, entity, table_row) }; DeferredMut { entity, old, // NOTE: we could try to get an existing updated component from the record, // but we can't reliably do that across all threads. Better to say that all - // newly-created Tracked values will match what's in the ECS. + // newly-created DeferredMut values will match what's in the ECS. new: None, - record: fetch.record, + record: &state.record, } } } diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index b597c10f40..ec4f649331 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -195,25 +195,10 @@ fn demo_3(world: &mut World) { } } -#[derive(Component, Clone, Copy, PartialEq, Eq)] -#[component(immutable)] -pub struct MyImmutableCounter(u32); - -fn demo_4(world: &mut World) { - world.spawn_batch(iter::repeat_n(MyImmutableCounter(0), 10)); - - let _ = world.run_system_once(|counters: Query>| { - for mut counter in counters { - counter.0 += 1; - } - }); -} - fn main() { App::new() .add_systems(Startup, demo_1) .add_systems(Startup, demo_2) .add_systems(Startup, demo_3) - .add_systems(Startup, demo_4) .run(); } From 2d6bd31f77b684340f37f9911495d7c2e8198209 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 14:48:57 -0700 Subject: [PATCH 17/34] revert other example changes --- examples/ecs/immutable_components.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/ecs/immutable_components.rs b/examples/ecs/immutable_components.rs index ec4f649331..c0ee241923 100644 --- a/examples/ecs/immutable_components.rs +++ b/examples/ecs/immutable_components.rs @@ -10,8 +10,7 @@ use bevy::{ prelude::*, ptr::OwningPtr, }; -use bevy_ecs::{query::DeferredMut, system::RunSystemOnce}; -use core::{alloc::Layout, iter}; +use core::alloc::Layout; /// This component is mutable, the default case. This is indicated by components /// implementing [`Component`] where [`Component::Mutability`] is [`Mutable`](bevy::ecs::component::Mutable). @@ -172,7 +171,7 @@ fn demo_3(world: &mut World) { for (_name, size, component_id) in &my_registered_components { // We're just storing some zeroes for the sake of demonstration. - let data = iter::repeat_n(0, *size).collect::>(); + let data = core::iter::repeat_n(0, *size).collect::>(); OwningPtr::make(data, |ptr| { // SAFETY: From 6427fa158227bfabe89852c800116c3e67a19423 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 15:00:29 -0700 Subject: [PATCH 18/34] feature gate `DeferredMut` --- crates/bevy_ecs/src/query/fetch.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 36ce216def..9281fbf7be 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2490,6 +2490,9 @@ impl ReleaseStateQueryData for Has { } } +// gate `DeferredMut` behind support for Parallel +bevy_utils::cfg::parallel! { + /// Provides "fake" mutable access to the component `T` /// /// `DeferredMut` only accesses `&T` from the world, but when mutably @@ -2716,6 +2719,8 @@ unsafe impl<'__w, '__s, T: Component + Clone> QueryData for DeferredMut<'__w, '_ // applies those changes through commands. unsafe impl<'__w, '__s, T: Component + Clone> ReadOnlyQueryData for DeferredMut<'__w, '__s, T> {} +} + /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// /// `Query>` is equivalent to `Query<(Option<&A>, Option<&B>, Option<&mut C>), Or<(With, With, With)>>`. From d0659792c1475b4211d8fbc5182b2bfaf33f2b26 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 15:01:14 -0700 Subject: [PATCH 19/34] fix doc --- crates/bevy_ecs/src/query/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 9281fbf7be..f41e151fef 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2514,7 +2514,7 @@ bevy_utils::cfg::parallel! { /// # // # #[derive(Component)] /// # struct Poisoned; -/// #[derive(Component)] +/// #[derive(Component, Clone)] /// #[component(immutable)] /// struct Health(u32); /// From 4a81229e3ca0d976e35418d88221253584a11743 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 15:01:35 -0700 Subject: [PATCH 20/34] fix doc --- crates/bevy_ecs/src/query/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index f41e151fef..bc040c93aa 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2523,7 +2523,7 @@ bevy_utils::cfg::parallel! { /// health.0 -= 1; /// } /// } -/// # bevy_ecs::system::assert_is_system(tick_counters); +/// # bevy_ecs::system::assert_is_system(tick_poison); /// ``` /// /// # Footguns From 2e357992afe5cd2e19788cdb1f69d44b30fd4cc9 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 15:01:58 -0700 Subject: [PATCH 21/34] fix doc --- crates/bevy_ecs/src/query/fetch.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index bc040c93aa..5401b9fb74 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2509,6 +2509,7 @@ bevy_utils::cfg::parallel! { /// ``` /// # use bevy_ecs::component::Component; /// # use bevy_ecs::query::DeferredMut; + # use bevy_ecs::query::Has; /// # use bevy_ecs::system::IntoSystem; /// # use bevy_ecs::system::Query; /// # From b43fff53bbfd0c2c2a2d425cb554e6c7c5252d1c Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 15:04:26 -0700 Subject: [PATCH 22/34] fix feature gate --- crates/bevy_ecs/src/query/fetch.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 5401b9fb74..4e63a4584e 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -14,7 +14,9 @@ use crate::{ }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; use bevy_utils::prelude::DebugName; -use bevy_utils::Parallel; +bevy_utils::cfg::parallel! { + use bevy_utils::Parallel; +} use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; use smallvec::SmallVec; use variadics_please::all_tuples; From 56f17d20e90246a92c4b6c5d68aa5644cc4e8509 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 15:05:38 -0700 Subject: [PATCH 23/34] fix doc --- crates/bevy_ecs/src/query/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 4e63a4584e..6ab1c7017f 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2511,7 +2511,7 @@ bevy_utils::cfg::parallel! { /// ``` /// # use bevy_ecs::component::Component; /// # use bevy_ecs::query::DeferredMut; - # use bevy_ecs::query::Has; +/// # use bevy_ecs::query::Has; /// # use bevy_ecs::system::IntoSystem; /// # use bevy_ecs::system::Query; /// # From 58d9eab1924c0b650ce7573d501eb3c9506babfd Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 15:18:46 -0700 Subject: [PATCH 24/34] fix feature gate --- crates/bevy_ecs/src/query/fetch.rs | 456 ++++++++++++++--------------- 1 file changed, 226 insertions(+), 230 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 6ab1c7017f..212f553368 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -6,23 +6,17 @@ use crate::{ entity::{Entities, Entity, EntityHashMap, EntityLocation}, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, - system::{lifetimeless::Read, SystemMeta}, world::{ - unsafe_world_cell::UnsafeWorldCell, DeferredWorld, EntityMut, EntityMutExcept, EntityRef, - EntityRefExcept, FilteredEntityMut, FilteredEntityRef, Mut, Ref, World, + unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, + FilteredEntityMut, FilteredEntityRef, Mut, Ref, World, }, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; use bevy_utils::prelude::DebugName; -bevy_utils::cfg::parallel! { - use bevy_utils::Parallel; -} use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; use smallvec::SmallVec; use variadics_please::all_tuples; -use core::ops::{Deref, DerefMut}; - /// Types that can be fetched from a [`World`] using a [`Query`]. /// /// There are many types that natively implement this trait: @@ -2494,234 +2488,236 @@ impl ReleaseStateQueryData for Has { // gate `DeferredMut` behind support for Parallel bevy_utils::cfg::parallel! { + use core::ops::{Deref, DerefMut}; + use crate::{system::SystemMeta, world::DeferredWorld}; + use bevy_utils::Parallel; -/// Provides "fake" mutable access to the component `T` -/// -/// `DeferredMut` only accesses `&T` from the world, but when mutably -/// dereferenced will clone it and return a reference to that cloned value. -/// Once the `DeferredMut` is dropped, the query keeps track of the new value -/// and inserts it into the world at the next sync point. -/// -/// This can be used to "mutably" access immutable components! -/// However, this will still be slower than direct mutation, so this should -/// mainly be used for its ergonomics. -/// -/// # Examples -/// -/// ``` -/// # use bevy_ecs::component::Component; -/// # use bevy_ecs::query::DeferredMut; -/// # use bevy_ecs::query::Has; -/// # use bevy_ecs::system::IntoSystem; -/// # use bevy_ecs::system::Query; -/// # -// # #[derive(Component)] -/// # struct Poisoned; -/// #[derive(Component, Clone)] -/// #[component(immutable)] -/// struct Health(u32); -/// -/// fn tick_poison(mut health_query: Query<(DeferredMut, Has)>) { -/// for (mut health, is_poisoned) in health_query { -/// health.0 -= 1; -/// } -/// } -/// # bevy_ecs::system::assert_is_system(tick_poison); -/// ``` -/// -/// # Footguns -/// -/// It's possible to query multiple `DeferredMut` values from the same entity. -/// However, since mutations are deferred, each new value won't see the changes -/// applied to previous iterations. -/// -/// Normally, the final iteration will be the one that "wins" and gets inserted -/// onto the entity, but parallelism can mess with that, too. Since `DeferredMut` -/// internally uses a thread-local [`EntityHashMap`] to keep track of mutations, -/// if two `DeferredMut` values for the same entity are created in the same system -/// on different threads, then they'll each be inserted into the entity in an -/// undetermined order. -pub struct DeferredMut<'w, 's, T: Component + Clone> { - entity: Entity, - old: &'w T, - new: Option, - record: &'s DeferredMutations, -} - -impl<'w, 's, T: Component + Clone> DeferredMut<'w, 's, T> { - /// Returns a reference to the `T` value still present in the ECS - pub fn stale(&self) -> &'w T { - self.old - } - - /// Returns a reference to the `T` value currently being updated. - /// If none is present yet, this method will clone from `Self::stale` - pub fn fresh(&mut self) -> &mut T { - self.new.get_or_insert(self.old.clone()) - } - - /// Returns a (possibly absent) reference to the `T` value currently being updated. - pub fn try_get_fresh(&mut self) -> Option<&mut T> { - self.new.as_mut() - } -} - -impl<'w, 's, T: Component + Clone> Drop for DeferredMut<'w, 's, T> { - fn drop(&mut self) { - if let Some(new) = self.new.take() { - self.record.insert(self.entity, new); - } - } -} - -impl<'w, 's, T: Component + Clone> Deref for DeferredMut<'w, 's, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.new.as_ref().unwrap_or(self.old) - } -} - -impl<'w, 's, T: Component + Clone> DerefMut for DeferredMut<'w, 's, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.fresh() - } -} - -/// The [`WorldQuery::State`] type for [`DeferredMut`] -pub struct DeferredMutState { - component_id: ComponentId, - record: DeferredMutations, -} - -struct DeferredMutations(Parallel>); - -impl Default for DeferredMutations { - fn default() -> Self { - Self(Default::default()) - } -} - -impl DeferredMutations { - fn insert(&self, entity: Entity, component: T) { - self.0.scope(|map| map.insert(entity, component)); - } - - fn drain(&mut self) -> impl Iterator { - self.0.drain() - } -} - -// SAFETY: impl defers to `<&T as WorldQuery>` for all methods -unsafe impl<'__w, '__s, T: Component + Clone> WorldQuery for DeferredMut<'__w, '__s, T> { - type Fetch<'w> = ReadFetch<'w, T>; - - type State = DeferredMutState; - - fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { - fetch - } - - unsafe fn init_fetch<'w, 's>( - world: UnsafeWorldCell<'w>, - state: &'s Self::State, - last_run: Tick, - this_run: Tick, - ) -> Self::Fetch<'w> { - // SAFETY: invariants are upheld by the caller - unsafe { <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) } - } - - const IS_DENSE: bool = true; - - unsafe fn set_archetype<'w>( - fetch: &mut Self::Fetch<'w>, - state: &Self::State, - archetype: &'w Archetype, - table: &'w Table, - ) { - <&T as WorldQuery>::set_archetype(fetch, &state.component_id, archetype, table); - } - - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { - <&T as WorldQuery>::set_table(fetch, &state.component_id, table); - } - - fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { - <&T as WorldQuery>::update_component_access(&state.component_id, access); - } - - fn init_state(world: &mut World) -> Self::State { - DeferredMutState { - component_id: world.register_component::(), - record: Default::default(), - } - } - - fn get_state(components: &Components) -> Option { - Some(DeferredMutState { - component_id: components.component_id::()?, - record: Default::default(), - }) - } - - fn matches_component_set( - state: &Self::State, - set_contains_id: &impl Fn(ComponentId) -> bool, - ) -> bool { - <&T as WorldQuery>::matches_component_set(&state.component_id, set_contains_id) - } - - fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { - world.insert_batch(state.record.drain()); - } - - fn queue(state: &mut Self::State, _system_meta: &SystemMeta, mut world: DeferredWorld) { - world - .commands() - .insert_batch(state.record.drain().collect::>()); - } -} - -// SAFETY: DeferredMut defers to &T internally, so it must be readonly and Self::ReadOnly = Self. -unsafe impl<'__w, '__s, T: Component + Clone> QueryData for DeferredMut<'__w, '__s, T> { - const IS_READ_ONLY: bool = true; - - type ReadOnly = Self; - - type Item<'w, 's> = DeferredMut<'w, 's, T>; - - fn shrink<'wlong: 'wshort, 'wshort, 's>( - item: Self::Item<'wlong, 's>, - ) -> Self::Item<'wshort, 's> { - item - } - - unsafe fn fetch<'w, 's>( - state: &'s Self::State, - fetch: &mut Self::Fetch<'w>, + /// Provides "fake" mutable access to the component `T` + /// + /// `DeferredMut` only accesses `&T` from the world, but when mutably + /// dereferenced will clone it and return a reference to that cloned value. + /// Once the `DeferredMut` is dropped, the query keeps track of the new value + /// and inserts it into the world at the next sync point. + /// + /// This can be used to "mutably" access immutable components! + /// However, this will still be slower than direct mutation, so this should + /// mainly be used for its ergonomics. + /// + /// # Examples + /// + /// ``` + /// # use bevy_ecs::component::Component; + /// # use bevy_ecs::query::DeferredMut; + /// # use bevy_ecs::query::Has; + /// # use bevy_ecs::system::IntoSystem; + /// # use bevy_ecs::system::Query; + /// # + // # #[derive(Component)] + /// # struct Poisoned; + /// #[derive(Component, Clone)] + /// #[component(immutable)] + /// struct Health(u32); + /// + /// fn tick_poison(mut health_query: Query<(DeferredMut, Has)>) { + /// for (mut health, is_poisoned) in health_query { + /// health.0 -= 1; + /// } + /// } + /// # bevy_ecs::system::assert_is_system(tick_poison); + /// ``` + /// + /// # Footguns + /// + /// It's possible to query multiple `DeferredMut` values from the same entity. + /// However, since mutations are deferred, each new value won't see the changes + /// applied to previous iterations. + /// + /// Normally, the final iteration will be the one that "wins" and gets inserted + /// onto the entity, but parallelism can mess with that, too. Since `DeferredMut` + /// internally uses a thread-local [`EntityHashMap`] to keep track of mutations, + /// if two `DeferredMut` values for the same entity are created in the same system + /// on different threads, then they'll each be inserted into the entity in an + /// undetermined order. + pub struct DeferredMut<'w, 's, T: Component + Clone> { entity: Entity, - table_row: TableRow, - ) -> Self::Item<'w, 's> { - // SAFETY: invariants are upheld by the caller - let old = - unsafe { as QueryData>::fetch(&state.component_id, fetch, entity, table_row) }; - DeferredMut { - entity, - old, - // NOTE: we could try to get an existing updated component from the record, - // but we can't reliably do that across all threads. Better to say that all - // newly-created DeferredMut values will match what's in the ECS. - new: None, - record: &state.record, + old: &'w T, + new: Option, + record: &'s DeferredMutations, + } + + impl<'w, 's, T: Component + Clone> DeferredMut<'w, 's, T> { + /// Returns a reference to the `T` value still present in the ECS + pub fn stale(&self) -> &'w T { + self.old + } + + /// Returns a reference to the `T` value currently being updated. + /// If none is present yet, this method will clone from `Self::stale` + pub fn fresh(&mut self) -> &mut T { + self.new.get_or_insert(self.old.clone()) + } + + /// Returns a (possibly absent) reference to the `T` value currently being updated. + pub fn try_get_fresh(&mut self) -> Option<&mut T> { + self.new.as_mut() } } -} -// SAFETY: Tracked only accesses &T from the world. Though it provides mutable access, it only -// applies those changes through commands. -unsafe impl<'__w, '__s, T: Component + Clone> ReadOnlyQueryData for DeferredMut<'__w, '__s, T> {} + impl<'w, 's, T: Component + Clone> Drop for DeferredMut<'w, 's, T> { + fn drop(&mut self) { + if let Some(new) = self.new.take() { + self.record.insert(self.entity, new); + } + } + } + impl<'w, 's, T: Component + Clone> Deref for DeferredMut<'w, 's, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.new.as_ref().unwrap_or(self.old) + } + } + + impl<'w, 's, T: Component + Clone> DerefMut for DeferredMut<'w, 's, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.fresh() + } + } + + /// The [`WorldQuery::State`] type for [`DeferredMut`] + pub struct DeferredMutState { + component_id: ComponentId, + record: DeferredMutations, + } + + struct DeferredMutations(Parallel>); + + impl Default for DeferredMutations { + fn default() -> Self { + Self(Default::default()) + } + } + + impl DeferredMutations { + fn insert(&self, entity: Entity, component: T) { + self.0.scope(|map| map.insert(entity, component)); + } + + fn drain(&mut self) -> impl Iterator { + self.0.drain() + } + } + + // SAFETY: impl defers to `<&T as WorldQuery>` for all methods + unsafe impl<'__w, '__s, T: Component + Clone> WorldQuery for DeferredMut<'__w, '__s, T> { + type Fetch<'w> = ReadFetch<'w, T>; + + type State = DeferredMutState; + + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + + unsafe fn init_fetch<'w, 's>( + world: UnsafeWorldCell<'w>, + state: &'s Self::State, + last_run: Tick, + this_run: Tick, + ) -> Self::Fetch<'w> { + // SAFETY: invariants are upheld by the caller + unsafe { <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) } + } + + const IS_DENSE: bool = true; + + unsafe fn set_archetype<'w>( + fetch: &mut Self::Fetch<'w>, + state: &Self::State, + archetype: &'w Archetype, + table: &'w Table, + ) { + <&T as WorldQuery>::set_archetype(fetch, &state.component_id, archetype, table); + } + + unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + <&T as WorldQuery>::set_table(fetch, &state.component_id, table); + } + + fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { + <&T as WorldQuery>::update_component_access(&state.component_id, access); + } + + fn init_state(world: &mut World) -> Self::State { + DeferredMutState { + component_id: world.register_component::(), + record: Default::default(), + } + } + + fn get_state(components: &Components) -> Option { + Some(DeferredMutState { + component_id: components.component_id::()?, + record: Default::default(), + }) + } + + fn matches_component_set( + state: &Self::State, + set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + <&T as WorldQuery>::matches_component_set(&state.component_id, set_contains_id) + } + + fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { + world.insert_batch(state.record.drain()); + } + + fn queue(state: &mut Self::State, _system_meta: &SystemMeta, mut world: DeferredWorld) { + world + .commands() + .insert_batch(state.record.drain().collect::>()); + } + } + + // SAFETY: DeferredMut defers to &T internally, so it must be readonly and Self::ReadOnly = Self. + unsafe impl<'__w, '__s, T: Component + Clone> QueryData for DeferredMut<'__w, '__s, T> { + const IS_READ_ONLY: bool = true; + + type ReadOnly = Self; + + type Item<'w, 's> = DeferredMut<'w, 's, T>; + + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + item + } + + unsafe fn fetch<'w, 's>( + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entity: Entity, + table_row: TableRow, + ) -> Self::Item<'w, 's> { + // SAFETY: invariants are upheld by the caller + let old = + unsafe { <&T as QueryData>::fetch(&state.component_id, fetch, entity, table_row) }; + DeferredMut { + entity, + old, + // NOTE: we could try to get an existing updated component from the record, + // but we can't reliably do that across all threads. Better to say that all + // newly-created DeferredMut values will match what's in the ECS. + new: None, + record: &state.record, + } + } + } + + // SAFETY: Tracked only accesses &T from the world. Though it provides mutable access, it only + // applies those changes through commands. + unsafe impl<'__w, '__s, T: Component + Clone> ReadOnlyQueryData for DeferredMut<'__w, '__s, T> {} } /// The `AnyOf` query parameter fetches entities with any of the component types included in T. From 075de1fb4b4851e16f93d7bf7afc76b0e771d181 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 19 Jun 2025 15:32:37 -0700 Subject: [PATCH 25/34] fix imports and docs --- crates/bevy_ecs/src/query/fetch.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 212f553368..6fff5b21f2 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -3,7 +3,7 @@ use crate::{ bundle::Bundle, change_detection::{MaybeLocation, Ticks, TicksMut}, component::{Component, ComponentId, Components, Mutable, StorageType, Tick}, - entity::{Entities, Entity, EntityHashMap, EntityLocation}, + entity::{Entities, Entity, EntityLocation}, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, world::{ @@ -2489,7 +2489,7 @@ impl ReleaseStateQueryData for Has { // gate `DeferredMut` behind support for Parallel bevy_utils::cfg::parallel! { use core::ops::{Deref, DerefMut}; - use crate::{system::SystemMeta, world::DeferredWorld}; + use crate::{system::SystemMeta, entity::EntityHashMap, world::DeferredWorld}; use bevy_utils::Parallel; /// Provides "fake" mutable access to the component `T` @@ -2519,7 +2519,7 @@ bevy_utils::cfg::parallel! { /// struct Health(u32); /// /// fn tick_poison(mut health_query: Query<(DeferredMut, Has)>) { - /// for (mut health, is_poisoned) in health_query { + /// for (mut health, is_poisoned) in &health_query { /// health.0 -= 1; /// } /// } From 8c80cf8c044d53eaf5af377a6147dc6ff5148f36 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Fri, 20 Jun 2025 01:04:35 -0700 Subject: [PATCH 26/34] fix docs --- crates/bevy_ecs/src/query/fetch.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 6fff5b21f2..ff72259f58 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2508,18 +2508,19 @@ bevy_utils::cfg::parallel! { /// ``` /// # use bevy_ecs::component::Component; /// # use bevy_ecs::query::DeferredMut; - /// # use bevy_ecs::query::Has; + /// # use bevy_ecs::query::With; /// # use bevy_ecs::system::IntoSystem; /// # use bevy_ecs::system::Query; /// # - // # #[derive(Component)] + /// # #[derive(Component)] /// # struct Poisoned; + /// # /// #[derive(Component, Clone)] /// #[component(immutable)] /// struct Health(u32); /// - /// fn tick_poison(mut health_query: Query<(DeferredMut, Has)>) { - /// for (mut health, is_poisoned) in &health_query { + /// fn tick_poison(mut health_query: Query, With>) { + /// for mut health in &health_query { /// health.0 -= 1; /// } /// } From 4202bd4dd1e1cc392cf13ebbc242df320b12f2cd Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 22 Jun 2025 17:35:40 -0700 Subject: [PATCH 27/34] update docs --- crates/bevy_ecs/src/query/fetch.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index ff72259f58..beca1a03b6 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2529,7 +2529,11 @@ bevy_utils::cfg::parallel! { /// /// # Footguns /// - /// It's possible to query multiple `DeferredMut` values from the same entity. + /// 1. The mutations tracked by `DeferredMut` will *not* be applied if used + /// through the manual [`QueryState`](super::QueryState) API. Instead, it + /// should be used through a query in a system param or [`SystemState`](crate::system::SystemState). + /// + /// 2. It's possible to query multiple `DeferredMut` values from the same entity. /// However, since mutations are deferred, each new value won't see the changes /// applied to previous iterations. /// From e79bed351b12cf1182ce920e5d3aa607027eeb47 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Wed, 25 Jun 2025 09:43:01 -0700 Subject: [PATCH 28/34] Update crates/bevy_ecs/src/query/fetch.rs Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com> --- crates/bevy_ecs/src/query/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index beca1a03b6..4682659cd1 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2634,7 +2634,7 @@ bevy_utils::cfg::parallel! { unsafe { <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) } } - const IS_DENSE: bool = true; + const IS_DENSE: bool = <&T as WorldQuery>::IS_DENSE; unsafe fn set_archetype<'w>( fetch: &mut Self::Fetch<'w>, From 5a10368e8fe937f034d7be10b8c7585a15e461a8 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Wed, 25 Jun 2025 09:53:53 -0700 Subject: [PATCH 29/34] defer all the things --- crates/bevy_ecs/src/query/fetch.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 4682659cd1..3a4551f3cc 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2592,7 +2592,7 @@ bevy_utils::cfg::parallel! { /// The [`WorldQuery::State`] type for [`DeferredMut`] pub struct DeferredMutState { - component_id: ComponentId, + internal: <&'static T as WorldQuery>::State, record: DeferredMutations, } @@ -2631,7 +2631,7 @@ bevy_utils::cfg::parallel! { this_run: Tick, ) -> Self::Fetch<'w> { // SAFETY: invariants are upheld by the caller - unsafe { <&T as WorldQuery>::init_fetch(world, &state.component_id, last_run, this_run) } + unsafe { <&T as WorldQuery>::init_fetch(world, &state.internal, last_run, this_run) } } const IS_DENSE: bool = <&T as WorldQuery>::IS_DENSE; @@ -2642,27 +2642,27 @@ bevy_utils::cfg::parallel! { archetype: &'w Archetype, table: &'w Table, ) { - <&T as WorldQuery>::set_archetype(fetch, &state.component_id, archetype, table); + <&T as WorldQuery>::set_archetype(fetch, &state.internal, archetype, table); } unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { - <&T as WorldQuery>::set_table(fetch, &state.component_id, table); + <&T as WorldQuery>::set_table(fetch, &state.internal, table); } fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { - <&T as WorldQuery>::update_component_access(&state.component_id, access); + <&T as WorldQuery>::update_component_access(&state.internal, access); } fn init_state(world: &mut World) -> Self::State { DeferredMutState { - component_id: world.register_component::(), + internal: <&T as WorldQuery>::init_state(world), record: Default::default(), } } fn get_state(components: &Components) -> Option { Some(DeferredMutState { - component_id: components.component_id::()?, + internal: <&T as WorldQuery>::get_state(components)?, record: Default::default(), }) } @@ -2671,7 +2671,7 @@ bevy_utils::cfg::parallel! { state: &Self::State, set_contains_id: &impl Fn(ComponentId) -> bool, ) -> bool { - <&T as WorldQuery>::matches_component_set(&state.component_id, set_contains_id) + <&T as WorldQuery>::matches_component_set(&state.internal, set_contains_id) } fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { @@ -2707,7 +2707,7 @@ bevy_utils::cfg::parallel! { ) -> Self::Item<'w, 's> { // SAFETY: invariants are upheld by the caller let old = - unsafe { <&T as QueryData>::fetch(&state.component_id, fetch, entity, table_row) }; + unsafe { <&T as QueryData>::fetch(&state.internal, fetch, entity, table_row) }; DeferredMut { entity, old, From cff7af21ecfff0e463d1f9d98c26f080bebe1755 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Wed, 25 Jun 2025 10:29:44 -0700 Subject: [PATCH 30/34] defer even more things --- crates/bevy_ecs/src/query/fetch.rs | 20 ++++++++++++++++++++ crates/bevy_ecs/src/query/filter.rs | 15 ++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 3a4551f3cc..1135535785 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2275,6 +2275,14 @@ unsafe impl WorldQuery for Option { ) -> bool { true } + + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + ::apply(state, system_meta, world); + } + + fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) { + ::queue(state, system_meta, world); + } } // SAFETY: defers to soundness of `T: WorldQuery` impl @@ -2822,6 +2830,10 @@ macro_rules! impl_anytuple_fetch { unused_variables, reason = "Zero-length tuples won't use any of the parameters." )] + #[allow( + unused_mut, + reason = "Zero-length tuples won't access any of the parameters mutably." + )] #[allow( clippy::unused_unit, reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." @@ -2919,6 +2931,14 @@ macro_rules! impl_anytuple_fetch { let ($($name,)*) = _state; false $(|| $name::matches_component_set($name, _set_contains_id))* } + + fn apply(($($state,)*): &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + $(<$name as WorldQuery>::apply($state, system_meta, world);)* + } + + fn queue(($($state,)*): &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + $(<$name as WorldQuery>::queue($state, system_meta, world.reborrow());)* + } } #[expect( diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 312b330e04..15f0f469eb 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -4,7 +4,8 @@ use crate::{ entity::{Entities, Entity}, query::{DebugCheckedUnwrap, FilteredAccess, StorageSwitch, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, - world::{unsafe_world_cell::UnsafeWorldCell, World}, + system::SystemMeta, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; use bevy_utils::prelude::DebugName; @@ -378,6 +379,10 @@ macro_rules! impl_or_query_filter { unused_variables, reason = "Zero-length tuples won't use any of the parameters." )] + #[allow( + unused_mut, + reason = "Zero-length tuples won't access any of the parameters mutably." + )] #[allow( clippy::unused_unit, reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." @@ -478,6 +483,14 @@ macro_rules! impl_or_query_filter { let ($($filter,)*) = state; false $(|| $filter::matches_component_set($filter, set_contains_id))* } + + fn apply(($($state,)*): &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + $(<$filter as WorldQuery>::apply($state, system_meta, world);)* + } + + fn queue(($($state,)*): &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + $(<$filter as WorldQuery>::queue($state, system_meta, world.reborrow());)* + } } #[expect( From 7cbb656588323ced685f199f9743914f5aa14555 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Wed, 25 Jun 2025 10:43:12 -0700 Subject: [PATCH 31/34] remove extra clone bounds --- crates/bevy_ecs/src/query/fetch.rs | 47 +++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 1135535785..941c9657cf 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2551,32 +2551,49 @@ bevy_utils::cfg::parallel! { /// if two `DeferredMut` values for the same entity are created in the same system /// on different threads, then they'll each be inserted into the entity in an /// undetermined order. - pub struct DeferredMut<'w, 's, T: Component + Clone> { + pub struct DeferredMut<'w, 's, T: Component> { entity: Entity, old: &'w T, new: Option, record: &'s DeferredMutations, } - impl<'w, 's, T: Component + Clone> DeferredMut<'w, 's, T> { + impl<'w, 's, T: Component> DeferredMut<'w, 's, T> { /// Returns a reference to the `T` value still present in the ECS + #[inline] pub fn stale(&self) -> &'w T { self.old } /// Returns a reference to the `T` value currently being updated. /// If none is present yet, this method will clone from `Self::stale` - pub fn fresh(&mut self) -> &mut T { - self.new.get_or_insert(self.old.clone()) + #[inline] + pub fn fresh(&mut self) -> &mut T where T: Clone { + self.get_fresh_or_insert(self.old.clone()) } /// Returns a (possibly absent) reference to the `T` value currently being updated. - pub fn try_get_fresh(&mut self) -> Option<&mut T> { + #[inline] + pub fn get_fresh(&mut self) -> Option<&mut T> { self.new.as_mut() } + + /// Returns a reference to the `T` value currently being updated. + /// If absent, it will insert the provided value. + #[inline] + pub fn get_fresh_or_insert(&mut self, value: T) -> &mut T { + self.new.get_or_insert(value) + } + + /// Replaces the `T` value currently being updated + #[inline] + pub fn insert(&mut self, value: T) { + self.new = Some(value); + } } - impl<'w, 's, T: Component + Clone> Drop for DeferredMut<'w, 's, T> { + impl<'w, 's, T: Component> Drop for DeferredMut<'w, 's, T> { + #[inline] fn drop(&mut self) { if let Some(new) = self.new.take() { self.record.insert(self.entity, new); @@ -2584,46 +2601,50 @@ bevy_utils::cfg::parallel! { } } - impl<'w, 's, T: Component + Clone> Deref for DeferredMut<'w, 's, T> { + impl<'w, 's, T: Component> Deref for DeferredMut<'w, 's, T> { type Target = T; + #[inline] fn deref(&self) -> &Self::Target { self.new.as_ref().unwrap_or(self.old) } } impl<'w, 's, T: Component + Clone> DerefMut for DeferredMut<'w, 's, T> { + #[inline] fn deref_mut(&mut self) -> &mut Self::Target { self.fresh() } } /// The [`WorldQuery::State`] type for [`DeferredMut`] - pub struct DeferredMutState { + pub struct DeferredMutState { internal: <&'static T as WorldQuery>::State, record: DeferredMutations, } struct DeferredMutations(Parallel>); - impl Default for DeferredMutations { + impl Default for DeferredMutations { fn default() -> Self { Self(Default::default()) } } - impl DeferredMutations { + impl DeferredMutations { + #[inline] fn insert(&self, entity: Entity, component: T) { self.0.scope(|map| map.insert(entity, component)); } + #[inline] fn drain(&mut self) -> impl Iterator { self.0.drain() } } // SAFETY: impl defers to `<&T as WorldQuery>` for all methods - unsafe impl<'__w, '__s, T: Component + Clone> WorldQuery for DeferredMut<'__w, '__s, T> { + unsafe impl<'__w, '__s, T: Component> WorldQuery for DeferredMut<'__w, '__s, T> { type Fetch<'w> = ReadFetch<'w, T>; type State = DeferredMutState; @@ -2694,7 +2715,7 @@ bevy_utils::cfg::parallel! { } // SAFETY: DeferredMut defers to &T internally, so it must be readonly and Self::ReadOnly = Self. - unsafe impl<'__w, '__s, T: Component + Clone> QueryData for DeferredMut<'__w, '__s, T> { + unsafe impl<'__w, '__s, T: Component> QueryData for DeferredMut<'__w, '__s, T> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; @@ -2730,7 +2751,7 @@ bevy_utils::cfg::parallel! { // SAFETY: Tracked only accesses &T from the world. Though it provides mutable access, it only // applies those changes through commands. - unsafe impl<'__w, '__s, T: Component + Clone> ReadOnlyQueryData for DeferredMut<'__w, '__s, T> {} + unsafe impl<'__w, '__s, T: Component> ReadOnlyQueryData for DeferredMut<'__w, '__s, T> {} } /// The `AnyOf` query parameter fetches entities with any of the component types included in T. From efaca422556bbd305b959e56882c2bbaa96344d1 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Wed, 25 Jun 2025 11:35:09 -0700 Subject: [PATCH 32/34] fix imports --- crates/bevy_ecs/src/query/fetch.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 2525c3b7cd..001eae5213 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -6,9 +6,10 @@ use crate::{ entity::{Entities, Entity, EntityLocation}, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, + system::SystemMeta, world::{ - unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, - FilteredEntityMut, FilteredEntityRef, Mut, Ref, World, + unsafe_world_cell::UnsafeWorldCell, DeferredWorld, EntityMut, EntityMutExcept, EntityRef, + EntityRefExcept, FilteredEntityMut, FilteredEntityRef, Mut, Ref, World, }, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; @@ -2500,7 +2501,7 @@ impl ReleaseStateQueryData for Has { // gate `DeferredMut` behind support for Parallel bevy_utils::cfg::parallel! { use core::ops::{Deref, DerefMut}; - use crate::{system::SystemMeta, entity::EntityHashMap, world::DeferredWorld}; + use crate::entity::EntityHashMap; use bevy_utils::Parallel; /// Provides "fake" mutable access to the component `T` From 548ec53da3ada89ef351c35887fc3543075c6e07 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 26 Jun 2025 14:23:27 -0700 Subject: [PATCH 33/34] fix not setting has_deferred --- crates/bevy_ecs/src/query/fetch.rs | 6 ++++++ crates/bevy_ecs/src/query/filter.rs | 2 ++ crates/bevy_ecs/src/query/world_query.rs | 11 +++++++++++ crates/bevy_ecs/src/system/system_param.rs | 4 ++++ 4 files changed, 23 insertions(+) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 001eae5213..215a01cac2 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2280,6 +2280,8 @@ unsafe impl WorldQuery for Option { true } + const HAS_DEFERRED: bool = T::HAS_DEFERRED; + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { ::apply(state, system_meta, world); } @@ -2707,6 +2709,8 @@ bevy_utils::cfg::parallel! { <&T as WorldQuery>::matches_component_set(&state.internal, set_contains_id) } + const HAS_DEFERRED: bool = true; + fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { world.insert_batch(state.record.drain()); } @@ -2957,6 +2961,8 @@ macro_rules! impl_anytuple_fetch { false $(|| $name::matches_component_set($name, _set_contains_id))* } + const HAS_DEFERRED: bool = false $(|| $name::HAS_DEFERRED)*; + fn apply(($($state,)*): &mut Self::State, system_meta: &SystemMeta, world: &mut World) { $(<$name as WorldQuery>::apply($state, system_meta, world);)* } diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 8ec40f3411..c0d32294d2 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -484,6 +484,8 @@ macro_rules! impl_or_query_filter { false $(|| $filter::matches_component_set($filter, set_contains_id))* } + const HAS_DEFERRED: bool = false $(|| $filter::HAS_DEFERRED)*; + fn apply(($($state,)*): &mut Self::State, system_meta: &SystemMeta, world: &mut World) { $(<$filter as WorldQuery>::apply($state, system_meta, world);)* } diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index 927a7cd803..669c271d24 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -122,9 +122,16 @@ pub unsafe trait WorldQuery { /// access to [`Components`]. fn get_state(components: &Components) -> Option; + /// Returns true if (and only if) this [`WorldQuery`] contains deferred state + /// that needs to be applied at the next sync point. If this is set to `false`, + /// `apply` or `queue` may not be called. + const HAS_DEFERRED: bool = false; + /// Applies any deferred mutations stored in this [`WorldQuery`]'s state. /// This is used to apply [`Commands`] during [`ApplyDeferred`](crate::prelude::ApplyDeferred). /// + /// If this is not a no-op, then `HAS_DEFERRED` should be `true` + /// /// [`Commands`]: crate::prelude::Commands #[inline] #[expect( @@ -134,6 +141,8 @@ pub unsafe trait WorldQuery { fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {} /// Queues any deferred mutations to be applied at the next [`ApplyDeferred`](crate::prelude::ApplyDeferred). + /// + /// If this is not a no-op, then `HAS_DEFERRED` should be `true` #[inline] #[expect( unused_variables, @@ -231,6 +240,8 @@ macro_rules! impl_tuple_world_query { Some(($($name::get_state(components)?,)*)) } + const HAS_DEFERRED: bool = false $(|| $name::HAS_DEFERRED)*; + #[inline] fn apply(($($name,)*): &mut Self::State, system_meta: &SystemMeta, world: &mut World) { $($name::apply($name, system_meta, world);)* diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index cc798cafa6..a2fbc56a98 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -351,6 +351,10 @@ unsafe impl SystemParam for Qu world, ); component_access_set.add(state.component_access.clone()); + + if D::HAS_DEFERRED || F::HAS_DEFERRED { + system_meta.set_has_deferred(); + } } #[inline] From 0011d170bd4ca6b931058b2647788879bd63f914 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 26 Jun 2025 14:40:40 -0700 Subject: [PATCH 34/34] add release note --- .../release-notes/new_querydata.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 release-content/release-notes/new_querydata.md diff --git a/release-content/release-notes/new_querydata.md b/release-content/release-notes/new_querydata.md new file mode 100644 index 0000000000..ad94e56439 --- /dev/null +++ b/release-content/release-notes/new_querydata.md @@ -0,0 +1,45 @@ +--- +title: New QueryData Types +authors: ["@ecoskey"] +pull_requests: [19602] +--- + +Bevy queries have some new powers for advanced users. Namely, custom `WorldQuery` +implementations can store and apply "deferred" mutations, just like `Commands`! +This release includes a few new types making use of this capability, and +we're sure third-party crates will find all kinds of new ways to do cool stuff +with this. + +## `DeferredMut` + +When working with immutable components in Bevy, the acts of reading and writing +component values are very clearly separated. This can be valuable, especially +if a component has expensive hooks or observers attached and `insert`ing it +has a significant cost, but in some cases it can feel like boilerplate. + +`DeferredMut` is meant to improve the ergonomics of the latter case, by providing +"fake" mutable access to any component, even immutable ones! Internally, it +keeps track of any modifications and inserts them into the ECS at the next +sync point. + +```rs +// without `DeferredMut` +pub fn tick_poison( + mut commands: Commands, + query: Query<(Entity, &Health), With> +) { + for (entity, Health(health_points)) in query { + commands.insert(entity, Health(health_points - 1)) + } +} + +// with `DeferredMut` +pub fn tick_poison( + mut health_query: Query, With> +) { + for mut health in &health_query { + health.0 -= 1; + } +} + +```