From 39bf69f6af0dd54a555f17aca9beb70abf1d8398 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:18:45 +0100 Subject: [PATCH 01/52] contiguous query data/filter --- .../iteration/iter_simple_contiguous.rs | 50 +++ .../iteration/iter_simple_contiguous_avx2.rs | 66 ++++ .../iteration/iter_simple_no_detection.rs | 42 +++ .../iter_simple_no_detection_contiguous.rs | 45 +++ benches/benches/bevy_ecs/iteration/mod.rs | 22 ++ crates/bevy_ecs/Cargo.toml | 4 + .../bevy_ecs/src/change_detection/params.rs | 60 +++- crates/bevy_ecs/src/query/fetch.rs | 295 +++++++++++++++++- crates/bevy_ecs/src/query/filter.rs | 117 +++++++ crates/bevy_ecs/src/query/iter.rs | 75 ++++- crates/bevy_ecs/src/query/mod.rs | 1 + crates/bevy_ecs/src/query/table_query.rs | 0 crates/bevy_ptr/src/lib.rs | 59 ++++ 13 files changed, 833 insertions(+), 3 deletions(-) create mode 100644 benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs create mode 100644 benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs create mode 100644 benches/benches/bevy_ecs/iteration/iter_simple_no_detection.rs create mode 100644 benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs create mode 100644 crates/bevy_ecs/src/query/table_query.rs diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs new file mode 100644 index 0000000000000..5df29bcd38029 --- /dev/null +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs @@ -0,0 +1,50 @@ +use bevy_ecs::prelude::*; +use glam::*; + +#[derive(Component, Copy, Clone)] +struct Transform(Mat4); + +#[derive(Component, Copy, Clone)] +struct Position(Vec3); + +#[derive(Component, Copy, Clone)] +struct Rotation(Vec3); + +#[derive(Component, Copy, Clone)] +struct Velocity(Vec3); + +pub struct Benchmark<'w>(World, QueryState<(&'w Velocity, &'w mut Position)>); + +impl<'w> Benchmark<'w> { + pub fn new() -> Self { + let mut world = World::new(); + + world.spawn_batch(core::iter::repeat_n( + ( + Transform(Mat4::from_scale(Vec3::ONE)), + Position(Vec3::X), + Rotation(Vec3::X), + Velocity(Vec3::X), + ), + 10_000, + )); + + let query = world.query::<(&Velocity, &mut Position)>(); + Self(world, query) + } + + #[inline(never)] + pub fn run(&mut self) { + let mut iter = self.1.iter_mut(&mut self.0); + while let Some((velocity, (position, mut ticks))) = iter.next_contiguous() { + for (v, p) in velocity.iter().zip(position.iter_mut()) { + p.0 += v.0; + } + let tick = ticks.this_run(); + // to match the iter_simple benchmark + for t in ticks.get_changed_ticks_mut() { + *t = tick; + } + } + } +} diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs new file mode 100644 index 0000000000000..7da01fc1dfacd --- /dev/null +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs @@ -0,0 +1,66 @@ +use bevy_ecs::prelude::*; +use glam::*; + +#[derive(Component, Copy, Clone)] +struct Transform(Mat4); + +#[derive(Component, Copy, Clone)] +struct Position(Vec3); + +#[derive(Component, Copy, Clone)] +struct Rotation(Vec3); + +#[derive(Component, Copy, Clone)] +struct Velocity(Vec3); + +pub struct Benchmark<'w>(World, QueryState<(&'w Velocity, &'w mut Position)>); + +impl<'w> Benchmark<'w> { + pub fn supported() -> bool { + is_x86_feature_detected!("avx2") + } + + pub fn new() -> Option { + if !Self::supported() { + return None; + } + + let mut world = World::new(); + + world.spawn_batch(core::iter::repeat_n( + ( + Transform(Mat4::from_scale(Vec3::ONE)), + Position(Vec3::X), + Rotation(Vec3::X), + Velocity(Vec3::X), + ), + 10_000, + )); + + let query = world.query::<(&Velocity, &mut Position)>(); + Some(Self(world, query)) + } + + #[inline(never)] + pub fn run(&mut self) { + #[target_feature(enable = "avx2")] + fn exec(position: &mut [Position], velocity: &[Velocity]) { + for i in 0..position.len() { + position[i].0 += velocity[i].0; + } + } + + let mut iter = self.1.iter_mut(&mut self.0); + while let Some((velocity, (position, mut ticks))) = iter.next_contiguous() { + // SAFETY: checked in new + unsafe { + exec(position, velocity); + } + let tick = ticks.this_run(); + // to match the iter_simple benchmark + for t in ticks.get_changed_ticks_mut() { + *t = tick; + } + } + } +} diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection.rs b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection.rs new file mode 100644 index 0000000000000..7381d3a5db67e --- /dev/null +++ b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection.rs @@ -0,0 +1,42 @@ +use bevy_ecs::prelude::*; +use glam::*; + +#[derive(Component, Copy, Clone)] +struct Transform(Mat4); + +#[derive(Component, Copy, Clone)] +struct Position(Vec3); + +#[derive(Component, Copy, Clone)] +struct Rotation(Vec3); + +#[derive(Component, Copy, Clone)] +struct Velocity(Vec3); + +pub struct Benchmark<'w>(World, QueryState<(&'w Velocity, &'w mut Position)>); + +impl<'w> Benchmark<'w> { + pub fn new() -> Self { + let mut world = World::new(); + + world.spawn_batch(core::iter::repeat_n( + ( + Transform(Mat4::from_scale(Vec3::ONE)), + Position(Vec3::X), + Rotation(Vec3::X), + Velocity(Vec3::X), + ), + 10_000, + )); + + let query = world.query::<(&Velocity, &mut Position)>(); + Self(world, query) + } + + #[inline(never)] + pub fn run(&mut self) { + for (velocity, mut position) in self.1.iter_mut(&mut self.0) { + position.bypass_change_detection().0 += velocity.0; + } + } +} diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs new file mode 100644 index 0000000000000..d2a00c1472c89 --- /dev/null +++ b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs @@ -0,0 +1,45 @@ +use bevy_ecs::prelude::*; +use glam::*; + +#[derive(Component, Copy, Clone)] +struct Transform(Mat4); + +#[derive(Component, Copy, Clone)] +struct Position(Vec3); + +#[derive(Component, Copy, Clone)] +struct Rotation(Vec3); + +#[derive(Component, Copy, Clone)] +struct Velocity(Vec3); + +pub struct Benchmark<'w>(World, QueryState<(&'w Velocity, &'w mut Position)>); + +impl<'w> Benchmark<'w> { + pub fn new() -> Self { + let mut world = World::new(); + + world.spawn_batch(core::iter::repeat_n( + ( + Transform(Mat4::from_scale(Vec3::ONE)), + Position(Vec3::X), + Rotation(Vec3::X), + Velocity(Vec3::X), + ), + 10_000, + )); + + let query = world.query::<(&Velocity, &mut Position)>(); + Self(world, query) + } + + #[inline(never)] + pub fn run(&mut self) { + let mut iter = self.1.iter_mut(&mut self.0); + while let Some((velocity, (position, _ticks))) = iter.next_contiguous() { + for (v, p) in velocity.iter().zip(position.iter_mut()) { + p.0 += v.0; + } + } + } +} diff --git a/benches/benches/bevy_ecs/iteration/mod.rs b/benches/benches/bevy_ecs/iteration/mod.rs index b296c5ce0b091..d695df48ad5ef 100644 --- a/benches/benches/bevy_ecs/iteration/mod.rs +++ b/benches/benches/bevy_ecs/iteration/mod.rs @@ -8,11 +8,15 @@ mod iter_frag_sparse; mod iter_frag_wide; mod iter_frag_wide_sparse; mod iter_simple; +mod iter_simple_contiguous; +mod iter_simple_contiguous_avx2; mod iter_simple_foreach; mod iter_simple_foreach_hybrid; mod iter_simple_foreach_sparse_set; mod iter_simple_foreach_wide; mod iter_simple_foreach_wide_sparse_set; +mod iter_simple_no_detection; +mod iter_simple_no_detection_contiguous; mod iter_simple_sparse_set; mod iter_simple_system; mod iter_simple_wide; @@ -40,6 +44,24 @@ fn iter_simple(c: &mut Criterion) { let mut bench = iter_simple::Benchmark::new(); b.iter(move || bench.run()); }); + group.bench_function("base_contiguous", |b| { + let mut bench = iter_simple_contiguous::Benchmark::new(); + b.iter(move || bench.run()); + }); + if iter_simple_contiguous_avx2::Benchmark::supported() { + group.bench_function("base_contiguous_avx2", |b| { + let mut bench = iter_simple_contiguous_avx2::Benchmark::new().unwrap(); + b.iter(move || bench.run()); + }); + } + group.bench_function("base_no_detection", |b| { + let mut bench = iter_simple_no_detection::Benchmark::new(); + b.iter(move || bench.run()); + }); + group.bench_function("base_no_detection_contiguous", |b| { + let mut bench = iter_simple_no_detection_contiguous::Benchmark::new(); + b.iter(move || bench.run()); + }); group.bench_function("wide", |b| { let mut bench = iter_simple_wide::Benchmark::new(); b.iter(move || bench.run()); diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 98b9c33bb07c8..0cc1fbc5342e9 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -149,6 +149,10 @@ path = "examples/resources.rs" name = "change_detection" path = "examples/change_detection.rs" +[[example]] +name = "contigious" +path = "examples/contigious.rs" + [lints] workspace = true diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index e66d62864a604..650dcd1e5e3f1 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -3,8 +3,9 @@ use crate::{ ptr::PtrMut, resource::Resource, }; -use bevy_ptr::{Ptr, UnsafeCellDeref}; +use bevy_ptr::{Ptr, ThinSlicePtr, UnsafeCellDeref}; use core::{ + cell::UnsafeCell, ops::{Deref, DerefMut}, panic::Location, }; @@ -463,6 +464,63 @@ impl<'w, T: ?Sized> Mut<'w, T> { } } +/// Used by [`Mut`] for [`ContiguousQueryData`] to allow marking component's changes +pub struct ContiguousComponentTicks<'w, const MUTABLE: bool> { + added: ThinSlicePtr<'w, UnsafeCell>, + changed: ThinSlicePtr<'w, UnsafeCell>, + changed_by: MaybeLocation>>>, + count: usize, + last_run: Tick, + this_run: Tick, +} + +impl<'w> ContiguousComponentTicks<'w, true> { + /// Returns mutable changed ticks slice + pub fn get_changed_ticks_mut(&mut self) -> &mut [Tick] { + unsafe { self.changed.as_mut_slice(self.count) } + } +} + +impl<'w, const MUTABLE: bool> ContiguousComponentTicks<'w, MUTABLE> { + pub(crate) unsafe fn new( + added: ThinSlicePtr<'w, UnsafeCell>, + changed: ThinSlicePtr<'w, UnsafeCell>, + changed_by: MaybeLocation>>>, + count: usize, + last_run: Tick, + this_run: Tick, + ) -> Self { + Self { + added, + changed, + count, + changed_by, + last_run, + this_run, + } + } + + /// Returns immutable changed ticks slice + pub fn get_changed_ticks(&self) -> &[Tick] { + unsafe { self.changed.cast::().as_slice(self.count) } + } + + /// Returns immutable added ticks slice + pub fn get_added_ticks(&self) -> &[Tick] { + unsafe { self.added.cast::().as_slice(self.count) } + } + + /// Returns the last tick system ran + pub fn last_run(&self) -> Tick { + self.last_run + } + + /// Returns the current tick + pub fn this_run(&self) -> Tick { + self.this_run + } +} + impl<'w, T: ?Sized> From> for Ref<'w, T> { fn from(mut_ref: Mut<'w, T>) -> Self { Self { diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index d672b6ae4d0b7..0c7ac45aac972 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1,7 +1,9 @@ use crate::{ archetype::{Archetype, Archetypes}, bundle::Bundle, - change_detection::{ComponentTicksMut, ComponentTicksRef, MaybeLocation, Tick}, + change_detection::{ + ComponentTicksMut, ComponentTicksRef, ContiguousComponentTicks, MaybeLocation, Tick, + }, component::{Component, ComponentId, Components, Mutable, StorageType}, entity::{Entities, Entity, EntityLocation}, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, @@ -344,6 +346,39 @@ pub unsafe trait QueryData: WorldQuery { ) -> Option>; } +/// A QueryData which allows getting a direct access to contiguous chunks of components' values +/// +// NOTE: The safety rules might not be used to optimize the library, it still may be better to ensure +// that contiguous query data methods match their non-contiguous versions +// NOTE: Even though all component references (&T, &mut T) implement this trait, it won't be executed for +// SparseSet components because in that case the query is not dense. +/// # Safety +/// +/// - The result of [`ContiguousQueryData::fetch_contiguous`] must represent the same result as if +/// [`QueryData::fetch`] was executed for each entity of the set table +pub unsafe trait ContiguousQueryData: QueryData { + /// Item returned by [`ContiguousQueryData::fetch_contiguous`]. + /// Represents a contiguous chunk of memory. + type Contiguous<'w, 's>; + + /// Fetch [`Self::Contiguous`] which represents a contiguous chunk of memory (e.g., an array) in the current [`Table`]. + /// This must always be called after [`WorldQuery::set_table`]. + /// + /// # Safety + /// + /// - Must always be called _after_ [`WorldQuery::set_table`]. + /// - `entities`'s length must match the length of the set table. + /// - `entities` must match the entities of the set table. + /// - `offset` must be less than the length of the set table. + /// - There must not be simultaneous conflicting component access registered in `update_component_access`. + unsafe fn fetch_contiguous<'w, 's>( + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's>; +} + /// A [`QueryData`] that is read only. /// /// # Safety @@ -463,6 +498,20 @@ impl ReleaseStateQueryData for Entity { impl ArchetypeQueryData for Entity {} +/// SAFETY: matches the [`QueryData::fetch`] implementation +unsafe impl ContiguousQueryData for Entity { + type Contiguous<'w, 's> = &'w [Entity]; + + unsafe fn fetch_contiguous<'w, 's>( + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's> { + &entities[offset..] + } +} + /// SAFETY: /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. @@ -1670,6 +1719,37 @@ unsafe impl QueryData for &T { } } +/// SAFETY: The result represents all values of [`T`] in the set table. +unsafe impl ContiguousQueryData for &T { + type Contiguous<'w, 's> = &'w [T]; + + unsafe fn fetch_contiguous<'w, 's>( + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's> { + fetch.components.extract( + |table| { + // SAFETY: set_table was previously called + let table = unsafe { table.debug_checked_unwrap() }; + // UnsafeCell has the same alignment as T because of transparent representation + // (i.e. repr(transparent)) of UnsafeCell + let table = table.cast::(); + // SAFETY: Caller ensures `rows` is the amount of rows in the table + let item = unsafe { table.as_slice(entities.len()) }; + &item[offset..] + }, + |_| { + #[cfg(debug_assertions)] + unreachable!(); + #[cfg(not(debug_assertions))] + core::hint::unreachable_unchecked(); + }, + ) + } +} + /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &T {} @@ -1894,6 +1974,43 @@ impl ReleaseStateQueryData for Ref<'_, T> { impl ArchetypeQueryData for Ref<'_, T> {} +/// SAFETY: Refer to [`&mut T`]'s implementation +unsafe impl ContiguousQueryData for Ref<'_, T> { + type Contiguous<'w, 's> = (&'w [T], ContiguousComponentTicks<'w, false>); + + unsafe fn fetch_contiguous<'w, 's>( + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's> { + fetch.components.extract( + |table| { + let (table_components, added_ticks, changed_ticks, callers) = + unsafe { table.debug_checked_unwrap() }; + + ( + &table_components.cast::().as_slice(entities.len())[offset..], + ContiguousComponentTicks::<'w, false>::new( + added_ticks.add(offset), + changed_ticks.add(offset), + callers.map(|callers| callers.add(offset)), + entities.len() - offset, + fetch.last_run, + fetch.this_run, + ), + ) + }, + |_| { + #[cfg(debug_assertions)] + unreachable!(); + #[cfg(not(debug_assertions))] + core::hint::unreachable_unchecked(); + }, + ) + } +} + /// The [`WorldQuery::Fetch`] type for `&mut T`. pub struct WriteFetch<'w, T: Component> { components: StorageSwitch< @@ -2104,6 +2221,45 @@ impl> ReleaseStateQueryData for &mut T { impl> ArchetypeQueryData for &mut T {} +/// SAFETY: +/// - The first element of [`Self::Contiguous`] tuple represents all components' values in the set table. +/// - The second element of [`Self::Contiguous`] tuple represents all components' ticks in the set table. +unsafe impl> ContiguousQueryData for &mut T { + type Contiguous<'w, 's> = (&'w mut [T], ContiguousComponentTicks<'w, true>); + + unsafe fn fetch_contiguous<'w, 's>( + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's> { + fetch.components.extract( + |table| { + let (table_components, added_ticks, changed_ticks, callers) = + unsafe { table.debug_checked_unwrap() }; + + ( + &mut table_components.as_mut_slice(entities.len())[offset..], + ContiguousComponentTicks::<'w, true>::new( + added_ticks.add(offset), + changed_ticks.add(offset), + callers.map(|callers| callers.add(offset)), + entities.len() - offset, + fetch.last_run, + fetch.this_run, + ), + ) + }, + |_| { + #[cfg(debug_assertions)] + unreachable!(); + #[cfg(not(debug_assertions))] + core::hint::unreachable_unchecked(); + }, + ) + } +} + /// When `Mut` is used in a query, it will be converted to `Ref` when transformed into its read-only form, providing access to change detection methods. /// /// By contrast `&mut T` will result in a `Mut` item in mutable form to record mutations, but result in a bare `&T` in read-only form. @@ -2219,6 +2375,20 @@ impl> ReleaseStateQueryData for Mut<'_, T> { impl> ArchetypeQueryData for Mut<'_, T> {} +/// SAFETY: Refer to soundness of `&mut T` implementation +unsafe impl<'__w, T: Component> ContiguousQueryData for Mut<'__w, T> { + type Contiguous<'w, 's> = (&'w mut [T], ContiguousComponentTicks<'w, true>); + + unsafe fn fetch_contiguous<'w, 's>( + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's> { + <&mut T as ContiguousQueryData>::fetch_contiguous(state, fetch, entities, offset) + } +} + #[doc(hidden)] pub struct OptionFetch<'w, T: WorldQuery> { fetch: T::Fetch<'w>, @@ -2372,6 +2542,23 @@ impl ReleaseStateQueryData for Option { // so it's always an `ArchetypeQueryData`, even for non-archetypal `T`. impl ArchetypeQueryData for Option {} +/// SAFETY: [`fetch.matches`] depends solely on the table. +unsafe impl ContiguousQueryData for Option { + type Contiguous<'w, 's> = Option>; + + unsafe fn fetch_contiguous<'w, 's>( + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's> { + fetch + .matches + // SAFETY: The invariants are upheld by the caller + .then(|| unsafe { T::fetch_contiguous(state, &mut fetch.fetch, entities, offset) }) + } +} + /// Returns a bool that describes if an entity has the component `T`. /// /// This can be used in a [`Query`](crate::system::Query) if you want to know whether or not entities @@ -2631,6 +2818,39 @@ macro_rules! impl_tuple_query_data { $(#[$meta])* impl<$($name: ArchetypeQueryData),*> ArchetypeQueryData for ($($name,)*) {} + + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + non_snake_case, + reason = "The names of some variables are provided by the macro's caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + #[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." + )] + $(#[$meta])* + // SAFETY: The returned result represents the result of individual fetches. + unsafe impl<$($name: ContiguousQueryData),*> ContiguousQueryData for ($($name,)*) { + type Contiguous<'w, 's> = ($($name::Contiguous::<'w, 's>,)*); + + unsafe fn fetch_contiguous<'w, 's>( + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's> { + let ($($state,)*) = state; + let ($($name,)*) = fetch; + ($(unsafe {$name::fetch_contiguous($state, $name, entities, offset)},)*) + } + } }; } @@ -3338,4 +3558,77 @@ mod tests { // we want EntityRef to use the change ticks of the system schedule.run(&mut world); } + + #[test] + fn test_contiguous_query_data() { + #[derive(Component, PartialEq, Eq, Debug)] + pub struct C(i32); + + #[derive(Component, PartialEq, Eq, Debug)] + pub struct D(bool); + + let mut world = World::new(); + world.spawn((C(0), D(true))); + world.spawn((C(1), D(false))); + world.spawn(C(2)); + + let mut query = world.query::<(&C, &D)>(); + let mut iter = query.iter(&world); + let c = iter.next_contiguous().unwrap(); + assert_eq!(c.0, [C(0), C(1)].as_slice()); + assert_eq!(c.1, [D(true), D(false)].as_slice()); + assert!(iter.next_contiguous().is_none()); + + let mut query = world.query::<&C>(); + let mut iter = query.iter(&world); + let mut present = [false; 3]; + let mut len = 0; + for _ in 0..2 { + let c = iter.next_contiguous().unwrap(); + for c in c { + present[c.0 as usize] = true; + len += 1; + } + } + assert!(iter.next_contiguous().is_none()); + assert_eq!(len, 3); + assert_eq!(present, [true; 3]); + + let mut query = world.query::<&mut C>(); + let mut iter = query.iter_mut(&mut world); + for _ in 0..2 { + let c = iter.next_contiguous().unwrap(); + for c in c.0 { + c.0 *= 2; + } + } + assert!(iter.next_contiguous().is_none()); + let mut iter = query.iter(&mut world); + let mut present = [false; 6]; + let mut len = 0; + for _ in 0..2 { + let c = iter.next_contiguous().unwrap(); + for c in c { + present[c.0 as usize] = true; + len += 1; + } + } + assert_eq!(present, [true, false, true, false, true, false]); + assert_eq!(len, 3); + } + + #[test] + fn sparse_set_contiguous_query() { + #[derive(Component, Debug, PartialEq, Eq)] + #[component(storage = "SparseSet")] + pub struct S(i32); + + let mut world = World::new(); + world.spawn(S(0)); + + let mut query = world.query::<&mut S>(); + let mut iter = query.iter_mut(&mut world); + assert!(iter.next_contiguous().is_none()); + assert_eq!(iter.next().unwrap().as_ref(), &S(0)); + } } diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index f8578d5d21705..9151fe4facc1e 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -112,6 +112,37 @@ pub unsafe trait QueryFilter: WorldQuery { ) -> bool; } +/// Types that filter contiguous chunks of memory +/// +/// Some types which implement this trait: +/// - [`With`] and [`Without`] +/// +/// Some [`QueryFilter`]s which **do not** implement this trait: +/// - [`Added`], [`Changed`] and [`Spawned`] due to their selective filters within contiguous chunks of memory +/// (i.e., it might exclude entities thus breaking contiguity) +/// +// NOTE: The safety rules might not be used to optimize the library, it still might be better to ensure +// that contiguous query filters match their non-contiguous versions +/// # Safety +/// +/// - The result of [`ContiguousQueryFilter::filter_fetch_contiguous`] must be the same as +/// The value returned by every call of [`QueryFilter::filter_fetch`] on the same table for every entity +/// (i.e., the value depends on the table not an entity) +pub unsafe trait ContiguousQueryFilter: QueryFilter { + /// # Safety + /// + /// - Must always be called _after_ [`WorldQuery::set_table`] + /// - `entities`'s length must match the length of the set table. + /// - `entities` must match the entities of the set table. + /// - `offset` must be less than the length of the set table. + unsafe fn filter_fetch_contiguous( + state: &Self::State, + fetch: &mut Self::Fetch<'_>, + entities: &[Entity], + offset: usize, + ) -> bool; +} + /// Filter that selects entities with a component `T`. /// /// This can be used in a [`Query`](crate::system::Query) if entities are required to have the @@ -216,6 +247,20 @@ unsafe impl QueryFilter for With { } } +/// # Safety +/// [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch`] both always return true +unsafe impl ContiguousQueryFilter for With { + #[inline(always)] + unsafe fn filter_fetch_contiguous( + _state: &Self::State, + _fetch: &mut Self::Fetch<'_>, + _table_entities: &[Entity], + _offset: usize, + ) -> bool { + true + } +} + /// Filter that selects entities without a component `T`. /// /// This is the negation of [`With`]. @@ -317,6 +362,20 @@ unsafe impl QueryFilter for Without { } } +/// # Safety +/// [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch`] both always return true +unsafe impl ContiguousQueryFilter for Without { + #[inline(always)] + unsafe fn filter_fetch_contiguous( + _state: &Self::State, + _fetch: &mut Self::Fetch<'_>, + _table_entities: &[Entity], + _offset: usize, + ) -> bool { + true + } +} + /// A filter that tests if any of the given filters apply. /// /// This is useful for example if a system with multiple components in a query only wants to run @@ -528,6 +587,37 @@ macro_rules! impl_or_query_filter { || !(false $(|| $filter.matches)*)) } } + + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + non_snake_case, + reason = "The names of some variables are provided by the macro's caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + $(#[$meta])* + // SAFETY: `filter_fetch_contiguous` matches the implementation of `filter_fetch` + unsafe impl<$($filter: ContiguousQueryFilter),*> ContiguousQueryFilter for Or<($($filter,)*)> { + #[inline(always)] + unsafe fn filter_fetch_contiguous( + state: &Self::State, + fetch: &mut Self::Fetch<'_>, + entities: &[Entity], + offset: usize, + ) -> bool { + let ($($state,)*) = state; + let ($($filter,)*) = fetch; + + (Self::IS_ARCHETYPAL + $(|| ($filter.matches && unsafe { $filter::filter_fetch_contiguous($state, &mut $filter.fetch, entities, offset) }))* + || !(false $(|| $filter.matches)*)) + } + } }; } @@ -564,6 +654,33 @@ macro_rules! impl_tuple_query_filter { } } + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + non_snake_case, + reason = "The names of some variables are provided by the macro's caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + /// # Safety + /// Implied by individual safety guarantees of the tuple's types + unsafe impl<$($name: ContiguousQueryFilter),*> ContiguousQueryFilter for ($($name,)*) { + unsafe fn filter_fetch_contiguous( + state: &Self::State, + fetch: &mut Self::Fetch<'_>, + table_entities: &[Entity], + offset: usize, + ) -> bool { + let ($($state,)*) = state; + let ($($name,)*) = fetch; + true $(&& unsafe { $name::filter_fetch_contiguous($state, $name, table_entities, offset) })* + } + } + }; } diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 0bd178ba03727..bef193414d69b 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -4,7 +4,10 @@ use crate::{ bundle::Bundle, change_detection::Tick, entity::{ContainsEntity, Entities, Entity, EntityEquivalent, EntitySet, EntitySetIterator}, - query::{ArchetypeFilter, ArchetypeQueryData, DebugCheckedUnwrap, QueryState, StorageId}, + query::{ + ArchetypeFilter, ArchetypeQueryData, ContiguousQueryData, ContiguousQueryFilter, + DebugCheckedUnwrap, QueryState, StorageId, + }, storage::{Table, TableRow, Tables}, world::{ unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, @@ -900,6 +903,20 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { ) } } + + /// Returns the next contiguous chunk of memory + /// or [`None`] if the query doesn't support contiguous access or if there is no elements left + #[inline(always)] + pub fn next_contiguous(&mut self) -> Option> + where + D: ContiguousQueryData, + F: ContiguousQueryFilter, + { + // SAFETY: + // `tables` belongs to the same world that the cursor was initialized for. + // `query_state` is the state that was passed to `QueryIterationCursor::init` + unsafe { self.cursor.next_contiguous(self.tables, self.query_state) } + } } impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> { @@ -2530,6 +2547,62 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { remaining_matched + self.current_len - self.current_row } + /// Returns the next contiguous chunk of memory or [`None`] if it is impossible or there is none + /// + /// # Safety + /// - `tables` must belong to the same world that the [`QueryIterationCursor`] was initialized for. + /// - `query_state` must be the same [`QueryState`] that was passed to `init` or `init_empty`. + #[inline(always)] + unsafe fn next_contiguous( + &mut self, + tables: &'w Tables, + query_state: &'s QueryState, + ) -> Option> + where + D: ContiguousQueryData, + F: ContiguousQueryFilter, + { + if !self.is_dense { + return None; + } + + loop { + if self.current_row == self.current_len { + let table_id = self.storage_id_iter.next()?.table_id; + let table = tables.get(table_id).debug_checked_unwrap(); + if table.is_empty() { + continue; + } + D::set_table(&mut self.fetch, &query_state.fetch_state, table); + F::set_table(&mut self.filter, &query_state.filter_state, table); + self.table_entities = table.entities(); + self.current_len = table.entity_count(); + self.current_row = 0; + } + + let offset = self.current_row as usize; + self.current_row = self.current_len; + + if !F::filter_fetch_contiguous( + &query_state.filter_state, + &mut self.filter, + self.table_entities, + offset, + ) { + continue; + } + + let item = D::fetch_contiguous( + &query_state.fetch_state, + &mut self.fetch, + self.table_entities, + offset, + ); + + return Some(item); + } + } + // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QuerySortedIter, QueryManyIter, QuerySortedManyIter, QueryCombinationIter, // QueryState::par_fold_init_unchecked_manual, QueryState::par_many_fold_init_unchecked_manual, diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index eb3dad12bd367..0c53143f8f982 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -9,6 +9,7 @@ mod iter; mod par_iter; mod state; mod world_query; +mod table_query; pub use access::*; pub use bevy_ecs_macros::{QueryData, QueryFilter}; diff --git a/crates/bevy_ecs/src/query/table_query.rs b/crates/bevy_ecs/src/query/table_query.rs new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index e76fcc9f57250..6766938f3a40b 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1104,6 +1104,50 @@ impl<'a, T> ThinSlicePtr<'a, T> { unsafe { &*self.ptr.add(index).as_ptr() } } + /// Returns a slice without performing bounds checks. + /// + /// # Safety + /// + /// `len` must be less or equal to the length of the slice. + pub unsafe fn as_slice(&self, len: usize) -> &'a [T] { + #[cfg(debug_assertions)] + assert!(len <= self.len, "tried to create an out-of-bounds slice"); + + // SAFETY: The caller guarantees `len` is not greater than the length of the slice + unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), len) } + } + + /// Casts the slice to another type + pub fn cast(&self) -> ThinSlicePtr<'a, U> { + ThinSlicePtr { + ptr: self.ptr.cast::(), + #[cfg(debug_assertions)] + len: self.len * size_of::() / size_of::(), + _marker: PhantomData, + } + } + + /// Offsets the slice beginning by [`count`] elements + /// + /// # Safety + /// + /// - `count` must be less or equal to the length of the slice + // The result pointer must lie within the same allocation + pub unsafe fn add(&self, count: usize) -> ThinSlicePtr<'a, T> { + #[cfg(debug_assertions)] + assert!( + count <= self.len, + "tried to offset the slice by more than the length" + ); + + Self { + ptr: unsafe { self.ptr.add(count) }, + #[cfg(debug_assertions)] + len: self.len - count, + _marker: PhantomData, + } + } + /// Indexes the slice without performing bounds checks. /// /// # Safety @@ -1116,6 +1160,21 @@ impl<'a, T> ThinSlicePtr<'a, T> { } } +impl<'a, T> ThinSlicePtr<'a, UnsafeCell> { + /// Returns a mutable reference of the slice + /// + /// # Safety + /// + /// - There must not be any aliases to the slice + /// - `len` must be less or equal to the length of the slice + pub unsafe fn as_mut_slice(&self, len: usize) -> &'a mut [T] { + #[cfg(debug_assertions)] + assert!(len <= self.len, "tried to create an out-of-bounds slice"); + + unsafe { core::slice::from_raw_parts_mut(UnsafeCell::raw_get(self.ptr.as_ptr()), len) } + } +} + impl<'a, T> Clone for ThinSlicePtr<'a, T> { fn clone(&self) -> Self { *self From 19c7a2a3fceb729d57c95b6298ab46e3d6bd4cd9 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:16:34 +0100 Subject: [PATCH 02/52] contiguous iter --- .../iteration/iter_simple_contiguous.rs | 7 +-- .../iteration/iter_simple_contiguous_avx2.rs | 7 +-- .../iter_simple_no_detection_contiguous.rs | 2 +- .../bevy_ecs/src/change_detection/params.rs | 16 +++++++ crates/bevy_ecs/src/query/fetch.rs | 20 +++++---- crates/bevy_ecs/src/query/iter.rs | 43 +++++++++++++------ 6 files changed, 64 insertions(+), 31 deletions(-) diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs index 5df29bcd38029..c40d59e90c3db 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs @@ -36,15 +36,12 @@ impl<'w> Benchmark<'w> { #[inline(never)] pub fn run(&mut self) { let mut iter = self.1.iter_mut(&mut self.0); - while let Some((velocity, (position, mut ticks))) = iter.next_contiguous() { + for (velocity, (position, mut ticks)) in iter.as_contiguous_iter().unwrap() { for (v, p) in velocity.iter().zip(position.iter_mut()) { p.0 += v.0; } - let tick = ticks.this_run(); // to match the iter_simple benchmark - for t in ticks.get_changed_ticks_mut() { - *t = tick; - } + ticks.mark_all_as_updated(); } } } diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs index 7da01fc1dfacd..41d7a7594acfa 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs @@ -51,16 +51,13 @@ impl<'w> Benchmark<'w> { } let mut iter = self.1.iter_mut(&mut self.0); - while let Some((velocity, (position, mut ticks))) = iter.next_contiguous() { + for (velocity, (position, mut ticks)) in iter.as_contiguous_iter().unwrap() { // SAFETY: checked in new unsafe { exec(position, velocity); } - let tick = ticks.this_run(); // to match the iter_simple benchmark - for t in ticks.get_changed_ticks_mut() { - *t = tick; - } + ticks.mark_all_as_updated(); } } } diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs index d2a00c1472c89..ca8209bff2b5f 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs @@ -36,7 +36,7 @@ impl<'w> Benchmark<'w> { #[inline(never)] pub fn run(&mut self) { let mut iter = self.1.iter_mut(&mut self.0); - while let Some((velocity, (position, _ticks))) = iter.next_contiguous() { + for (velocity, (position, _ticks)) in iter.as_contiguous_iter().unwrap() { for (v, p) in velocity.iter().zip(position.iter_mut()) { p.0 += v.0; } diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 650dcd1e5e3f1..cabe37a60e758 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -479,6 +479,22 @@ impl<'w> ContiguousComponentTicks<'w, true> { pub fn get_changed_ticks_mut(&mut self) -> &mut [Tick] { unsafe { self.changed.as_mut_slice(self.count) } } + + /// Marks all components as updated + pub fn mark_all_as_updated(&mut self) { + let this_run = self.this_run; + + for i in 0..self.count { + // SAFETY: `changed_by` slice is `self.count` long, aliasing rules are uphold by `new` + self.changed_by + .map(|v| unsafe { v.get_unchecked(i).deref_mut() }) + .assign(MaybeLocation::caller()); + } + + for t in self.get_changed_ticks_mut() { + *t = this_run; + } + } } impl<'w, const MUTABLE: bool> ContiguousComponentTicks<'w, MUTABLE> { diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 0c7ac45aac972..acbca4803be87 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -3574,40 +3574,44 @@ mod tests { let mut query = world.query::<(&C, &D)>(); let mut iter = query.iter(&world); - let c = iter.next_contiguous().unwrap(); + let mut iter = iter.as_contiguous_iter().unwrap(); + let c = iter.next().unwrap(); assert_eq!(c.0, [C(0), C(1)].as_slice()); assert_eq!(c.1, [D(true), D(false)].as_slice()); - assert!(iter.next_contiguous().is_none()); + assert!(iter.next().is_none()); let mut query = world.query::<&C>(); let mut iter = query.iter(&world); + let mut iter = iter.as_contiguous_iter().unwrap(); let mut present = [false; 3]; let mut len = 0; for _ in 0..2 { - let c = iter.next_contiguous().unwrap(); + let c = iter.next().unwrap(); for c in c { present[c.0 as usize] = true; len += 1; } } - assert!(iter.next_contiguous().is_none()); + assert!(iter.next().is_none()); assert_eq!(len, 3); assert_eq!(present, [true; 3]); let mut query = world.query::<&mut C>(); let mut iter = query.iter_mut(&mut world); + let mut iter = iter.as_contiguous_iter().unwrap(); for _ in 0..2 { - let c = iter.next_contiguous().unwrap(); + let c = iter.next().unwrap(); for c in c.0 { c.0 *= 2; } } - assert!(iter.next_contiguous().is_none()); + assert!(iter.next().is_none()); let mut iter = query.iter(&mut world); + let mut iter = iter.as_contiguous_iter().unwrap(); let mut present = [false; 6]; let mut len = 0; for _ in 0..2 { - let c = iter.next_contiguous().unwrap(); + let c = iter.next().unwrap(); for c in c { present[c.0 as usize] = true; len += 1; @@ -3628,7 +3632,7 @@ mod tests { let mut query = world.query::<&mut S>(); let mut iter = query.iter_mut(&mut world); - assert!(iter.next_contiguous().is_none()); + assert!(iter.as_contiguous_iter().is_none()); assert_eq!(iter.next().unwrap().as_ref(), &S(0)); } } diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index bef193414d69b..28cc2327ab177 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -904,18 +904,15 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } } - /// Returns the next contiguous chunk of memory - /// or [`None`] if the query doesn't support contiguous access or if there is no elements left - #[inline(always)] - pub fn next_contiguous(&mut self) -> Option> + /// Returns a contiguous iter or [`None`] if contiguous access is not supported + pub fn as_contiguous_iter(&mut self) -> Option> where D: ContiguousQueryData, F: ContiguousQueryFilter, { - // SAFETY: - // `tables` belongs to the same world that the cursor was initialized for. - // `query_state` is the state that was passed to `QueryIterationCursor::init` - unsafe { self.cursor.next_contiguous(self.tables, self.query_state) } + self.cursor + .is_dense + .then(|| QueryContiguousIter { iter: self }) } } @@ -1009,6 +1006,31 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter> Clone for QueryIter<'w, 's, D } } +/// Iterator for contiguous chunks of memory +pub struct QueryContiguousIter<'a, 'w, 's, D: ContiguousQueryData, F: ContiguousQueryFilter> { + iter: &'a mut QueryIter<'w, 's, D, F>, +} + +impl<'a, 'w, 's, D, F> Iterator for QueryContiguousIter<'a, 'w, 's, D, F> +where + D: ContiguousQueryData, + F: ContiguousQueryFilter, +{ + type Item = D::Contiguous<'w, 's>; + + #[inline(always)] + fn next(&mut self) -> Option { + // SAFETY: + // `tables` belongs to the same world that the cursor was initialized for. + // `query_state` is the state that was passed to `QueryIterationCursor::init` + unsafe { + self.iter + .cursor + .next_contiguous(&self.iter.tables, &mut self.iter.query_state) + } + } +} + /// An [`Iterator`] over sorted query results of a [`Query`](crate::system::Query). /// /// This struct is created by the [`QueryIter::sort`], [`QueryIter::sort_unstable`], @@ -2552,6 +2574,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// # Safety /// - `tables` must belong to the same world that the [`QueryIterationCursor`] was initialized for. /// - `query_state` must be the same [`QueryState`] that was passed to `init` or `init_empty`. + /// - Query must be dense #[inline(always)] unsafe fn next_contiguous( &mut self, @@ -2562,10 +2585,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { D: ContiguousQueryData, F: ContiguousQueryFilter, { - if !self.is_dense { - return None; - } - loop { if self.current_row == self.current_len { let table_id = self.storage_id_iter.next()?.table_id; From 9f391c78c24057140d7c3c5c9b4195b9298c71c5 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:22:55 +0100 Subject: [PATCH 03/52] fmt --- crates/bevy_ecs/src/query/mod.rs | 1 - crates/bevy_ecs/src/query/table_query.rs | 0 2 files changed, 1 deletion(-) delete mode 100644 crates/bevy_ecs/src/query/table_query.rs diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index 0c53143f8f982..eb3dad12bd367 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -9,7 +9,6 @@ mod iter; mod par_iter; mod state; mod world_query; -mod table_query; pub use access::*; pub use bevy_ecs_macros::{QueryData, QueryFilter}; diff --git a/crates/bevy_ecs/src/query/table_query.rs b/crates/bevy_ecs/src/query/table_query.rs deleted file mode 100644 index e69de29bb2d1d..0000000000000 From 564b771bd8a13876fa8f807b1595f5dfa5e4ae3d Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:26:38 +0100 Subject: [PATCH 04/52] fmt --- crates/bevy_ecs/Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 0cc1fbc5342e9..98b9c33bb07c8 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -149,10 +149,6 @@ path = "examples/resources.rs" name = "change_detection" path = "examples/change_detection.rs" -[[example]] -name = "contigious" -path = "examples/contigious.rs" - [lints] workspace = true From 667955dbb4f681695ce836f58aed623c28da0a11 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:45:39 +0100 Subject: [PATCH 05/52] safety comment --- crates/bevy_ecs/src/change_detection/params.rs | 3 +++ crates/bevy_ecs/src/query/fetch.rs | 7 +++++-- crates/bevy_ecs/src/query/filter.rs | 11 +++++------ crates/bevy_ecs/src/query/iter.rs | 4 ++-- crates/bevy_ptr/src/lib.rs | 2 ++ 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index cabe37a60e758..9a177ef289cfc 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -477,6 +477,7 @@ pub struct ContiguousComponentTicks<'w, const MUTABLE: bool> { impl<'w> ContiguousComponentTicks<'w, true> { /// Returns mutable changed ticks slice pub fn get_changed_ticks_mut(&mut self) -> &mut [Tick] { + // SAFETY: `changed` slice is `self.count` long, aliasing rules are uphold by `new`. unsafe { self.changed.as_mut_slice(self.count) } } @@ -518,11 +519,13 @@ impl<'w, const MUTABLE: bool> ContiguousComponentTicks<'w, MUTABLE> { /// Returns immutable changed ticks slice pub fn get_changed_ticks(&self) -> &[Tick] { + // SAFETY: `self.changed` is `self.count` long unsafe { self.changed.cast::().as_slice(self.count) } } /// Returns immutable added ticks slice pub fn get_added_ticks(&self) -> &[Tick] { + // SAFETY: `self.added` is `self.count` long unsafe { self.added.cast::().as_slice(self.count) } } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index acbca4803be87..8bd1e15b69d07 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -346,9 +346,9 @@ pub unsafe trait QueryData: WorldQuery { ) -> Option>; } -/// A QueryData which allows getting a direct access to contiguous chunks of components' values +/// A [`QueryData`] which allows getting a direct access to contiguous chunks of components' values /// -// NOTE: The safety rules might not be used to optimize the library, it still may be better to ensure +// NOTE: The safety rules might not be used to optimize the library, it still might be better to ensure // that contiguous query data methods match their non-contiguous versions // NOTE: Even though all component references (&T, &mut T) implement this trait, it won't be executed for // SparseSet components because in that case the query is not dense. @@ -1986,6 +1986,7 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { ) -> Self::Contiguous<'w, 's> { fetch.components.extract( |table| { + // SAFETY: set_table was previously called let (table_components, added_ticks, changed_ticks, callers) = unsafe { table.debug_checked_unwrap() }; @@ -2235,6 +2236,7 @@ unsafe impl> ContiguousQueryData for &mut T { ) -> Self::Contiguous<'w, 's> { fetch.components.extract( |table| { + // SAFETY: set_table was previously called let (table_components, added_ticks, changed_ticks, callers) = unsafe { table.debug_checked_unwrap() }; @@ -2848,6 +2850,7 @@ macro_rules! impl_tuple_query_data { ) -> Self::Contiguous<'w, 's> { let ($($state,)*) = state; let ($($name,)*) = fetch; + // SAFETY: The invariants are upheld by the caller. ($(unsafe {$name::fetch_contiguous($state, $name, entities, offset)},)*) } } diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 9151fe4facc1e..482057655cbc4 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -247,8 +247,7 @@ unsafe impl QueryFilter for With { } } -/// # Safety -/// [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch`] both always return true +// SAFETY: [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch`] both always return true unsafe impl ContiguousQueryFilter for With { #[inline(always)] unsafe fn filter_fetch_contiguous( @@ -362,8 +361,7 @@ unsafe impl QueryFilter for Without { } } -/// # Safety -/// [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch`] both always return true +// SAFETY: [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch`] both always return true unsafe impl ContiguousQueryFilter for Without { #[inline(always)] unsafe fn filter_fetch_contiguous( @@ -614,6 +612,7 @@ macro_rules! impl_or_query_filter { let ($($filter,)*) = fetch; (Self::IS_ARCHETYPAL + // SAFETY: The invariants are upheld by the caller $(|| ($filter.matches && unsafe { $filter::filter_fetch_contiguous($state, &mut $filter.fetch, entities, offset) }))* || !(false $(|| $filter.matches)*)) } @@ -666,8 +665,7 @@ macro_rules! impl_tuple_query_filter { unused_variables, reason = "Zero-length tuples won't use any of the parameters." )] - /// # Safety - /// Implied by individual safety guarantees of the tuple's types + // SAFETY: Implied by individual safety guarantees of the tuple's types unsafe impl<$($name: ContiguousQueryFilter),*> ContiguousQueryFilter for ($($name,)*) { unsafe fn filter_fetch_contiguous( state: &Self::State, @@ -677,6 +675,7 @@ macro_rules! impl_tuple_query_filter { ) -> bool { let ($($state,)*) = state; let ($($name,)*) = fetch; + // SAFETY: The invariants are upheld by the caller. true $(&& unsafe { $name::filter_fetch_contiguous($state, $name, table_entities, offset) })* } } diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 28cc2327ab177..0992be08d2ead 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -912,7 +912,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { { self.cursor .is_dense - .then(|| QueryContiguousIter { iter: self }) + .then_some(QueryContiguousIter { iter: self }) } } @@ -1026,7 +1026,7 @@ where unsafe { self.iter .cursor - .next_contiguous(&self.iter.tables, &mut self.iter.query_state) + .next_contiguous(self.iter.tables, self.iter.query_state) } } } diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 6766938f3a40b..a94000c742c79 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1141,6 +1141,7 @@ impl<'a, T> ThinSlicePtr<'a, T> { ); Self { + // SAFETY: The caller guarantees that count is in-bounds. ptr: unsafe { self.ptr.add(count) }, #[cfg(debug_assertions)] len: self.len - count, @@ -1171,6 +1172,7 @@ impl<'a, T> ThinSlicePtr<'a, UnsafeCell> { #[cfg(debug_assertions)] assert!(len <= self.len, "tried to create an out-of-bounds slice"); + // SAFETY: The caller ensures no aliases exist and `len` is in-bounds. unsafe { core::slice::from_raw_parts_mut(UnsafeCell::raw_get(self.ptr.as_ptr()), len) } } } From 6ad54b00f95c7b6e781593fcaf29927dccb65f12 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:32:43 +0100 Subject: [PATCH 06/52] small fixes --- benches/benches/bevy_ecs/iteration/mod.rs | 14 +++++++++----- crates/bevy_ecs/src/change_detection/params.rs | 2 +- crates/bevy_ecs/src/query/fetch.rs | 10 +++++----- crates/bevy_ecs/src/query/filter.rs | 4 ++-- crates/bevy_ptr/src/lib.rs | 2 +- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/benches/benches/bevy_ecs/iteration/mod.rs b/benches/benches/bevy_ecs/iteration/mod.rs index d695df48ad5ef..7867507f62f67 100644 --- a/benches/benches/bevy_ecs/iteration/mod.rs +++ b/benches/benches/bevy_ecs/iteration/mod.rs @@ -9,6 +9,7 @@ mod iter_frag_wide; mod iter_frag_wide_sparse; mod iter_simple; mod iter_simple_contiguous; +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] mod iter_simple_contiguous_avx2; mod iter_simple_foreach; mod iter_simple_foreach_hybrid; @@ -48,11 +49,14 @@ fn iter_simple(c: &mut Criterion) { let mut bench = iter_simple_contiguous::Benchmark::new(); b.iter(move || bench.run()); }); - if iter_simple_contiguous_avx2::Benchmark::supported() { - group.bench_function("base_contiguous_avx2", |b| { - let mut bench = iter_simple_contiguous_avx2::Benchmark::new().unwrap(); - b.iter(move || bench.run()); - }); + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + if iter_simple_contiguous_avx2::Benchmark::supported() { + group.bench_function("base_contiguous_avx2", |b| { + let mut bench = iter_simple_contiguous_avx2::Benchmark::new().unwrap(); + b.iter(move || bench.run()); + }); + } } group.bench_function("base_no_detection", |b| { let mut bench = iter_simple_no_detection::Benchmark::new(); diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 9a177ef289cfc..842e13824424f 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -464,7 +464,7 @@ impl<'w, T: ?Sized> Mut<'w, T> { } } -/// Used by [`Mut`] for [`ContiguousQueryData`] to allow marking component's changes +/// Used by [`Mut`] for [`crate::query::ContiguousQueryData`] to allow marking component's changes pub struct ContiguousComponentTicks<'w, const MUTABLE: bool> { added: ThinSlicePtr<'w, UnsafeCell>, changed: ThinSlicePtr<'w, UnsafeCell>, diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 8bd1e15b69d07..0fe20de2973dc 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -361,7 +361,7 @@ pub unsafe trait ContiguousQueryData: QueryData { /// Represents a contiguous chunk of memory. type Contiguous<'w, 's>; - /// Fetch [`Self::Contiguous`] which represents a contiguous chunk of memory (e.g., an array) in the current [`Table`]. + /// Fetch [`ContiguousQueryData::Contiguous`] which represents a contiguous chunk of memory (e.g., an array) in the current [`Table`]. /// This must always be called after [`WorldQuery::set_table`]. /// /// # Safety @@ -1719,7 +1719,7 @@ unsafe impl QueryData for &T { } } -/// SAFETY: The result represents all values of [`T`] in the set table. +/// SAFETY: The result represents all values of [`Self`] in the set table. unsafe impl ContiguousQueryData for &T { type Contiguous<'w, 's> = &'w [T]; @@ -2223,8 +2223,8 @@ impl> ReleaseStateQueryData for &mut T { impl> ArchetypeQueryData for &mut T {} /// SAFETY: -/// - The first element of [`Self::Contiguous`] tuple represents all components' values in the set table. -/// - The second element of [`Self::Contiguous`] tuple represents all components' ticks in the set table. +/// - The first element of [`ContiguousQueryData::Contiguous`] tuple represents all components' values in the set table. +/// - The second element of [`ContiguousQueryData::Contiguous`] tuple represents all components' ticks in the set table. unsafe impl> ContiguousQueryData for &mut T { type Contiguous<'w, 's> = (&'w mut [T], ContiguousComponentTicks<'w, true>); @@ -2544,7 +2544,7 @@ impl ReleaseStateQueryData for Option { // so it's always an `ArchetypeQueryData`, even for non-archetypal `T`. impl ArchetypeQueryData for Option {} -/// SAFETY: [`fetch.matches`] depends solely on the table. +// SAFETY: matches the [`QueryData::fetch`] impl unsafe impl ContiguousQueryData for Option { type Contiguous<'w, 's> = Option>; diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 482057655cbc4..0ea957f6158bc 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -247,7 +247,7 @@ unsafe impl QueryFilter for With { } } -// SAFETY: [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch`] both always return true +// SAFETY: [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch_contiguous`] both always return true unsafe impl ContiguousQueryFilter for With { #[inline(always)] unsafe fn filter_fetch_contiguous( @@ -361,7 +361,7 @@ unsafe impl QueryFilter for Without { } } -// SAFETY: [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch`] both always return true +// SAFETY: [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch_contiguous`] both always return true unsafe impl ContiguousQueryFilter for Without { #[inline(always)] unsafe fn filter_fetch_contiguous( diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index a94000c742c79..cf89512d9550b 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1127,7 +1127,7 @@ impl<'a, T> ThinSlicePtr<'a, T> { } } - /// Offsets the slice beginning by [`count`] elements + /// Offsets the slice beginning by `count` elements /// /// # Safety /// From f3ebbc7661638eeb53b05cef735af24036362603 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Mon, 1 Dec 2025 22:40:42 +0100 Subject: [PATCH 07/52] removed contiguous query filter --- .../iteration/iter_simple_contiguous_avx2.rs | 2 + crates/bevy_ecs/src/query/fetch.rs | 9 +- crates/bevy_ecs/src/query/filter.rs | 122 +----------------- crates/bevy_ecs/src/query/iter.rs | 25 ++-- 4 files changed, 24 insertions(+), 134 deletions(-) diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs index 41d7a7594acfa..837c92be8c190 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs @@ -43,6 +43,8 @@ impl<'w> Benchmark<'w> { #[inline(never)] pub fn run(&mut self) { + /// # Safety + /// avx2 must be supported #[target_feature(enable = "avx2")] fn exec(position: &mut [Position], velocity: &[Velocity]) { for i in 0..position.len() { diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 0fe20de2973dc..b4c79b4ed0568 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -3317,6 +3317,7 @@ impl Copy for StorageSwitch {} mod tests { use super::*; use crate::change_detection::DetectChanges; + use crate::query::Without; use crate::system::{assert_is_system, Query}; use bevy_ecs::prelude::Schedule; use bevy_ecs_macros::QueryData; @@ -3609,7 +3610,7 @@ mod tests { } } assert!(iter.next().is_none()); - let mut iter = query.iter(&mut world); + let mut iter = query.iter(&world); let mut iter = iter.as_contiguous_iter().unwrap(); let mut present = [false; 6]; let mut len = 0; @@ -3622,6 +3623,12 @@ mod tests { } assert_eq!(present, [true, false, true, false, true, false]); assert_eq!(len, 3); + + let mut query = world.query_filtered::<&C, Without>(); + let mut iter = query.iter(&world); + let mut iter = iter.as_contiguous_iter().unwrap(); + assert_eq!(iter.next().unwrap(), &[C(4)]); + assert!(iter.next().is_none()); } #[test] diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 0ea957f6158bc..86c0941ef589c 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -112,37 +112,6 @@ pub unsafe trait QueryFilter: WorldQuery { ) -> bool; } -/// Types that filter contiguous chunks of memory -/// -/// Some types which implement this trait: -/// - [`With`] and [`Without`] -/// -/// Some [`QueryFilter`]s which **do not** implement this trait: -/// - [`Added`], [`Changed`] and [`Spawned`] due to their selective filters within contiguous chunks of memory -/// (i.e., it might exclude entities thus breaking contiguity) -/// -// NOTE: The safety rules might not be used to optimize the library, it still might be better to ensure -// that contiguous query filters match their non-contiguous versions -/// # Safety -/// -/// - The result of [`ContiguousQueryFilter::filter_fetch_contiguous`] must be the same as -/// The value returned by every call of [`QueryFilter::filter_fetch`] on the same table for every entity -/// (i.e., the value depends on the table not an entity) -pub unsafe trait ContiguousQueryFilter: QueryFilter { - /// # Safety - /// - /// - Must always be called _after_ [`WorldQuery::set_table`] - /// - `entities`'s length must match the length of the set table. - /// - `entities` must match the entities of the set table. - /// - `offset` must be less than the length of the set table. - unsafe fn filter_fetch_contiguous( - state: &Self::State, - fetch: &mut Self::Fetch<'_>, - entities: &[Entity], - offset: usize, - ) -> bool; -} - /// Filter that selects entities with a component `T`. /// /// This can be used in a [`Query`](crate::system::Query) if entities are required to have the @@ -247,19 +216,6 @@ unsafe impl QueryFilter for With { } } -// SAFETY: [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch_contiguous`] both always return true -unsafe impl ContiguousQueryFilter for With { - #[inline(always)] - unsafe fn filter_fetch_contiguous( - _state: &Self::State, - _fetch: &mut Self::Fetch<'_>, - _table_entities: &[Entity], - _offset: usize, - ) -> bool { - true - } -} - /// Filter that selects entities without a component `T`. /// /// This is the negation of [`With`]. @@ -361,19 +317,6 @@ unsafe impl QueryFilter for Without { } } -// SAFETY: [`QueryFilter::filter_fetch`] and [`ContiguousQueryFilter::filter_fetch_contiguous`] both always return true -unsafe impl ContiguousQueryFilter for Without { - #[inline(always)] - unsafe fn filter_fetch_contiguous( - _state: &Self::State, - _fetch: &mut Self::Fetch<'_>, - _table_entities: &[Entity], - _offset: usize, - ) -> bool { - true - } -} - /// A filter that tests if any of the given filters apply. /// /// This is useful for example if a system with multiple components in a query only wants to run @@ -585,38 +528,6 @@ macro_rules! impl_or_query_filter { || !(false $(|| $filter.matches)*)) } } - - #[expect( - clippy::allow_attributes, - reason = "This is a tuple-related macro; as such the lints below may not always apply." - )] - #[allow( - non_snake_case, - reason = "The names of some variables are provided by the macro's caller, not by us." - )] - #[allow( - unused_variables, - reason = "Zero-length tuples won't use any of the parameters." - )] - $(#[$meta])* - // SAFETY: `filter_fetch_contiguous` matches the implementation of `filter_fetch` - unsafe impl<$($filter: ContiguousQueryFilter),*> ContiguousQueryFilter for Or<($($filter,)*)> { - #[inline(always)] - unsafe fn filter_fetch_contiguous( - state: &Self::State, - fetch: &mut Self::Fetch<'_>, - entities: &[Entity], - offset: usize, - ) -> bool { - let ($($state,)*) = state; - let ($($filter,)*) = fetch; - - (Self::IS_ARCHETYPAL - // SAFETY: The invariants are upheld by the caller - $(|| ($filter.matches && unsafe { $filter::filter_fetch_contiguous($state, &mut $filter.fetch, entities, offset) }))* - || !(false $(|| $filter.matches)*)) - } - } }; } @@ -652,34 +563,6 @@ macro_rules! impl_tuple_query_filter { true $(&& unsafe { $name::filter_fetch($state, $name, entity, table_row) })* } } - - #[expect( - clippy::allow_attributes, - reason = "This is a tuple-related macro; as such the lints below may not always apply." - )] - #[allow( - non_snake_case, - reason = "The names of some variables are provided by the macro's caller, not by us." - )] - #[allow( - unused_variables, - reason = "Zero-length tuples won't use any of the parameters." - )] - // SAFETY: Implied by individual safety guarantees of the tuple's types - unsafe impl<$($name: ContiguousQueryFilter),*> ContiguousQueryFilter for ($($name,)*) { - unsafe fn filter_fetch_contiguous( - state: &Self::State, - fetch: &mut Self::Fetch<'_>, - table_entities: &[Entity], - offset: usize, - ) -> bool { - let ($($state,)*) = state; - let ($($name,)*) = fetch; - // SAFETY: The invariants are upheld by the caller. - true $(&& unsafe { $name::filter_fetch_contiguous($state, $name, table_entities, offset) })* - } - } - }; } @@ -1356,8 +1239,9 @@ unsafe impl QueryFilter for Spawned { /// A marker trait to indicate that the filter works at an archetype level. /// -/// This is needed to implement [`ExactSizeIterator`] for -/// [`QueryIter`](crate::query::QueryIter) that contains archetype-level filters. +/// This is needed to: +/// - implement [`ExactSizeIterator`] for [`QueryIter`](crate::query::QueryIter) that contains archetype-level filters. +/// - ensure table filtering for [`QueryContiguousIter`](crate::query::QueryContiguousIter). /// /// The trait must only be implemented for filters where its corresponding [`QueryFilter::IS_ARCHETYPAL`] /// is [`prim@true`]. As such, only the [`With`] and [`Without`] filters can implement the trait. diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 0992be08d2ead..475c5d05cb71f 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -5,8 +5,8 @@ use crate::{ change_detection::Tick, entity::{ContainsEntity, Entities, Entity, EntityEquivalent, EntitySet, EntitySetIterator}, query::{ - ArchetypeFilter, ArchetypeQueryData, ContiguousQueryData, ContiguousQueryFilter, - DebugCheckedUnwrap, QueryState, StorageId, + ArchetypeFilter, ArchetypeQueryData, ContiguousQueryData, DebugCheckedUnwrap, QueryState, + StorageId, }, storage::{Table, TableRow, Tables}, world::{ @@ -908,7 +908,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { pub fn as_contiguous_iter(&mut self) -> Option> where D: ContiguousQueryData, - F: ContiguousQueryFilter, + F: ArchetypeFilter, { self.cursor .is_dense @@ -1007,14 +1007,14 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter> Clone for QueryIter<'w, 's, D } /// Iterator for contiguous chunks of memory -pub struct QueryContiguousIter<'a, 'w, 's, D: ContiguousQueryData, F: ContiguousQueryFilter> { +pub struct QueryContiguousIter<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> { iter: &'a mut QueryIter<'w, 's, D, F>, } impl<'a, 'w, 's, D, F> Iterator for QueryContiguousIter<'a, 'w, 's, D, F> where D: ContiguousQueryData, - F: ContiguousQueryFilter, + F: ArchetypeFilter, { type Item = D::Contiguous<'w, 's>; @@ -2583,8 +2583,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { ) -> Option> where D: ContiguousQueryData, - F: ContiguousQueryFilter, + F: ArchetypeFilter, { + // SAFETY: Refer to [`Self::next`] loop { if self.current_row == self.current_len { let table_id = self.storage_id_iter.next()?.table_id; @@ -2602,14 +2603,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { let offset = self.current_row as usize; self.current_row = self.current_len; - if !F::filter_fetch_contiguous( - &query_state.filter_state, - &mut self.filter, - self.table_entities, - offset, - ) { - continue; - } + // no filtering because `F` implements `ArchetypeFilter` which ensures that `QueryFilter::fetch` + // always returns true let item = D::fetch_contiguous( &query_state.fetch_state, @@ -2638,6 +2633,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { query_state: &'s QueryState, ) -> Option> { if self.is_dense { + // NOTE: If you are changing this branch's code (the self.is_dense branch), + // don't forget to update [`Self::next_contiguous`] loop { // we are on the beginning of the query, or finished processing a table, so skip to the next if self.current_row == self.current_len { From d66a0cbb50dd6f2f53ea517bdf8450631b464c8c Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:32:08 +0100 Subject: [PATCH 08/52] macro --- Cargo.toml | 11 + .../tests/ui/world_query_derive.rs | 2 +- .../tests/ui/world_query_derive.stderr | 2 +- crates/bevy_ecs/macros/src/query_data.rs | 196 +++++++++++++++++- crates/bevy_ecs/src/query/fetch.rs | 113 +++++++++- examples/ecs/contiguous_query.rs | 55 +++++ examples/ecs/custom_query_param.rs | 35 +++- 7 files changed, 407 insertions(+), 7 deletions(-) create mode 100644 examples/ecs/contiguous_query.rs diff --git a/Cargo.toml b/Cargo.toml index d039936d83779..dafb735f40887 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4877,3 +4877,14 @@ name = "Pan Camera" description = "Example Pan-Camera Styled Camera Controller for 2D scenes" category = "Camera" wasm = true + +[[example]] +name = "contiguous_query" +path = "examples/ecs/contiguous_query.rs" +doc-scrape-examples = true + +[[package.metadata.example.contiguous_query]] +name = "Contiguous query" +description = "Demonstrates contiguous queries" +category = "ECS (Entity Component System)" +wasm = false diff --git a/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs index 9c2c5832e5f26..ce4ea266db9f6 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs +++ b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs @@ -12,7 +12,7 @@ struct MutableUnmarked { #[derive(QueryData)] #[query_data(mut)] -//~^ ERROR: invalid attribute, expected `mutable` or `derive` +//~^ ERROR: invalid attribute, expected `mutable`, `derive` or `contiguous` struct MutableInvalidAttribute { a: &'static mut Foo, } diff --git a/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr index ec71c112a6a05..8884a6d184a0b 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr @@ -1,4 +1,4 @@ -error: invalid attribute, expected `mutable` or `derive` +error: invalid attribute, expected `mutable`, `derive` or `contiguous` --> tests/ui/world_query_derive.rs:14:14 | 14 | #[query_data(mut)] diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 1e9dea94bbca3..2c48149611aa9 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -3,7 +3,8 @@ use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; use quote::{format_ident, quote}; use syn::{ - parse_macro_input, parse_quote, punctuated::Punctuated, token::Comma, DeriveInput, Meta, + parse_macro_input, parse_quote, punctuated::Punctuated, token::Comma, Attribute, DeriveInput, + Fields, ImplGenerics, Member, Meta, Type, TypeGenerics, Visibility, WhereClause, }; use crate::{ @@ -15,11 +16,15 @@ use crate::{ struct QueryDataAttributes { pub is_mutable: bool, + pub is_contiguous_mutable: bool, + pub is_contiguous_immutable: bool, + pub derive_args: Punctuated, } static MUTABLE_ATTRIBUTE_NAME: &str = "mutable"; static DERIVE_ATTRIBUTE_NAME: &str = "derive"; +static CONTIGUOUS_ATTRIBUTE_NAME: &str = "contiguous"; mod field_attr_keywords { syn::custom_keyword!(ignore); @@ -27,6 +32,80 @@ mod field_attr_keywords { pub static QUERY_DATA_ATTRIBUTE_NAME: &str = "query_data"; +fn contiguous_item_struct( + path: &syn::Path, + fields: &Fields, + derive_macro_call: &proc_macro2::TokenStream, + struct_name: &Ident, + visibility: &Visibility, + item_struct_name: &Ident, + field_types: &Vec, + user_impl_generics_with_world_and_state: &ImplGenerics, + field_attrs: &Vec>, + field_visibilities: &Vec, + field_members: &Vec, + user_ty_generics: &TypeGenerics, + user_ty_generics_with_world_and_state: &TypeGenerics, + user_where_clauses_with_world_and_state: Option<&WhereClause>, +) -> proc_macro2::TokenStream { + match fields { + Fields::Named(_) => quote! { + #derive_macro_call + #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state { + #(#(#field_attrs)* #field_visibilities #field_members: <#field_types as #path::query::ContiguousQueryData>::Contiguous<'__w, '__s>,)* + } + }, + Fields::Unnamed(_) => quote! { + #derive_macro_call + #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state ( + #( #field_visibilities <#field_types as #path::query::ContiguousQueryData>::Contiguous<'__w, '__s>, )* + ) + }, + Fields::Unit => quote! { + #visibility type #item_struct_name #user_ty_generics_with_world_and_state = #struct_name #user_ty_generics; + }, + } +} + +fn contiguous_query_data_impl( + path: &syn::Path, + struct_name: &Ident, + contiguous_item_struct_name: &Ident, + field_types: &Vec, + user_impl_generics: &ImplGenerics, + user_ty_generics: &TypeGenerics, + user_ty_generics_with_world_and_state: &TypeGenerics, + field_members: &Vec, + field_aliases: &Vec, + user_where_clauses: Option<&WhereClause>, +) -> proc_macro2::TokenStream { + quote! { + // SAFETY: Individual `fetch_contiguous` are called. + unsafe impl #user_impl_generics #path::query::ContiguousQueryData for #struct_name #user_ty_generics #user_where_clauses { + type Contiguous<'__w, '__s> = #contiguous_item_struct_name #user_ty_generics_with_world_and_state; + + unsafe fn fetch_contiguous<'__w, '__s>( + _state: &'__s ::State, + _fetch: &mut ::Fetch<'__w>, + _entities: &'__w [#path::entity::Entity], + _offset: usize, + ) -> Self::Contiguous<'__w, '__s> { + #contiguous_item_struct_name { + #( + #field_members: + <#field_types>::fetch_contiguous( + &_state.#field_aliases, + &mut _fetch.#field_aliases, + _entities, + _offset, + ), + )* + } + } + } + } +} + pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { let tokens = input.clone(); @@ -48,8 +127,24 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { attributes.derive_args.push(Meta::Path(meta.path)); Ok(()) }) + } else if meta.path.is_ident(CONTIGUOUS_ATTRIBUTE_NAME) { + meta.parse_nested_meta(|meta| { + if meta.path.is_ident("all") { + attributes.is_contiguous_mutable = true; + attributes.is_contiguous_immutable = true; + Ok(()) + } else if meta.path.is_ident("mutable") { + attributes.is_contiguous_mutable = true; + Ok(()) + } else if meta.path.is_ident("immutable") { + attributes.is_contiguous_immutable = true; + Ok(()) + } else { + Err(meta.error("invalid target, expected `all`, `mutable` or `immutable`")) + } + }) } else { - Err(meta.error(format_args!("invalid attribute, expected `{MUTABLE_ATTRIBUTE_NAME}` or `{DERIVE_ATTRIBUTE_NAME}`"))) + Err(meta.error(format_args!("invalid attribute, expected `{MUTABLE_ATTRIBUTE_NAME}`, `{DERIVE_ATTRIBUTE_NAME}` or `{CONTIGUOUS_ATTRIBUTE_NAME}`"))) } }); @@ -94,6 +189,19 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { } else { item_struct_name.clone() }; + let contiguous_item_struct_name = if attributes.is_contiguous_mutable { + Ident::new(&format!("{struct_name}ContiguousItem"), Span::call_site()) + } else { + item_struct_name.clone() + }; + let read_only_contiguous_item_struct_name = if attributes.is_contiguous_immutable { + Ident::new( + &format!("{struct_name}ReadOnlyContiguousItem"), + Span::call_site(), + ) + } else { + item_struct_name.clone() + }; let fetch_struct_name = Ident::new(&format!("{struct_name}Fetch"), Span::call_site()); let fetch_struct_name = ensure_no_collision(fetch_struct_name, tokens.clone()); @@ -124,7 +232,7 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { .members() .map(|m| format_ident!("field{}", m)) .collect(); - let field_types: Vec = fields.iter().map(|f| f.ty.clone()).collect(); + let field_types: Vec = fields.iter().map(|f| f.ty.clone()).collect(); let read_only_field_types = field_types .iter() .map(|ty| parse_quote!(<#ty as #path::query::QueryData>::ReadOnly)) @@ -167,6 +275,43 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { user_where_clauses_with_world, ); + let (mutable_contiguous_item_struct, mutable_contiguous_impl) = + if attributes.is_contiguous_mutable { + let contiguous_item_struct = contiguous_item_struct( + &path, + fields, + &derive_macro_call, + &struct_name, + &visibility, + &contiguous_item_struct_name, + &field_types, + &user_impl_generics_with_world_and_state, + &field_attrs, + &field_visibilities, + &field_members, + &user_ty_generics, + &user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, + ); + + let contiguous_impl = contiguous_query_data_impl( + &path, + &struct_name, + &contiguous_item_struct_name, + &field_types, + &user_impl_generics, + &user_ty_generics, + &user_ty_generics_with_world_and_state, + &field_members, + &field_aliases, + user_where_clauses, + ); + + (contiguous_item_struct, contiguous_impl) + } else { + (quote! {}, quote! {}) + }; + let (read_only_struct, read_only_impl) = if attributes.is_mutable { // If the query is mutable, we need to generate a separate readonly version of some things let readonly_item_struct = item_struct( @@ -226,6 +371,43 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { (quote! {}, quote! {}) }; + let (read_only_contiguous_item_struct, read_only_contiguous_impl) = + if attributes.is_mutable && attributes.is_contiguous_immutable { + let contiguous_item_struct = contiguous_item_struct( + &path, + fields, + &derive_macro_call, + &read_only_struct_name, + &visibility, + &read_only_contiguous_item_struct_name, + &read_only_field_types, + &user_impl_generics_with_world_and_state, + &field_attrs, + &field_visibilities, + &field_members, + &user_ty_generics, + &user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, + ); + + let contiguous_impl = contiguous_query_data_impl( + &path, + &read_only_struct_name, + &read_only_contiguous_item_struct_name, + &read_only_field_types, + &user_impl_generics, + &user_ty_generics, + &user_ty_generics_with_world_and_state, + &field_members, + &field_aliases, + user_where_clauses, + ); + + (contiguous_item_struct, contiguous_impl) + } else { + (quote! {}, quote! {}) + }; + let data_impl = { let read_only_data_impl = if attributes.is_mutable { quote! { @@ -391,6 +573,10 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { #read_only_struct + #mutable_contiguous_item_struct + + #read_only_contiguous_item_struct + const _: () = { #[doc(hidden)] #[doc = concat!( @@ -412,6 +598,10 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { #data_impl #read_only_data_impl + + #mutable_contiguous_impl + + #read_only_contiguous_impl }; #[allow(dead_code)] diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index b4c79b4ed0568..f504155ef96ba 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -356,7 +356,7 @@ pub unsafe trait QueryData: WorldQuery { /// /// - The result of [`ContiguousQueryData::fetch_contiguous`] must represent the same result as if /// [`QueryData::fetch`] was executed for each entity of the set table -pub unsafe trait ContiguousQueryData: QueryData { +pub unsafe trait ContiguousQueryData: ArchetypeQueryData { /// Item returned by [`ContiguousQueryData::fetch_contiguous`]. /// Represents a contiguous chunk of memory. type Contiguous<'w, 's>; @@ -3050,6 +3050,44 @@ macro_rules! impl_anytuple_fetch { $(#[$meta])* impl<$($name: ArchetypeQueryData),*> ArchetypeQueryData for AnyOf<($($name,)*)> {} + + + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + non_snake_case, + reason = "The names of some variables are provided by the macro's caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + #[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." + )] + $(#[$meta])* + // SAFETY: Matches the fetch implementation + unsafe impl<$($name: ContiguousQueryData),*> ContiguousQueryData for AnyOf<($($name,)*)> { + type Contiguous<'w, 's> = ($(Option<$name::Contiguous<'w,'s>>,)*); + + unsafe fn fetch_contiguous<'w, 's>( + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entities: &'w [Entity], + offset: usize, + ) -> Self::Contiguous<'w, 's> { + let ($($name,)*) = fetch; + let ($($state,)*) = state; + // Matches the [`QueryData::fetch`] except it always returns Some + ($( + // SAFETY: The invariants are upheld by the caller + $name.1.then(|| unsafe { $name::fetch_contiguous($state, &mut $name.0, entities, offset) }), + )*) + } + } }; } @@ -3645,4 +3683,77 @@ mod tests { assert!(iter.as_contiguous_iter().is_none()); assert_eq!(iter.next().unwrap().as_ref(), &S(0)); } + + #[test] + fn any_of_contiguous_test() { + #[derive(Component, Debug, Clone, Copy)] + pub struct C(i32); + + #[derive(Component, Debug, Clone, Copy)] + pub struct D(i32); + + let mut world = World::new(); + world.spawn((C(0), D(1))); + world.spawn(C(2)); + world.spawn(D(3)); + world.spawn(()); + + let mut query = world.query::>(); + let mut iter = query.iter(&world); + let mut present = [false; 4]; + + for (c, d) in iter.as_contiguous_iter().unwrap() { + assert!(c.is_some() || d.is_some()); + let c = c.unwrap_or(&[]); + let d = d.unwrap_or(&[]); + for i in 0..c.len().max(d.len()) { + let c = c.get(i).cloned(); + let d = d.get(i).cloned(); + if let Some(C(c)) = c { + assert!(!present[c as usize]); + present[c as usize] = true; + } + if let Some(D(d)) = d { + assert!(!present[d as usize]); + present[d as usize] = true; + } + } + } + + assert_eq!(present, [true; 4]); + } + + #[test] + fn option_contiguous_test() { + #[derive(Component, Clone, Copy)] + struct C(i32); + + #[derive(Component, Clone, Copy)] + struct D(i32); + + let mut world = World::new(); + world.spawn((C(0), D(1))); + world.spawn(D(2)); + world.spawn(C(3)); + + let mut query = world.query::<(Option<&C>, &D)>(); + let mut iter = query.iter(&world); + let mut present = [false; 3]; + + for (c, d) in iter.as_contiguous_iter().unwrap() { + let c = c.unwrap_or(&[]); + for i in 0..d.len() { + let c = c.get(i).cloned(); + let D(d) = d[i]; + if let Some(C(c)) = c { + assert!(!present[c as usize]); + present[c as usize] = true; + } + assert!(!present[d as usize]); + present[d as usize] = true; + } + } + + assert_eq!(present, [true; 3]); + } } diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs new file mode 100644 index 0000000000000..a435967dd36fb --- /dev/null +++ b/examples/ecs/contiguous_query.rs @@ -0,0 +1,55 @@ +//! Demonstrates how contiguous queries work + +use bevy::prelude::*; + +#[derive(Component)] +/// When the value reaches 0.0 the entity dies +pub struct Health(pub f32); + +#[derive(Component)] +/// Each tick an entity will have his health multiplied by the factor, which +/// for a big amount of entities can be accelerated using contiguous queries +pub struct HealthDecay(pub f32); + +fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { + // as_contiguous_iter() would return None if query couldn't be iterated contiguously + for ((health, _health_ticks), decay) in query.iter_mut().as_contiguous_iter().unwrap() { + // all slices returned by component queries are the same size + assert!(health.len() == decay.len()); + for i in 0..health.len() { + health[i].0 *= decay[i].0; + } + // we could have updated health's ticks but it is unnecessary hence we can make less work + // _health_ticks.mark_all_as_updated(); + } +} + +fn finish_off_first(mut commands: Commands, mut query: Query<(Entity, &mut Health)>) { + if let Some((entity, mut health)) = query.iter_mut().next() { + health.0 -= 1.0; + if health.0 <= 0.0 { + commands.entity(entity).despawn(); + println!("Finishing off {entity:?}"); + } + } +} + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Update, (apply_health_decay, finish_off_first).chain()) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands) { + let mut i = 0; + commands.spawn_batch(std::iter::from_fn(move || { + i += 1; + if i == 10_000 { + None + } else { + Some((Health(i as f32 * 5.0), HealthDecay(0.9))) + } + })); +} diff --git a/examples/ecs/custom_query_param.rs b/examples/ecs/custom_query_param.rs index 8960557ef4e85..28025915bd9a5 100644 --- a/examples/ecs/custom_query_param.rs +++ b/examples/ecs/custom_query_param.rs @@ -28,6 +28,7 @@ fn main() { print_components_iter_mut, print_components_iter, print_components_tuple, + print_components_contiguous_iter, ) .chain(), ) @@ -111,7 +112,7 @@ struct NestedQuery { } #[derive(QueryData)] -#[query_data(derive(Debug))] +#[query_data(derive(Debug), contiguous(mutable))] struct GenericQuery { generic: (&'static T, &'static P), } @@ -193,3 +194,35 @@ fn print_components_tuple( println!("Generic: {generic_c:?} {generic_d:?}"); } } + +/// If you are going to contiguously iterate the data in a query, you must mark it with the `contiguous` attribute, +/// which accepts one of 3 targets (`all`, `immutable` and `mutable`) +/// +/// - `all` will make read only query as well as mutable query both be able to be iterated contiguosly +/// - `mutable` will only make the original query (i.e., in that case [`CustomContiguousQuery`]) be able to be iterated contiguously +/// - `immutable` will only make the read only query (which is only useful when you mark the original query as `mutable`) +/// be able to be iterated contiguously +#[derive(QueryData)] +#[query_data(derive(Debug), contiguous(all))] +struct CustomContiguousQuery { + entity: Entity, + a: &'static ComponentA, + b: Option<&'static ComponentB>, + generic: GenericQuery, +} + +fn print_components_contiguous_iter(query: Query>) { + println!("Print components (contiguous_iter):"); + for e in query.iter().as_contiguous_iter().unwrap() { + let e: CustomContiguousQueryContiguousItem<'_, '_, _, _> = e; + for i in 0..e.entity.len() { + println!("Entity: {:?}", e.entity[i]); + println!("A: {:?}", e.a[i]); + println!("B: {:?}", e.b.map(|b| &b[i])); + println!( + "Generic: {:?} {:?}", + e.generic.generic.0[i], e.generic.generic.1[i] + ); + } + } +} From 9e54e50175ed21a5631e20d371dbd939e9394eac Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:43:12 +0100 Subject: [PATCH 09/52] added example into the table --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 440c54d0c2bdb..31dbaff9b97c0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -311,6 +311,7 @@ Example | Description --- | --- [Change Detection](../examples/ecs/change_detection.rs) | Change detection on components and resources [Component Hooks](../examples/ecs/component_hooks.rs) | Define component hooks to manage component lifecycle events +[Contiguous Query](../examples/ecs/contiguous_query.rs) | Demonstrates contiguous queries [Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type [Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules [Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components From c5aa42631c798031a750b08423cb6a5c7d08f095 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:30:37 +0100 Subject: [PATCH 10/52] example's metadata fix --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dafb735f40887..ddcaf238ad235 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4883,7 +4883,7 @@ name = "contiguous_query" path = "examples/ecs/contiguous_query.rs" doc-scrape-examples = true -[[package.metadata.example.contiguous_query]] +[package.metadata.example.contiguous_query] name = "Contiguous query" description = "Demonstrates contiguous queries" category = "ECS (Entity Component System)" From cd000c1e7f59cf46a50726cbf150a53c82cbf412 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:36:44 +0100 Subject: [PATCH 11/52] example fix --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ddcaf238ad235..951fc9dced9a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4884,7 +4884,7 @@ path = "examples/ecs/contiguous_query.rs" doc-scrape-examples = true [package.metadata.example.contiguous_query] -name = "Contiguous query" +name = "Contiguous Query" description = "Demonstrates contiguous queries" category = "ECS (Entity Component System)" wasm = false From 420dbd57e16717180e23a482f5cdfbb4886b99e1 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:14:31 +0100 Subject: [PATCH 12/52] (Contiguous)QueryData's docs --- crates/bevy_ecs/src/query/fetch.rs | 53 +++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index f504155ef96ba..91a62a80bffcc 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -98,14 +98,16 @@ use variadics_please::all_tuples; /// /// ## Macro expansion /// -/// Expanding the macro will declare one or three additional structs, depending on whether or not the struct is marked as mutable. +/// Expanding the macro will declare one to five additional structs, depending on whether or not the struct is marked as mutable or as contiguous. /// For a struct named `X`, the additional structs will be: /// -/// |Struct name|`mutable` only|Description| -/// |:---:|:---:|---| -/// |`XItem`|---|The type of the query item for `X`| -/// |`XReadOnlyItem`|✓|The type of the query item for `XReadOnly`| -/// |`XReadOnly`|✓|[`ReadOnly`] variant of `X`| +/// |Struct name|`mutable` only|`contiguous` target|Description| +/// |:---:|:---:|:---:|---| +/// |`XItem`|---|---|The type of the query item for `X`| +/// |`XReadOnlyItem`|✓|---|The type of the query item for `XReadOnly`| +/// |`XReadOnly`|✓|---|[`ReadOnly`] variant of `X`| +/// |`XContiguousItem`|---|`mutable` or `all`|The type of the contiguous query item for `X`| +/// |`XContiguousReadOnlyItem`|✓|`immutable` or `all`|The type of the contiguous query item for `XReadOnly`| /// /// ## Adding mutable references /// @@ -141,11 +143,38 @@ use variadics_please::all_tuples; /// } /// ``` /// +/// ## Adding contiguous items +/// +/// To create contiguous items additionally, the struct must be marked with the `#[query_data(contiguous(target))]` attribute, +/// where the target may be `all`, `mutable` or `immutable` (see the table above). +/// +/// For mutable queries it may be done like that: +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_ecs::query::QueryData; +/// # +/// # #[derive(Component)] +/// # struct ComponentA; +/// # +/// #[derive(QueryData)] +/// /// - contiguous(all) will create contiguous items for both read and mutable versions +/// /// - contiguous(mutable) will only create a contiguous item for the mutable version +/// /// - contiguous(immutable) will only create a contiguous item for the read only version +/// #[query_data(mutable, contiguous(all))] +/// struct CustomQuery { +/// component_a: &'static mut ComponentA, +/// } +/// ``` +/// +/// For immutable queries `contiguous(immutable)` attribute will be **ignored**, meanwhile `contiguous(mutable)` and `contiguous(all)` +/// will only generate a contiguous item for the (original) read only version. +/// /// ## Adding methods to query items /// /// It is possible to add methods to query items in order to write reusable logic about related components. /// This will often make systems more readable because low level logic is moved out from them. -/// It is done by adding `impl` blocks with methods for the `-Item` or `-ReadOnlyItem` generated structs. +/// It is done by adding `impl` blocks with methods for the `-Item`, `-ReadOnlyItem`, `-ContiguousItem` or `ContiguousReadOnlyItem` +/// generated structs. /// /// ``` /// # use bevy_ecs::prelude::*; @@ -210,7 +239,7 @@ use variadics_please::all_tuples; /// # struct ComponentA; /// # /// #[derive(QueryData)] -/// #[query_data(mutable, derive(Debug))] +/// #[query_data(mutable, derive(Debug), contiguous(all))] /// struct CustomQuery { /// component_a: &'static ComponentA, /// } @@ -220,6 +249,8 @@ use variadics_please::all_tuples; /// /// assert_debug::(); /// assert_debug::(); +/// assert_debug::(); +/// assert_debug::(); /// ``` /// /// ## Query composition @@ -356,6 +387,12 @@ pub unsafe trait QueryData: WorldQuery { /// /// - The result of [`ContiguousQueryData::fetch_contiguous`] must represent the same result as if /// [`QueryData::fetch`] was executed for each entity of the set table +#[diagnostic::on_unimplemented( + message = "`{Self}` cannot be iterated contiguously", + label = "invalid contiguous `Query` data", + note = "if `{Self}` is a component type, ensure that it's storage type is `StorageType::Table`", + note = "if `{Self}` is a custom query type, using `QueryData` derive macro, ensure that the `#[query_data(contiguous(target))]` attribute is added" +)] pub unsafe trait ContiguousQueryData: ArchetypeQueryData { /// Item returned by [`ContiguousQueryData::fetch_contiguous`]. /// Represents a contiguous chunk of memory. From 2fb9cd3cb4219262324376947b513b8e1742b387 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 3 Dec 2025 23:24:58 +0100 Subject: [PATCH 13/52] typo --- crates/bevy_ecs/src/query/fetch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 91a62a80bffcc..fca79f75c40c1 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -107,7 +107,7 @@ use variadics_please::all_tuples; /// |`XReadOnlyItem`|✓|---|The type of the query item for `XReadOnly`| /// |`XReadOnly`|✓|---|[`ReadOnly`] variant of `X`| /// |`XContiguousItem`|---|`mutable` or `all`|The type of the contiguous query item for `X`| -/// |`XContiguousReadOnlyItem`|✓|`immutable` or `all`|The type of the contiguous query item for `XReadOnly`| +/// |`XReadOnlyContiguousItem`|✓|`immutable` or `all`|The type of the contiguous query item for `XReadOnly`| /// /// ## Adding mutable references /// @@ -250,7 +250,7 @@ use variadics_please::all_tuples; /// assert_debug::(); /// assert_debug::(); /// assert_debug::(); -/// assert_debug::(); +/// assert_debug::(); /// ``` /// /// ## Query composition From 59cd6c62a6ad107a115096fca04e55c158fa5a89 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:27:06 +0100 Subject: [PATCH 14/52] docs fix --- crates/bevy_ecs/src/query/fetch.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index fca79f75c40c1..350d7f6ddf714 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -390,7 +390,6 @@ pub unsafe trait QueryData: WorldQuery { #[diagnostic::on_unimplemented( message = "`{Self}` cannot be iterated contiguously", label = "invalid contiguous `Query` data", - note = "if `{Self}` is a component type, ensure that it's storage type is `StorageType::Table`", note = "if `{Self}` is a custom query type, using `QueryData` derive macro, ensure that the `#[query_data(contiguous(target))]` attribute is added" )] pub unsafe trait ContiguousQueryData: ArchetypeQueryData { From 6c359ee4553a28ead2a3e4feaef25c5e5a23f583 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:04:08 +0100 Subject: [PATCH 15/52] Has contiguous impl --- crates/bevy_ecs/src/query/fetch.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 350d7f6ddf714..d786e1ae90f64 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2771,6 +2771,20 @@ impl ReleaseStateQueryData for Has { impl ArchetypeQueryData for Has {} +/// SAFETY: matches [`QueryData::fetch`] +unsafe impl ContiguousQueryData for Has { + type Contiguous<'w, 's> = bool; + + unsafe fn fetch_contiguous<'w, 's>( + _state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + _entities: &'w [Entity], + _offset: usize, + ) -> Self::Contiguous<'w, 's> { + *fetch + } +} + /// 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 083f714496ac8d02f215aea2add4c458161d7c57 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 4 Dec 2025 22:34:49 +0100 Subject: [PATCH 16/52] thinsliceptr refinements --- crates/bevy_ecs/macros/src/query_data.rs | 14 +++++++++++ .../bevy_ecs/src/change_detection/params.rs | 6 ++--- crates/bevy_ecs/src/query/fetch.rs | 20 +++++++++------- crates/bevy_ptr/src/lib.rs | 24 +++++++++++++++---- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 2c48149611aa9..f89e639763103 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -48,20 +48,34 @@ fn contiguous_item_struct( user_ty_generics_with_world_and_state: &TypeGenerics, user_where_clauses_with_world_and_state: Option<&WhereClause>, ) -> proc_macro2::TokenStream { + let item_attrs = quote! { + #[doc = concat!( + "Automatically generated [`ContiguousQueryData`](", + stringify!(#path), + "::fetch::ContiguousQueryData) item type for [`", + stringify!(#struct_name), + "`], returned when iterating over contiguous query results", + )] + #[automatically_derived] + }; + match fields { Fields::Named(_) => quote! { #derive_macro_call + #item_attrs #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state { #(#(#field_attrs)* #field_visibilities #field_members: <#field_types as #path::query::ContiguousQueryData>::Contiguous<'__w, '__s>,)* } }, Fields::Unnamed(_) => quote! { #derive_macro_call + #item_attrs #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state ( #( #field_visibilities <#field_types as #path::query::ContiguousQueryData>::Contiguous<'__w, '__s>, )* ) }, Fields::Unit => quote! { + #item_attrs #visibility type #item_struct_name #user_ty_generics_with_world_and_state = #struct_name #user_ty_generics; }, } diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 842e13824424f..bfa1646f23d66 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -478,7 +478,7 @@ impl<'w> ContiguousComponentTicks<'w, true> { /// Returns mutable changed ticks slice pub fn get_changed_ticks_mut(&mut self) -> &mut [Tick] { // SAFETY: `changed` slice is `self.count` long, aliasing rules are uphold by `new`. - unsafe { self.changed.as_mut_slice(self.count) } + unsafe { self.changed.as_mut_slice_unchecked(self.count) } } /// Marks all components as updated @@ -520,13 +520,13 @@ impl<'w, const MUTABLE: bool> ContiguousComponentTicks<'w, MUTABLE> { /// Returns immutable changed ticks slice pub fn get_changed_ticks(&self) -> &[Tick] { // SAFETY: `self.changed` is `self.count` long - unsafe { self.changed.cast::().as_slice(self.count) } + unsafe { self.changed.cast::().as_slice_unchecked(self.count) } } /// Returns immutable added ticks slice pub fn get_added_ticks(&self) -> &[Tick] { // SAFETY: `self.added` is `self.count` long - unsafe { self.added.cast::().as_slice(self.count) } + unsafe { self.added.cast::().as_slice_unchecked(self.count) } } /// Returns the last tick system ran diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index d786e1ae90f64..0310e10812837 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1773,7 +1773,7 @@ unsafe impl ContiguousQueryData for &T { // (i.e. repr(transparent)) of UnsafeCell let table = table.cast::(); // SAFETY: Caller ensures `rows` is the amount of rows in the table - let item = unsafe { table.as_slice(entities.len()) }; + let item = unsafe { table.as_slice_unchecked(entities.len()) }; &item[offset..] }, |_| { @@ -2027,11 +2027,13 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { unsafe { table.debug_checked_unwrap() }; ( - &table_components.cast::().as_slice(entities.len())[offset..], + &table_components + .cast::() + .as_slice_unchecked(entities.len())[offset..], ContiguousComponentTicks::<'w, false>::new( - added_ticks.add(offset), - changed_ticks.add(offset), - callers.map(|callers| callers.add(offset)), + added_ticks.add_unchecked(offset), + changed_ticks.add_unchecked(offset), + callers.map(|callers| callers.add_unchecked(offset)), entities.len() - offset, fetch.last_run, fetch.this_run, @@ -2277,11 +2279,11 @@ unsafe impl> ContiguousQueryData for &mut T { unsafe { table.debug_checked_unwrap() }; ( - &mut table_components.as_mut_slice(entities.len())[offset..], + &mut table_components.as_mut_slice_unchecked(entities.len())[offset..], ContiguousComponentTicks::<'w, true>::new( - added_ticks.add(offset), - changed_ticks.add(offset), - callers.map(|callers| callers.add(offset)), + added_ticks.add_unchecked(offset), + changed_ticks.add_unchecked(offset), + callers.map(|callers| callers.add_unchecked(offset)), entities.len() - offset, fetch.last_run, fetch.this_run, diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index cf89512d9550b..7a1cf2b67a193 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1109,7 +1109,7 @@ impl<'a, T> ThinSlicePtr<'a, T> { /// # Safety /// /// `len` must be less or equal to the length of the slice. - pub unsafe fn as_slice(&self, len: usize) -> &'a [T] { + pub unsafe fn as_slice_unchecked(&self, len: usize) -> &'a [T] { #[cfg(debug_assertions)] assert!(len <= self.len, "tried to create an out-of-bounds slice"); @@ -1121,8 +1121,24 @@ impl<'a, T> ThinSlicePtr<'a, T> { pub fn cast(&self) -> ThinSlicePtr<'a, U> { ThinSlicePtr { ptr: self.ptr.cast::(), + // self.len is equal the amount of elements of T in the slice, which takes + // size_of:: * self.len bytes, thus the length of the same slice but for U is the amount + // of bytes divided by the size of U. + // + // when the size of U is 0, then the length of the slice may be infinite. + // + // when the size of T is 0 as well, then we can logically assume that the lengths of the both slices (of type T, + // and of type U) are equal. #[cfg(debug_assertions)] - len: self.len * size_of::() / size_of::(), + len: if size_of::() == 0 { + if size_of::() == 0 { + self.len + } else { + isize::MAX as usize + } + } else { + self.len * size_of::() / size_of::() + }, _marker: PhantomData, } } @@ -1133,7 +1149,7 @@ impl<'a, T> ThinSlicePtr<'a, T> { /// /// - `count` must be less or equal to the length of the slice // The result pointer must lie within the same allocation - pub unsafe fn add(&self, count: usize) -> ThinSlicePtr<'a, T> { + pub unsafe fn add_unchecked(&self, count: usize) -> ThinSlicePtr<'a, T> { #[cfg(debug_assertions)] assert!( count <= self.len, @@ -1168,7 +1184,7 @@ impl<'a, T> ThinSlicePtr<'a, UnsafeCell> { /// /// - There must not be any aliases to the slice /// - `len` must be less or equal to the length of the slice - pub unsafe fn as_mut_slice(&self, len: usize) -> &'a mut [T] { + pub unsafe fn as_mut_slice_unchecked(&self, len: usize) -> &'a mut [T] { #[cfg(debug_assertions)] assert!(len <= self.len, "tried to create an out-of-bounds slice"); From 3881d638b91809224695024f0591b95954cc1ec4 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 4 Dec 2025 23:23:00 +0100 Subject: [PATCH 17/52] QueryContiguousIter additions --- crates/bevy_ecs/src/query/iter.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 475c5d05cb71f..f39c6222a2ba0 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -1011,10 +1011,8 @@ pub struct QueryContiguousIter<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeF iter: &'a mut QueryIter<'w, 's, D, F>, } -impl<'a, 'w, 's, D, F> Iterator for QueryContiguousIter<'a, 'w, 's, D, F> -where - D: ContiguousQueryData, - F: ArchetypeFilter, +impl<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> Iterator + for QueryContiguousIter<'a, 'w, 's, D, F> { type Item = D::Contiguous<'w, 's>; @@ -1029,6 +1027,22 @@ where .next_contiguous(self.iter.tables, self.iter.query_state) } } + + fn size_hint(&self) -> (usize, Option) { + self.iter.cursor.storage_id_iter.size_hint() + } +} + +// [`QueryIterationCursor::next_contiguous`] always returns None when exhausted +impl<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> FusedIterator + for QueryContiguousIter<'a, 'w, 's, D, F> +{ +} + +// self.iter.cursor.storage_id_iter is a slice's iterator hence has an exact size +impl<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> ExactSizeIterator + for QueryContiguousIter<'a, 'w, 's, D, F> +{ } /// An [`Iterator`] over sorted query results of a [`Query`](crate::system::Query). From 7d5ba3d6f2d6bd1b41223cbee89caf907da60fe7 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:33:34 +0100 Subject: [PATCH 18/52] ThinSlicePtr::cast refinements, test ticks and docs --- crates/bevy_ecs/src/query/fetch.rs | 3 ++- crates/bevy_ptr/src/lib.rs | 39 ++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 0310e10812837..d253b09b946f9 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -3694,10 +3694,11 @@ mod tests { let mut iter = query.iter_mut(&mut world); let mut iter = iter.as_contiguous_iter().unwrap(); for _ in 0..2 { - let c = iter.next().unwrap(); + let mut c = iter.next().unwrap(); for c in c.0 { c.0 *= 2; } + c.1.mark_all_as_updated(); } assert!(iter.next().is_none()); let mut iter = query.iter(&world); diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 7a1cf2b67a193..271771e3131a8 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1108,17 +1108,39 @@ impl<'a, T> ThinSlicePtr<'a, T> { /// /// # Safety /// - /// `len` must be less or equal to the length of the slice. + /// - There must be no mutable aliases for the lifetime `'a` to the slice. to the slice. + /// - `len` must be less than or equal to the length of the slice. pub unsafe fn as_slice_unchecked(&self, len: usize) -> &'a [T] { #[cfg(debug_assertions)] assert!(len <= self.len, "tried to create an out-of-bounds slice"); - // SAFETY: The caller guarantees `len` is not greater than the length of the slice + // SAFETY: + // - The caller guarantees `len` is not greater than the length of the slice. + // - The caller guarantess the aliasing rules. + // - `self.ptr` is a valid pointer for the type `T`. + // - `len` is valid hence `len * size_of::()` is less than `isize::MAX`. unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), len) } } /// Casts the slice to another type + /// + /// # Panics + /// + /// When the feature `debug_assertions` is enabled, panics, when the new type for + /// the slice doesn't use all bytes reserved by this slice. + /// (this can happen for example, when the size of `T` is not 0 modulo the size of `U`) + /// + /// When the feature `debug_assertions` is disabled, panics, when the size of `U` is 0 but the size of `T` is not. pub fn cast(&self) -> ThinSlicePtr<'a, U> { + #[cfg(debug_assertions)] + assert!( + size_of::() == 0 + || (size_of::() != 0 && self.len * size_of::() % size_of::() == 0) + ); + + // must be evaluated in the compilation + assert!(size_of::() != 0 || size_of::() == 0); + ThinSlicePtr { ptr: self.ptr.cast::(), // self.len is equal the amount of elements of T in the slice, which takes @@ -1134,7 +1156,7 @@ impl<'a, T> ThinSlicePtr<'a, T> { if size_of::() == 0 { self.len } else { - isize::MAX as usize + unreachable!() } } else { self.len * size_of::() / size_of::() @@ -1147,7 +1169,7 @@ impl<'a, T> ThinSlicePtr<'a, T> { /// /// # Safety /// - /// - `count` must be less or equal to the length of the slice + /// - `count` must be less than or equal to the length of the slice // The result pointer must lie within the same allocation pub unsafe fn add_unchecked(&self, count: usize) -> ThinSlicePtr<'a, T> { #[cfg(debug_assertions)] @@ -1182,13 +1204,16 @@ impl<'a, T> ThinSlicePtr<'a, UnsafeCell> { /// /// # Safety /// - /// - There must not be any aliases to the slice - /// - `len` must be less or equal to the length of the slice + /// - There must not be any aliases for the lifetime `'a` to the slice. + /// - `len` must be less than or equal to the length of the slice. pub unsafe fn as_mut_slice_unchecked(&self, len: usize) -> &'a mut [T] { #[cfg(debug_assertions)] assert!(len <= self.len, "tried to create an out-of-bounds slice"); - // SAFETY: The caller ensures no aliases exist and `len` is in-bounds. + // SAFETY: + // - The caller ensures no aliases exist and `len` is in-bounds. + // - `self.ptr` is a valid pointer for the type `T`. + // - `len` is valid hence `len * size_of::()` is less than `isize::MAX`. unsafe { core::slice::from_raw_parts_mut(UnsafeCell::raw_get(self.ptr.as_ptr()), len) } } } From 8571bd7bfe98ddd06743cc3637242bbc6ecc8f72 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:34:45 +0100 Subject: [PATCH 19/52] typo --- crates/bevy_ptr/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 271771e3131a8..b98d503b8089d 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1116,7 +1116,7 @@ impl<'a, T> ThinSlicePtr<'a, T> { // SAFETY: // - The caller guarantees `len` is not greater than the length of the slice. - // - The caller guarantess the aliasing rules. + // - The caller guarantees the aliasing rules. // - `self.ptr` is a valid pointer for the type `T`. // - `len` is valid hence `len * size_of::()` is less than `isize::MAX`. unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), len) } From d1d9386400cc75b9dc76dbf7dc40ebc7ab512308 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:26:14 +0100 Subject: [PATCH 20/52] cast refinements --- .../bevy_ecs/src/change_detection/params.rs | 12 +++++- crates/bevy_ecs/src/query/fetch.rs | 4 +- crates/bevy_ptr/src/lib.rs | 38 +++---------------- 3 files changed, 18 insertions(+), 36 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index bfa1646f23d66..500c9dab2980b 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -520,13 +520,21 @@ impl<'w, const MUTABLE: bool> ContiguousComponentTicks<'w, MUTABLE> { /// Returns immutable changed ticks slice pub fn get_changed_ticks(&self) -> &[Tick] { // SAFETY: `self.changed` is `self.count` long - unsafe { self.changed.cast::().as_slice_unchecked(self.count) } + unsafe { + self.changed + .cast_unchecked::() + .as_slice_unchecked(self.count) + } } /// Returns immutable added ticks slice pub fn get_added_ticks(&self) -> &[Tick] { // SAFETY: `self.added` is `self.count` long - unsafe { self.added.cast::().as_slice_unchecked(self.count) } + unsafe { + self.added + .cast_unchecked::() + .as_slice_unchecked(self.count) + } } /// Returns the last tick system ran diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index d253b09b946f9..81a467cf2cabe 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1771,7 +1771,7 @@ unsafe impl ContiguousQueryData for &T { let table = unsafe { table.debug_checked_unwrap() }; // UnsafeCell has the same alignment as T because of transparent representation // (i.e. repr(transparent)) of UnsafeCell - let table = table.cast::(); + let table = table.cast_unchecked::(); // SAFETY: Caller ensures `rows` is the amount of rows in the table let item = unsafe { table.as_slice_unchecked(entities.len()) }; &item[offset..] @@ -2028,7 +2028,7 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { ( &table_components - .cast::() + .cast_unchecked::() .as_slice_unchecked(entities.len())[offset..], ContiguousComponentTicks::<'w, false>::new( added_ticks.add_unchecked(offset), diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index b98d503b8089d..ba75b15bf82e9 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1124,43 +1124,17 @@ impl<'a, T> ThinSlicePtr<'a, T> { /// Casts the slice to another type /// - /// # Panics - /// - /// When the feature `debug_assertions` is enabled, panics, when the new type for - /// the slice doesn't use all bytes reserved by this slice. - /// (this can happen for example, when the size of `T` is not 0 modulo the size of `U`) - /// - /// When the feature `debug_assertions` is disabled, panics, when the size of `U` is 0 but the size of `T` is not. - pub fn cast(&self) -> ThinSlicePtr<'a, U> { + /// # Safety + /// - The type `U` must have the same in-memory representation as `T`. + pub unsafe fn cast_unchecked(&self) -> ThinSlicePtr<'a, U> { #[cfg(debug_assertions)] - assert!( - size_of::() == 0 - || (size_of::() != 0 && self.len * size_of::() % size_of::() == 0) - ); - - // must be evaluated in the compilation - assert!(size_of::() != 0 || size_of::() == 0); + // it doesn't fully ensure that `U` and `T` have the same in-memory representations + assert!(size_of::() == size_of::() && align_of::() == align_of::()); ThinSlicePtr { ptr: self.ptr.cast::(), - // self.len is equal the amount of elements of T in the slice, which takes - // size_of:: * self.len bytes, thus the length of the same slice but for U is the amount - // of bytes divided by the size of U. - // - // when the size of U is 0, then the length of the slice may be infinite. - // - // when the size of T is 0 as well, then we can logically assume that the lengths of the both slices (of type T, - // and of type U) are equal. #[cfg(debug_assertions)] - len: if size_of::() == 0 { - if size_of::() == 0 { - self.len - } else { - unreachable!() - } - } else { - self.len * size_of::() / size_of::() - }, + len: self.len, _marker: PhantomData, } } From 28d40ccddbc3d18ec9570d7a9117535661071544 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Tue, 9 Dec 2025 20:49:16 +0100 Subject: [PATCH 21/52] release notes --- .../bevy_ecs/src/change_detection/params.rs | 2 ++ .../release-notes/contiguous_access.md | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 release-content/release-notes/contiguous_access.md diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 500c9dab2980b..f1f88ae39da5f 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -482,6 +482,8 @@ impl<'w> ContiguousComponentTicks<'w, true> { } /// Marks all components as updated + /// + /// **Executes in O(n), where n is the amount of rows.** pub fn mark_all_as_updated(&mut self) { let this_run = self.this_run; diff --git a/release-content/release-notes/contiguous_access.md b/release-content/release-notes/contiguous_access.md new file mode 100644 index 0000000000000..8590a891f9495 --- /dev/null +++ b/release-content/release-notes/contiguous_access.md @@ -0,0 +1,31 @@ +--- +title: Contiguous access +authors: ["@Jenya705"] +pull_requests: [21984] +--- + +Enables accessing slices from tables directly via Queries. + +## Goals + +`QueryIter` has a new method `as_contiguous_iter`, which allows quering contiguously (i.e., over tables). For it to work the query data must implement `ContiguousQueryData` and the query filter `ArchetypeFilter`. When a contiguous iterator is used, the iterator will jump over whole tables, returning corresponding values. Some notable implementors of `ContiguousQueryData` are `&T` and `&mut T`, returning `&[T]` and `(&mut T, ContiguousComponentTicks)` correspondingly, where the latter structure in the latter tuple lets you change update ticks. Some notable implementors of `ArchetypeFilter` are `With` and `Without` and notable structs not implementing it are `Changed` and `Added`. + +This is for example useful, when an operation must be applied on a big amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. + +### Usage + +`QueryIter::as_contiguous_iter` method returns an `Option`, which is only `None`, when the query is not dense (i.e., iterates over archetypes, not over tables). + +```rust +fn apply_velocity(query: Query<(&Velocity, &mut Position)>) { + // `as_contiguous_iter()` cannot ensure all invariants on the compilation stage, thus + // when a component uses a sparse set storage, the method will return `None` + for (velocity, (position, mut ticks)) in query.iter_mut().as_contiguous_iter().unwrap() { + for (v, p) in velocity.iter().zip(position.iter_mut()) { + p.0 += v.0; + } + // sets ticks, which is optional + ticks.mark_all_as_updated(); + } +} +``` From f2afb53632f7863bdebbef83d1a3862f2d7d1017 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:16:00 +0100 Subject: [PATCH 22/52] format fix --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b8d90730c162f..44b104e1775ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4922,4 +4922,4 @@ required-features = ["pbr_clustered_decals", "https"] name = "Clustered Decal Maps" description = "Demonstrates normal and metallic-roughness maps of decals" category = "3D Rendering" -wasm = false \ No newline at end of file +wasm = false From 747782189fc491ee87dae8c8b8720c10b47b08c6 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:18:21 +0100 Subject: [PATCH 23/52] typo --- release-content/release-notes/contiguous_access.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/contiguous_access.md b/release-content/release-notes/contiguous_access.md index 8590a891f9495..9ba013b0b1b13 100644 --- a/release-content/release-notes/contiguous_access.md +++ b/release-content/release-notes/contiguous_access.md @@ -8,7 +8,7 @@ Enables accessing slices from tables directly via Queries. ## Goals -`QueryIter` has a new method `as_contiguous_iter`, which allows quering contiguously (i.e., over tables). For it to work the query data must implement `ContiguousQueryData` and the query filter `ArchetypeFilter`. When a contiguous iterator is used, the iterator will jump over whole tables, returning corresponding values. Some notable implementors of `ContiguousQueryData` are `&T` and `&mut T`, returning `&[T]` and `(&mut T, ContiguousComponentTicks)` correspondingly, where the latter structure in the latter tuple lets you change update ticks. Some notable implementors of `ArchetypeFilter` are `With` and `Without` and notable structs not implementing it are `Changed` and `Added`. +`QueryIter` has a new method `as_contiguous_iter`, which allows querying contiguously (i.e., over tables). For it to work the query data must implement `ContiguousQueryData` and the query filter `ArchetypeFilter`. When a contiguous iterator is used, the iterator will jump over whole tables, returning corresponding values. Some notable implementors of `ContiguousQueryData` are `&T` and `&mut T`, returning `&[T]` and `(&mut T, ContiguousComponentTicks)` correspondingly, where the latter structure in the latter tuple lets you change update ticks. Some notable implementors of `ArchetypeFilter` are `With` and `Without` and notable structs not implementing it are `Changed` and `Added`. This is for example useful, when an operation must be applied on a big amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. From f02d4c98c0b75a4c6f9746e08ac73ca426efe9a5 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 10 Dec 2025 20:18:50 +0100 Subject: [PATCH 24/52] cast --- .../bevy_ecs/src/change_detection/params.rs | 12 ++------ crates/bevy_ecs/src/query/fetch.rs | 6 ++-- crates/bevy_ptr/src/lib.rs | 28 ++++++++----------- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index f1f88ae39da5f..9f832fd29fad4 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -522,21 +522,13 @@ impl<'w, const MUTABLE: bool> ContiguousComponentTicks<'w, MUTABLE> { /// Returns immutable changed ticks slice pub fn get_changed_ticks(&self) -> &[Tick] { // SAFETY: `self.changed` is `self.count` long - unsafe { - self.changed - .cast_unchecked::() - .as_slice_unchecked(self.count) - } + unsafe { self.changed.cast().as_slice_unchecked(self.count) } } /// Returns immutable added ticks slice pub fn get_added_ticks(&self) -> &[Tick] { // SAFETY: `self.added` is `self.count` long - unsafe { - self.added - .cast_unchecked::() - .as_slice_unchecked(self.count) - } + unsafe { self.added.cast().as_slice_unchecked(self.count) } } /// Returns the last tick system ran diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 5f641768d320f..3e6c475941c6e 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1823,7 +1823,7 @@ unsafe impl ContiguousQueryData for &T { let table = unsafe { table.debug_checked_unwrap() }; // UnsafeCell has the same alignment as T because of transparent representation // (i.e. repr(transparent)) of UnsafeCell - let table = table.cast_unchecked::(); + let table = table.cast(); // SAFETY: Caller ensures `rows` is the amount of rows in the table let item = unsafe { table.as_slice_unchecked(entities.len()) }; &item[offset..] @@ -2083,9 +2083,7 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { unsafe { table.debug_checked_unwrap() }; ( - &table_components - .cast_unchecked::() - .as_slice_unchecked(entities.len())[offset..], + &table_components.cast().as_slice_unchecked(entities.len())[offset..], ContiguousComponentTicks::<'w, false>::new( added_ticks.add_unchecked(offset), changed_ticks.add_unchecked(offset), diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 1230bc2e61503..62dd1a2170f19 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1122,23 +1122,6 @@ impl<'a, T> ThinSlicePtr<'a, T> { unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), len) } } - /// Casts the slice to another type - /// - /// # Safety - /// - The type `U` must have the same in-memory representation as `T`. - pub unsafe fn cast_unchecked(&self) -> ThinSlicePtr<'a, U> { - #[cfg(debug_assertions)] - // it doesn't fully ensure that `U` and `T` have the same in-memory representations - assert!(size_of::() == size_of::() && align_of::() == align_of::()); - - ThinSlicePtr { - ptr: self.ptr.cast::(), - #[cfg(debug_assertions)] - len: self.len, - _marker: PhantomData, - } - } - /// Offsets the slice beginning by `count` elements /// /// # Safety @@ -1190,6 +1173,17 @@ impl<'a, T> ThinSlicePtr<'a, UnsafeCell> { // - `len` is valid hence `len * size_of::()` is less than `isize::MAX`. unsafe { core::slice::from_raw_parts_mut(UnsafeCell::raw_get(self.ptr.as_ptr()), len) } } + + /// Returns a slice pointer to the underlying type `T`. + pub fn cast(&self) -> ThinSlicePtr<'a, T> { + ThinSlicePtr { + // SAFETY: `self.ptr` is non null hence `UnsafeCell::raw_get` always returns a non null pointer + ptr: unsafe { NonNull::new_unchecked(UnsafeCell::raw_get(self.ptr.as_ptr())) }, + #[cfg(debug_assertions)] + len: self.len, + _marker: PhantomData, + } + } } impl<'a, T> Clone for ThinSlicePtr<'a, T> { From 0d74581f0d1d2fc5e68683d3236ef34fb200369e Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:50:06 +0100 Subject: [PATCH 25/52] Owned QueryContiguousIter --- .../iteration/iter_simple_contiguous.rs | 4 +- .../iteration/iter_simple_contiguous_avx2.rs | 6 +- .../iter_simple_no_detection_contiguous.rs | 4 +- crates/bevy_ecs/macros/src/query_data.rs | 2 - crates/bevy_ecs/src/query/fetch.rs | 71 ++++------ crates/bevy_ecs/src/query/iter.rs | 130 ++++++++---------- crates/bevy_ecs/src/query/state.rs | 35 ++++- crates/bevy_ecs/src/system/query.rs | 60 +++++++- examples/ecs/contiguous_query.rs | 2 +- examples/ecs/custom_query_param.rs | 3 +- 10 files changed, 187 insertions(+), 130 deletions(-) diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs index c40d59e90c3db..9621a4d79a00a 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs @@ -35,8 +35,8 @@ impl<'w> Benchmark<'w> { #[inline(never)] pub fn run(&mut self) { - let mut iter = self.1.iter_mut(&mut self.0); - for (velocity, (position, mut ticks)) in iter.as_contiguous_iter().unwrap() { + let iter = self.1.contiguous_iter_mut(&mut self.0).unwrap(); + for (velocity, (position, mut ticks)) in iter { for (v, p) in velocity.iter().zip(position.iter_mut()) { p.0 += v.0; } diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs index 837c92be8c190..bcb6a750bb2f9 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs @@ -46,14 +46,14 @@ impl<'w> Benchmark<'w> { /// # Safety /// avx2 must be supported #[target_feature(enable = "avx2")] - fn exec(position: &mut [Position], velocity: &[Velocity]) { + unsafe fn exec(position: &mut [Position], velocity: &[Velocity]) { for i in 0..position.len() { position[i].0 += velocity[i].0; } } - let mut iter = self.1.iter_mut(&mut self.0); - for (velocity, (position, mut ticks)) in iter.as_contiguous_iter().unwrap() { + let iter = self.1.contiguous_iter_mut(&mut self.0).unwrap(); + for (velocity, (position, mut ticks)) in iter { // SAFETY: checked in new unsafe { exec(position, velocity); diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs index ca8209bff2b5f..4df810f3fc8e0 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs @@ -35,8 +35,8 @@ impl<'w> Benchmark<'w> { #[inline(never)] pub fn run(&mut self) { - let mut iter = self.1.iter_mut(&mut self.0); - for (velocity, (position, _ticks)) in iter.as_contiguous_iter().unwrap() { + let iter = self.1.contiguous_iter_mut(&mut self.0).unwrap(); + for (velocity, (position, _ticks)) in iter { for (v, p) in velocity.iter().zip(position.iter_mut()) { p.0 += v.0; } diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index ef5262428e92f..ff081863d4424 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -102,7 +102,6 @@ fn contiguous_query_data_impl( _state: &'__s ::State, _fetch: &mut ::Fetch<'__w>, _entities: &'__w [#path::entity::Entity], - _offset: usize, ) -> Self::Contiguous<'__w, '__s> { #contiguous_item_struct_name { #( @@ -111,7 +110,6 @@ fn contiguous_query_data_impl( &_state.#field_aliases, &mut _fetch.#field_aliases, _entities, - _offset, ), )* } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 3e6c475941c6e..8522309c25371 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -413,13 +413,11 @@ pub unsafe trait ContiguousQueryData: ArchetypeQueryData { /// - Must always be called _after_ [`WorldQuery::set_table`]. /// - `entities`'s length must match the length of the set table. /// - `entities` must match the entities of the set table. - /// - `offset` must be less than the length of the set table. /// - There must not be simultaneous conflicting component access registered in `update_component_access`. unsafe fn fetch_contiguous<'w, 's>( state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's>; } @@ -554,9 +552,8 @@ unsafe impl ContiguousQueryData for Entity { _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's> { - &entities[offset..] + &entities } } @@ -1815,7 +1812,6 @@ unsafe impl ContiguousQueryData for &T { _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's> { fetch.components.extract( |table| { @@ -1826,7 +1822,7 @@ unsafe impl ContiguousQueryData for &T { let table = table.cast(); // SAFETY: Caller ensures `rows` is the amount of rows in the table let item = unsafe { table.as_slice_unchecked(entities.len()) }; - &item[offset..] + item }, |_| { #[cfg(debug_assertions)] @@ -2074,7 +2070,6 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's> { fetch.components.extract( |table| { @@ -2083,12 +2078,12 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { unsafe { table.debug_checked_unwrap() }; ( - &table_components.cast().as_slice_unchecked(entities.len())[offset..], + table_components.cast().as_slice_unchecked(entities.len()), ContiguousComponentTicks::<'w, false>::new( - added_ticks.add_unchecked(offset), - changed_ticks.add_unchecked(offset), - callers.map(|callers| callers.add_unchecked(offset)), - entities.len() - offset, + added_ticks, + changed_ticks, + callers, + entities.len(), fetch.last_run, fetch.this_run, ), @@ -2328,7 +2323,6 @@ unsafe impl> ContiguousQueryData for &mut T { _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's> { fetch.components.extract( |table| { @@ -2337,12 +2331,12 @@ unsafe impl> ContiguousQueryData for &mut T { unsafe { table.debug_checked_unwrap() }; ( - &mut table_components.as_mut_slice_unchecked(entities.len())[offset..], + table_components.as_mut_slice_unchecked(entities.len()), ContiguousComponentTicks::<'w, true>::new( - added_ticks.add_unchecked(offset), - changed_ticks.add_unchecked(offset), - callers.map(|callers| callers.add_unchecked(offset)), - entities.len() - offset, + added_ticks, + changed_ticks, + callers, + entities.len(), fetch.last_run, fetch.this_run, ), @@ -2485,9 +2479,8 @@ unsafe impl<'__w, T: Component> ContiguousQueryData for Mu state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's> { - <&mut T as ContiguousQueryData>::fetch_contiguous(state, fetch, entities, offset) + <&mut T as ContiguousQueryData>::fetch_contiguous(state, fetch, entities) } } @@ -2656,12 +2649,11 @@ unsafe impl ContiguousQueryData for Option { state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's> { fetch .matches // SAFETY: The invariants are upheld by the caller - .then(|| unsafe { T::fetch_contiguous(state, &mut fetch.fetch, entities, offset) }) + .then(|| unsafe { T::fetch_contiguous(state, &mut fetch.fetch, entities) }) } } @@ -2851,7 +2843,6 @@ unsafe impl ContiguousQueryData for Has { _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, _entities: &'w [Entity], - _offset: usize, ) -> Self::Contiguous<'w, 's> { *fetch } @@ -2973,12 +2964,11 @@ macro_rules! impl_tuple_query_data { state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's> { let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - ($(unsafe {$name::fetch_contiguous($state, $name, entities, offset)},)*) + ($(unsafe {$name::fetch_contiguous($state, $name, entities)},)*) } } }; @@ -3209,14 +3199,13 @@ macro_rules! impl_anytuple_fetch { state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], - offset: usize, ) -> Self::Contiguous<'w, 's> { let ($($name,)*) = fetch; let ($($state,)*) = state; // Matches the [`QueryData::fetch`] except it always returns Some ($( // SAFETY: The invariants are upheld by the caller - $name.1.then(|| unsafe { $name::fetch_contiguous($state, &mut $name.0, entities, offset) }), + $name.1.then(|| unsafe { $name::fetch_contiguous($state, &mut $name.0, entities) }), )*) } } @@ -3759,16 +3748,14 @@ mod tests { world.spawn(C(2)); let mut query = world.query::<(&C, &D)>(); - let mut iter = query.iter(&world); - let mut iter = iter.as_contiguous_iter().unwrap(); + let mut iter = query.contiguous_iter(&world).unwrap(); let c = iter.next().unwrap(); assert_eq!(c.0, [C(0), C(1)].as_slice()); assert_eq!(c.1, [D(true), D(false)].as_slice()); assert!(iter.next().is_none()); let mut query = world.query::<&C>(); - let mut iter = query.iter(&world); - let mut iter = iter.as_contiguous_iter().unwrap(); + let mut iter = query.contiguous_iter(&world).unwrap(); let mut present = [false; 3]; let mut len = 0; for _ in 0..2 { @@ -3783,8 +3770,7 @@ mod tests { assert_eq!(present, [true; 3]); let mut query = world.query::<&mut C>(); - let mut iter = query.iter_mut(&mut world); - let mut iter = iter.as_contiguous_iter().unwrap(); + let mut iter = query.contiguous_iter_mut(&mut world).unwrap(); for _ in 0..2 { let mut c = iter.next().unwrap(); for c in c.0 { @@ -3793,8 +3779,7 @@ mod tests { c.1.mark_all_as_updated(); } assert!(iter.next().is_none()); - let mut iter = query.iter(&world); - let mut iter = iter.as_contiguous_iter().unwrap(); + let mut iter = query.contiguous_iter(&world).unwrap(); let mut present = [false; 6]; let mut len = 0; for _ in 0..2 { @@ -3808,8 +3793,7 @@ mod tests { assert_eq!(len, 3); let mut query = world.query_filtered::<&C, Without>(); - let mut iter = query.iter(&world); - let mut iter = iter.as_contiguous_iter().unwrap(); + let mut iter = query.contiguous_iter(&world).unwrap(); assert_eq!(iter.next().unwrap(), &[C(4)]); assert!(iter.next().is_none()); } @@ -3824,9 +3808,8 @@ mod tests { world.spawn(S(0)); let mut query = world.query::<&mut S>(); - let mut iter = query.iter_mut(&mut world); - assert!(iter.as_contiguous_iter().is_none()); - assert_eq!(iter.next().unwrap().as_ref(), &S(0)); + let iter = query.contiguous_iter_mut(&mut world); + assert!(iter.is_none()); } #[test] @@ -3844,10 +3827,10 @@ mod tests { world.spawn(()); let mut query = world.query::>(); - let mut iter = query.iter(&world); + let iter = query.contiguous_iter(&world).unwrap(); let mut present = [false; 4]; - for (c, d) in iter.as_contiguous_iter().unwrap() { + for (c, d) in iter { assert!(c.is_some() || d.is_some()); let c = c.unwrap_or(&[]); let d = d.unwrap_or(&[]); @@ -3882,10 +3865,10 @@ mod tests { world.spawn(C(3)); let mut query = world.query::<(Option<&C>, &D)>(); - let mut iter = query.iter(&world); + let iter = query.contiguous_iter(&world).unwrap(); let mut present = [false; 3]; - for (c, d) in iter.as_contiguous_iter().unwrap() { + for (c, d) in iter { let c = c.unwrap_or(&[]); for i in 0..d.len() { let c = c.get(i).cloned(); diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index f39c6222a2ba0..cb589b22eee36 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -903,17 +903,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { ) } } - - /// Returns a contiguous iter or [`None`] if contiguous access is not supported - pub fn as_contiguous_iter(&mut self) -> Option> - where - D: ContiguousQueryData, - F: ArchetypeFilter, - { - self.cursor - .is_dense - .then_some(QueryContiguousIter { iter: self }) - } } impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> { @@ -1007,12 +996,39 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter> Clone for QueryIter<'w, 's, D } /// Iterator for contiguous chunks of memory -pub struct QueryContiguousIter<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> { - iter: &'a mut QueryIter<'w, 's, D, F>, +pub struct QueryContiguousIter<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> { + tables: &'w Tables, + storage_id_iter: core::slice::Iter<'s, StorageId>, + query_state: &'s QueryState, + fetch: D::Fetch<'w>, + filter: F::Fetch<'w>, } -impl<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> Iterator - for QueryContiguousIter<'a, 'w, 's, D, F> +impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> QueryContiguousIter<'w, 's, D, F> { + /// # Safety + /// - `world` must have permission to access any of the components registered in `query_state`. + /// - `world` must be the same one used to initialize `query_state`. + pub(crate) unsafe fn new( + world: UnsafeWorldCell<'w>, + query_state: &'s QueryState, + last_run: Tick, + this_run: Tick, + ) -> Option { + query_state.is_dense.then(|| Self { + // SAFETY: We only access table data that has been registered in `query_state` + tables: unsafe { &world.storages().tables }, + storage_id_iter: query_state.matched_storage_ids.iter(), + // SAFETY: The invariants are upheld by the caller. + fetch: unsafe { D::init_fetch(world, &query_state.fetch_state, last_run, this_run) }, + // SAFETY: The invariants are upheld by the caller. + filter: unsafe { F::init_fetch(world, &query_state.filter_state, last_run, this_run) }, + query_state, + }) + } +} + +impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> Iterator + for QueryContiguousIter<'w, 's, D, F> { type Item = D::Contiguous<'w, 's>; @@ -1021,27 +1037,45 @@ impl<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> Iterator // SAFETY: // `tables` belongs to the same world that the cursor was initialized for. // `query_state` is the state that was passed to `QueryIterationCursor::init` + // SAFETY: Refer to [`Self::next`] unsafe { - self.iter - .cursor - .next_contiguous(self.iter.tables, self.iter.query_state) + loop { + let table_id = self.storage_id_iter.next()?.table_id; + let table = self.tables.get(table_id).debug_checked_unwrap(); + if table.is_empty() { + continue; + } + D::set_table(&mut self.fetch, &self.query_state.fetch_state, table); + F::set_table(&mut self.filter, &self.query_state.filter_state, table); + + // no filtering because `F` implements `ArchetypeFilter` which ensures that `QueryFilter::fetch` + // always returns true + + let item = D::fetch_contiguous( + &self.query_state.fetch_state, + &mut self.fetch, + table.entities(), + ); + + return Some(item); + } } } fn size_hint(&self) -> (usize, Option) { - self.iter.cursor.storage_id_iter.size_hint() + self.storage_id_iter.size_hint() } } // [`QueryIterationCursor::next_contiguous`] always returns None when exhausted -impl<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> FusedIterator - for QueryContiguousIter<'a, 'w, 's, D, F> +impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> FusedIterator + for QueryContiguousIter<'w, 's, D, F> { } -// self.iter.cursor.storage_id_iter is a slice's iterator hence has an exact size -impl<'a, 'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> ExactSizeIterator - for QueryContiguousIter<'a, 'w, 's, D, F> +// self.storage_id_iter is a slice's iterator hence has an exact size +impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> ExactSizeIterator + for QueryContiguousIter<'w, 's, D, F> { } @@ -2583,54 +2617,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { remaining_matched + self.current_len - self.current_row } - /// Returns the next contiguous chunk of memory or [`None`] if it is impossible or there is none - /// - /// # Safety - /// - `tables` must belong to the same world that the [`QueryIterationCursor`] was initialized for. - /// - `query_state` must be the same [`QueryState`] that was passed to `init` or `init_empty`. - /// - Query must be dense - #[inline(always)] - unsafe fn next_contiguous( - &mut self, - tables: &'w Tables, - query_state: &'s QueryState, - ) -> Option> - where - D: ContiguousQueryData, - F: ArchetypeFilter, - { - // SAFETY: Refer to [`Self::next`] - loop { - if self.current_row == self.current_len { - let table_id = self.storage_id_iter.next()?.table_id; - let table = tables.get(table_id).debug_checked_unwrap(); - if table.is_empty() { - continue; - } - D::set_table(&mut self.fetch, &query_state.fetch_state, table); - F::set_table(&mut self.filter, &query_state.filter_state, table); - self.table_entities = table.entities(); - self.current_len = table.entity_count(); - self.current_row = 0; - } - - let offset = self.current_row as usize; - self.current_row = self.current_len; - - // no filtering because `F` implements `ArchetypeFilter` which ensures that `QueryFilter::fetch` - // always returns true - - let item = D::fetch_contiguous( - &query_state.fetch_state, - &mut self.fetch, - self.table_entities, - offset, - ); - - return Some(item); - } - } - // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QuerySortedIter, QueryManyIter, QuerySortedManyIter, QueryCombinationIter, // QueryState::par_fold_init_unchecked_manual, QueryState::par_many_fold_init_unchecked_manual, diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 79ecf09b20176..29257ce26ff07 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -5,7 +5,10 @@ use crate::{ entity::{Entity, EntityEquivalent, EntitySet, UniqueEntityArray}, entity_disabling::DefaultQueryFilters, prelude::FromWorld, - query::{FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter, WorldQuery}, + query::{ + ArchetypeFilter, ContiguousQueryData, FilteredAccess, QueryCombinationIter, + QueryContiguousIter, QueryIter, QueryParIter, WorldQuery, + }, storage::{SparseSetIndex, TableId}, system::Query, world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId}, @@ -1401,6 +1404,36 @@ impl QueryState { self.query_mut(world).par_iter_inner() } + /// Returns a contiguous iterator over the query results for the given [`World`] or [`None`] if + /// the query is not dense hence not contiguously iterable. + #[inline] + pub fn contiguous_iter<'w, 's>( + &'s mut self, + world: &'w World, + ) -> Option> + where + D::ReadOnly: ContiguousQueryData, + F: ArchetypeFilter, + { + self.query(world).contiguous_iter_inner().ok() + } + + /// Returns a contiguous iterator over the query results for the given [`World`] or [`None`] if + /// the query is not dense hence not contiguously iterable. + /// + /// This can only be called for mutable queries, see [`contiguous_iter`] for read-only-queries. + #[inline] + pub fn contiguous_iter_mut<'w, 's>( + &'s mut self, + world: &'w mut World, + ) -> Option> + where + D: ContiguousQueryData, + F: ArchetypeFilter, + { + self.query_mut(world).contiguous_iter_inner().ok() + } + /// Runs `func` on each query result in parallel for the given [`World`], where the last change and /// the current change tick are given. This is faster than the equivalent /// `iter()` method, but cannot be chained like a normal [`Iterator`]. diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index b8c29d448adbf..d088ebe03a180 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -5,8 +5,9 @@ use crate::{ change_detection::Tick, entity::{Entity, EntityEquivalent, EntitySet, UniqueEntityArray}, query::{ - DebugCheckedUnwrap, NopWorldQuery, QueryCombinationIter, QueryData, QueryEntityError, - QueryFilter, QueryIter, QueryManyIter, QueryManyUniqueIter, QueryParIter, QueryParManyIter, + ArchetypeFilter, ContiguousQueryData, DebugCheckedUnwrap, NopWorldQuery, + QueryCombinationIter, QueryContiguousIter, QueryData, QueryEntityError, QueryFilter, + QueryIter, QueryManyIter, QueryManyUniqueIter, QueryParIter, QueryParManyIter, QueryParManyUniqueIter, QuerySingleError, QueryState, ROQueryItem, ReadOnlyQueryData, }, world::unsafe_world_cell::UnsafeWorldCell, @@ -1354,6 +1355,61 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { } } + /// Returns a contiguous iterator over the query results for the given + /// [`World`](crate::world::World) or [`None`] if the query is not dense hence not contiguously + /// iterable. + /// + /// A mutable version: [`contiguous_iter_mut`] + pub fn contiguous_iter(&self) -> Option> + where + D::ReadOnly: ContiguousQueryData, + F: ArchetypeFilter, + { + // SAFETY: + // - `self.world` has permission to access the required components + // - `self.world` was used to initialize `self.state` + unsafe { + QueryContiguousIter::new( + self.world, + self.state.as_readonly(), + self.last_run, + self.this_run, + ) + } + } + + /// Returns a mutable contiguous iterator over the query results for the given + /// [`World`](crate::world::World) or [`None`] if the query is not dense hence not contiguously + /// iterable. + /// + /// An immutable version: [`contiguous_iter`] + pub fn contiguous_iter_mut(&mut self) -> Option> + where + D: ContiguousQueryData, + F: ArchetypeFilter, + { + // SAFETY: + // - `self.world` has permission to access the required components + // - `self.world` was used to initialize `self.state` + unsafe { QueryContiguousIter::new(self.world, self.state, self.last_run, self.this_run) } + } + + /// Returns a contiguous iterator over the query results for the given + /// [`World`](crate::world::World) or [`Err`] with this [`Query`] if the query is not dense hence not contiguously + /// iterable. + /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. + pub fn contiguous_iter_inner(self) -> Result, Self> + where + D: ContiguousQueryData, + F: ArchetypeFilter, + { + // SAFETY: + // - `self.world` has permission to access the required components + // - `self.world` was used to initialize `self.state` + unsafe { QueryContiguousIter::new(self.world, self.state, self.last_run, self.this_run) } + .ok_or(self) + } + /// Returns the read-only query item for the given [`Entity`]. /// /// In case of a nonexisting entity or mismatched component, a [`QueryEntityError`] is returned instead. diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index a435967dd36fb..041b6eeed7af5 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -13,7 +13,7 @@ pub struct HealthDecay(pub f32); fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { // as_contiguous_iter() would return None if query couldn't be iterated contiguously - for ((health, _health_ticks), decay) in query.iter_mut().as_contiguous_iter().unwrap() { + for ((health, _health_ticks), decay) in query.contiguous_iter_mut().unwrap() { // all slices returned by component queries are the same size assert!(health.len() == decay.len()); for i in 0..health.len() { diff --git a/examples/ecs/custom_query_param.rs b/examples/ecs/custom_query_param.rs index 28025915bd9a5..be322d2c3ec40 100644 --- a/examples/ecs/custom_query_param.rs +++ b/examples/ecs/custom_query_param.rs @@ -193,6 +193,7 @@ fn print_components_tuple( println!("Nested: {:?} {:?}", nested.0, nested.1); println!("Generic: {generic_c:?} {generic_d:?}"); } + println!(); } /// If you are going to contiguously iterate the data in a query, you must mark it with the `contiguous` attribute, @@ -213,7 +214,7 @@ struct CustomContiguousQuery { fn print_components_contiguous_iter(query: Query>) { println!("Print components (contiguous_iter):"); - for e in query.iter().as_contiguous_iter().unwrap() { + for e in query.contiguous_iter().unwrap() { let e: CustomContiguousQueryContiguousItem<'_, '_, _, _> = e; for i in 0..e.entity.len() { println!("Entity: {:?}", e.entity[i]); From 2c6c2518fab32aebc2566ef6a9491ec065ccefd7 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:50:29 +0100 Subject: [PATCH 26/52] ci fix --- crates/bevy_ecs/src/query/fetch.rs | 5 ++--- crates/bevy_ecs/src/query/state.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 8522309c25371..ffbcf75569d15 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -553,7 +553,7 @@ unsafe impl ContiguousQueryData for Entity { _fetch: &mut Self::Fetch<'w>, entities: &'w [Entity], ) -> Self::Contiguous<'w, 's> { - &entities + entities } } @@ -1821,8 +1821,7 @@ unsafe impl ContiguousQueryData for &T { // (i.e. repr(transparent)) of UnsafeCell let table = table.cast(); // SAFETY: Caller ensures `rows` is the amount of rows in the table - let item = unsafe { table.as_slice_unchecked(entities.len()) }; - item + unsafe { table.as_slice_unchecked(entities.len()) } }, |_| { #[cfg(debug_assertions)] diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 29257ce26ff07..c4043de5756d9 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1421,7 +1421,7 @@ impl QueryState { /// Returns a contiguous iterator over the query results for the given [`World`] or [`None`] if /// the query is not dense hence not contiguously iterable. /// - /// This can only be called for mutable queries, see [`contiguous_iter`] for read-only-queries. + /// This can only be called for mutable queries, see [`Self::contiguous_iter`] for read-only-queries. #[inline] pub fn contiguous_iter_mut<'w, 's>( &'s mut self, From 9599bfc9656b38b076299217fc696a4ed603ad33 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:20:34 +0100 Subject: [PATCH 27/52] Minor fixes --- .../bevy_ecs/src/change_detection/params.rs | 12 ++-- crates/bevy_ecs/src/query/fetch.rs | 2 + crates/bevy_ecs/src/query/iter.rs | 57 +++++++++++-------- crates/bevy_ecs/src/query/state.rs | 6 +- crates/bevy_ecs/src/system/query.rs | 21 ++----- crates/bevy_ptr/src/lib.rs | 22 ------- 6 files changed, 47 insertions(+), 73 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 9f832fd29fad4..7cd5d26a37098 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -487,12 +487,12 @@ impl<'w> ContiguousComponentTicks<'w, true> { pub fn mark_all_as_updated(&mut self) { let this_run = self.this_run; - for i in 0..self.count { - // SAFETY: `changed_by` slice is `self.count` long, aliasing rules are uphold by `new` - self.changed_by - .map(|v| unsafe { v.get_unchecked(i).deref_mut() }) - .assign(MaybeLocation::caller()); - } + self.changed_by.map(|v| { + for i in 0..self.count { + // SAFETY: `changed_by` slice is `self.count` long, aliasing rules are uphold by `new` + *unsafe { v.get_unchecked(i).deref_mut() } = Location::caller(); + } + }); for t in self.get_changed_ticks_mut() { *t = this_run; diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index ffbcf75569d15..713000d0c6717 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2091,6 +2091,7 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { |_| { #[cfg(debug_assertions)] unreachable!(); + // SAFETY: the caller ensures that [`Self::set_table`] was called beforehand. #[cfg(not(debug_assertions))] core::hint::unreachable_unchecked(); }, @@ -2344,6 +2345,7 @@ unsafe impl> ContiguousQueryData for &mut T { |_| { #[cfg(debug_assertions)] unreachable!(); + // SAFETY: the caller ensures that [`Self::set_table`] was called beforehand. #[cfg(not(debug_assertions))] core::hint::unreachable_unchecked(); }, diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index cb589b22eee36..68b05112cf9a8 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -1001,7 +1001,7 @@ pub struct QueryContiguousIter<'w, 's, D: ContiguousQueryData, F: ArchetypeFilte storage_id_iter: core::slice::Iter<'s, StorageId>, query_state: &'s QueryState, fetch: D::Fetch<'w>, - filter: F::Fetch<'w>, + // NOTE: no need for F::Fetch because it always returns true } impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> QueryContiguousIter<'w, 's, D, F> { @@ -1020,8 +1020,6 @@ impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> QueryContiguousIter<'w, storage_id_iter: query_state.matched_storage_ids.iter(), // SAFETY: The invariants are upheld by the caller. fetch: unsafe { D::init_fetch(world, &query_state.fetch_state, last_run, this_run) }, - // SAFETY: The invariants are upheld by the caller. - filter: unsafe { F::init_fetch(world, &query_state.filter_state, last_run, this_run) }, query_state, }) } @@ -1034,36 +1032,45 @@ impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> Iterator #[inline(always)] fn next(&mut self) -> Option { - // SAFETY: - // `tables` belongs to the same world that the cursor was initialized for. - // `query_state` is the state that was passed to `QueryIterationCursor::init` - // SAFETY: Refer to [`Self::next`] - unsafe { - loop { - let table_id = self.storage_id_iter.next()?.table_id; - let table = self.tables.get(table_id).debug_checked_unwrap(); - if table.is_empty() { - continue; - } + loop { + // SAFETY: Query is dense + let table_id = unsafe { self.storage_id_iter.next()?.table_id }; + // SAFETY: `table_id` was returned by `self.storage_id_iter` which always returns a + // valid id + let table = unsafe { self.tables.get(table_id).debug_checked_unwrap() }; + if table.is_empty() { + continue; + } + // SAFETY: + // - `table` is from the same world as `self.query_state` (`self.storage_id_iter` is + // from the same world as `self.query_state`, see [`Self::new`]) + // - `self.fetch` was initialized with `self.query_state` (in [`Self::new`]) + unsafe { D::set_table(&mut self.fetch, &self.query_state.fetch_state, table); - F::set_table(&mut self.filter, &self.query_state.filter_state, table); + } - // no filtering because `F` implements `ArchetypeFilter` which ensures that `QueryFilter::fetch` - // always returns true + // no filtering because `F` implements `ArchetypeFilter` which ensures that `QueryFilter::fetch` + // always returns true - let item = D::fetch_contiguous( + // SAFETY: + // - [`D::set_table`] is executed prior. + // - `table.entities()` return a valid entity array + // - the caller of [`Self::new`] ensures that the world has permission to access any of + // the components registered in `self.query_state` + let item = unsafe { + D::fetch_contiguous( &self.query_state.fetch_state, &mut self.fetch, table.entities(), - ); + ) + }; - return Some(item); - } + return Some(item); } } fn size_hint(&self) -> (usize, Option) { - self.storage_id_iter.size_hint() + (0, self.storage_id_iter.size_hint().1) } } @@ -2620,7 +2627,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QuerySortedIter, QueryManyIter, QuerySortedManyIter, QueryCombinationIter, // QueryState::par_fold_init_unchecked_manual, QueryState::par_many_fold_init_unchecked_manual, - // QueryState::par_many_unique_fold_init_unchecked_manual + // QueryState::par_many_unique_fold_init_unchecked_manual, QueryContiguousIter::next /// # Safety /// `tables` and `archetypes` must belong to the same world that the [`QueryIterationCursor`] /// was initialized for. @@ -2633,8 +2640,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { query_state: &'s QueryState, ) -> Option> { if self.is_dense { - // NOTE: If you are changing this branch's code (the self.is_dense branch), - // don't forget to update [`Self::next_contiguous`] + // NOTE: if you are changing this branch you would probably have to change + // QueryContiguousIter::next as well loop { // we are on the beginning of the query, or finished processing a table, so skip to the next if self.current_row == self.current_len { diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index c4043de5756d9..2147a797fafe2 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1465,7 +1465,7 @@ impl QueryState { { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QueryManyIter, QueryCombinationIter,QueryState::par_fold_init_unchecked_manual, - // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual + // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual, QueryContiguousIter::next use arrayvec::ArrayVec; bevy_tasks::ComputeTaskPool::get().scope(|scope| { @@ -1581,7 +1581,7 @@ impl QueryState { { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QueryManyIter, QueryCombinationIter,QueryState::par_fold_init_unchecked_manual - // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual + // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual, QueryContiguousIter::next bevy_tasks::ComputeTaskPool::get().scope(|scope| { let chunks = entity_list.chunks_exact(batch_size as usize); @@ -1644,7 +1644,7 @@ impl QueryState { { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QueryManyIter, QueryCombinationIter, QueryState::par_fold_init_unchecked_manual - // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual + // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual, QueryContiguousIter::next bevy_tasks::ComputeTaskPool::get().scope(|scope| { let chunks = entity_list.chunks_exact(batch_size as usize); diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index d088ebe03a180..4184fde813298 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1359,39 +1359,26 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`World`](crate::world::World) or [`None`] if the query is not dense hence not contiguously /// iterable. /// - /// A mutable version: [`contiguous_iter_mut`] + /// A mutable version: [`Self::contiguous_iter_mut`] pub fn contiguous_iter(&self) -> Option> where D::ReadOnly: ContiguousQueryData, F: ArchetypeFilter, { - // SAFETY: - // - `self.world` has permission to access the required components - // - `self.world` was used to initialize `self.state` - unsafe { - QueryContiguousIter::new( - self.world, - self.state.as_readonly(), - self.last_run, - self.this_run, - ) - } + self.as_readonly().contiguous_iter_inner().ok() } /// Returns a mutable contiguous iterator over the query results for the given /// [`World`](crate::world::World) or [`None`] if the query is not dense hence not contiguously /// iterable. /// - /// An immutable version: [`contiguous_iter`] + /// An immutable version: [`Self::contiguous_iter`] pub fn contiguous_iter_mut(&mut self) -> Option> where D: ContiguousQueryData, F: ArchetypeFilter, { - // SAFETY: - // - `self.world` has permission to access the required components - // - `self.world` was used to initialize `self.state` - unsafe { QueryContiguousIter::new(self.world, self.state, self.last_run, self.this_run) } + self.reborrow().contiguous_iter_inner().ok() } /// Returns a contiguous iterator over the query results for the given diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 62dd1a2170f19..1d20535d914f7 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -1122,28 +1122,6 @@ impl<'a, T> ThinSlicePtr<'a, T> { unsafe { core::slice::from_raw_parts(self.ptr.as_ptr(), len) } } - /// Offsets the slice beginning by `count` elements - /// - /// # Safety - /// - /// - `count` must be less than or equal to the length of the slice - // The result pointer must lie within the same allocation - pub unsafe fn add_unchecked(&self, count: usize) -> ThinSlicePtr<'a, T> { - #[cfg(debug_assertions)] - assert!( - count <= self.len, - "tried to offset the slice by more than the length" - ); - - Self { - // SAFETY: The caller guarantees that count is in-bounds. - ptr: unsafe { self.ptr.add(count) }, - #[cfg(debug_assertions)] - len: self.len - count, - _marker: PhantomData, - } - } - /// Indexes the slice without performing bounds checks. /// /// # Safety From 6ade0c98fd815d7407106450529b5fce9706ddd0 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:22:45 +0100 Subject: [PATCH 28/52] No exactsize --- crates/bevy_ecs/src/query/iter.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 68b05112cf9a8..6bb98a8671f2b 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -1080,12 +1080,6 @@ impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> FusedIterator { } -// self.storage_id_iter is a slice's iterator hence has an exact size -impl<'w, 's, D: ContiguousQueryData, F: ArchetypeFilter> ExactSizeIterator - for QueryContiguousIter<'w, 's, D, F> -{ -} - /// An [`Iterator`] over sorted query results of a [`Query`](crate::system::Query). /// /// This struct is created by the [`QueryIter::sort`], [`QueryIter::sort_unstable`], From c9c293d51e4dc5cddabf58c964c1bf5d419d6a59 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:17:12 +0100 Subject: [PATCH 29/52] Contiguous[Ref/Mut] --- .../iteration/iter_simple_contiguous.rs | 7 +- .../iteration/iter_simple_contiguous_avx2.rs | 7 +- .../iter_simple_no_detection_contiguous.rs | 5 +- .../src/change_detection/maybe_location.rs | 7 + .../bevy_ecs/src/change_detection/params.rs | 212 +++++++++++++----- crates/bevy_ecs/src/query/fetch.rs | 77 ++++--- examples/ecs/contiguous_query.rs | 12 +- examples/ecs/custom_query_param.rs | 2 +- 8 files changed, 219 insertions(+), 110 deletions(-) diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs index 9621a4d79a00a..ae03ac81b1a35 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs @@ -36,12 +36,13 @@ impl<'w> Benchmark<'w> { #[inline(never)] pub fn run(&mut self) { let iter = self.1.contiguous_iter_mut(&mut self.0).unwrap(); - for (velocity, (position, mut ticks)) in iter { - for (v, p) in velocity.iter().zip(position.iter_mut()) { + for (velocity, mut position) in iter { + assert!(velocity.len() == position.data_slice().len()); + for (v, p) in velocity.iter().zip(position.data_slice_mut().iter_mut()) { p.0 += v.0; } // to match the iter_simple benchmark - ticks.mark_all_as_updated(); + position.mark_all_as_updated(); } } } diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs index bcb6a750bb2f9..dc6ea09ba7a12 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs @@ -47,19 +47,20 @@ impl<'w> Benchmark<'w> { /// avx2 must be supported #[target_feature(enable = "avx2")] unsafe fn exec(position: &mut [Position], velocity: &[Velocity]) { + assert!(position.len() == velocity.len()); for i in 0..position.len() { position[i].0 += velocity[i].0; } } let iter = self.1.contiguous_iter_mut(&mut self.0).unwrap(); - for (velocity, (position, mut ticks)) in iter { + for (velocity, mut position) in iter { // SAFETY: checked in new unsafe { - exec(position, velocity); + exec(position.data_slice_mut(), velocity); } // to match the iter_simple benchmark - ticks.mark_all_as_updated(); + position.mark_all_as_updated(); } } } diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs index 4df810f3fc8e0..19722654462c5 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs @@ -36,8 +36,9 @@ impl<'w> Benchmark<'w> { #[inline(never)] pub fn run(&mut self) { let iter = self.1.contiguous_iter_mut(&mut self.0).unwrap(); - for (velocity, (position, _ticks)) in iter { - for (v, p) in velocity.iter().zip(position.iter_mut()) { + for (velocity, mut position) in iter { + assert!(velocity.len() == position.data_slice().len()); + for (v, p) in velocity.iter().zip(position.data_slice_mut().iter_mut()) { p.0 += v.0; } } diff --git a/crates/bevy_ecs/src/change_detection/maybe_location.rs b/crates/bevy_ecs/src/change_detection/maybe_location.rs index 45272d090d487..e66b0b1c66d46 100644 --- a/crates/bevy_ecs/src/change_detection/maybe_location.rs +++ b/crates/bevy_ecs/src/change_detection/maybe_location.rs @@ -83,6 +83,13 @@ impl MaybeLocation { } } + /// Mutates the value of `MaybeLocation` + #[inline] + pub fn mutate(&mut self, _f: impl FnOnce(&mut T)) { + #[cfg(feature = "track_location")] + _f(self.value) + } + /// Converts a pair of `MaybeLocation` values to an `MaybeLocation` of a tuple. #[inline] pub fn zip(self, _other: MaybeLocation) -> MaybeLocation<(T, U)> { diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 7cd5d26a37098..f450ef3beeb2d 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -43,6 +43,34 @@ impl<'w> ComponentTicksRef<'w> { } } +#[derive(Clone)] +pub(crate) struct ContiguousComponentTicksRef<'w> { + pub(crate) added: &'w [Tick], + pub(crate) changed: &'w [Tick], + pub(crate) changed_by: MaybeLocation<&'w [&'static Location<'static>]>, + pub(crate) last_run: Tick, + pub(crate) this_run: Tick, +} + +impl<'w> ContiguousComponentTicksRef<'w> { + pub(crate) unsafe fn from_slice_ptrs( + added: ThinSlicePtr<'w, UnsafeCell>, + changed: ThinSlicePtr<'w, UnsafeCell>, + changed_by: MaybeLocation>>>, + len: usize, + this_run: Tick, + last_run: Tick, + ) -> Self { + Self { + added: unsafe { added.cast().as_slice_unchecked(len) }, + changed: unsafe { changed.cast().as_slice_unchecked(len) }, + changed_by: changed_by.map(|v| unsafe { v.cast().as_slice_unchecked(len) }), + last_run, + this_run, + } + } +} + /// Used by mutable query parameters (such as [`Mut`] and [`ResMut`]) /// to store mutable access to the [`Tick`]s of a single component or resource. pub(crate) struct ComponentTicksMut<'w> { @@ -87,6 +115,47 @@ impl<'w> From> for ComponentTicksRef<'w> { } } +pub(crate) struct ContiguousComponentTicksMut<'w> { + pub(crate) added: &'w mut [Tick], + pub(crate) changed: &'w mut [Tick], + pub(crate) changed_by: MaybeLocation<&'w mut [&'static Location<'static>]>, + pub(crate) last_run: Tick, + pub(crate) this_run: Tick, +} + +impl<'w> ContiguousComponentTicksMut<'w> { + pub(crate) unsafe fn from_slice_ptrs( + added: ThinSlicePtr<'w, UnsafeCell>, + changed: ThinSlicePtr<'w, UnsafeCell>, + changed_by: MaybeLocation>>>, + len: usize, + this_run: Tick, + last_run: Tick, + ) -> Self { + Self { + added: unsafe { added.as_mut_slice_unchecked(len) }, + changed: unsafe { changed.as_mut_slice_unchecked(len) }, + changed_by: changed_by.map(|v| unsafe { v.as_mut_slice_unchecked(len) }), + last_run, + this_run, + } + } + + pub fn mark_all_as_updated(&mut self) { + let this_run = self.this_run; + + self.changed_by.mutate(|v| { + for v in v.iter_mut() { + *v = Location::caller(); + } + }); + + for t in self.changed.iter_mut() { + *t = this_run; + } + } +} + /// Shared borrow of a [`Resource`]. /// /// See the [`Resource`] documentation for usage. @@ -363,6 +432,44 @@ impl<'w, T: ?Sized> Ref<'w, T> { } } +/// Data type returned by [`ContiguousQueryData::fetch_contiguous`](crate::query::ContiguousQueryData::fetch_contiguous) for [`Ref`]. +pub struct ContiguousRef<'w, T> { + pub(crate) value: &'w [T], + pub(crate) ticks: ContiguousComponentTicksRef<'w>, +} + +impl<'w, T> ContiguousRef<'w, T> { + /// Returns the data slice. + #[inline] + pub fn data_slice(&self) -> &[T] { + self.value + } + + /// Returns the added ticks. + #[inline] + pub fn added_ticks_slice(&self) -> &[Tick] { + self.ticks.added + } + + /// Returns the changed ticks. + #[inline] + pub fn changed_ticks_slice(&self) -> &[Tick] { + self.ticks.changed + } + + /// Returns the tick when the system last ran. + #[inline] + pub fn last_run_tick(&self) -> Tick { + self.ticks.last_run + } + + /// Returns the tick of the system's current run. + #[inline] + pub fn this_run_tick(&self) -> Tick { + self.ticks.this_run + } +} + impl<'w, 'a, T> IntoIterator for &'a Ref<'w, T> where &'a T: IntoIterator, @@ -464,81 +571,68 @@ impl<'w, T: ?Sized> Mut<'w, T> { } } -/// Used by [`Mut`] for [`crate::query::ContiguousQueryData`] to allow marking component's changes -pub struct ContiguousComponentTicks<'w, const MUTABLE: bool> { - added: ThinSlicePtr<'w, UnsafeCell>, - changed: ThinSlicePtr<'w, UnsafeCell>, - changed_by: MaybeLocation>>>, - count: usize, - last_run: Tick, - this_run: Tick, +/// Data type returned by [`ContiguousQueryData::fetch_contiguous`](crate::query::ContiguousQueryData::fetch_contiguous) +/// for [`Mut`] and `&mut T` +pub struct ContiguousMut<'w, T> { + pub(crate) value: &'w mut [T], + pub(crate) ticks: ContiguousComponentTicksMut<'w>, } -impl<'w> ContiguousComponentTicks<'w, true> { - /// Returns mutable changed ticks slice - pub fn get_changed_ticks_mut(&mut self) -> &mut [Tick] { - // SAFETY: `changed` slice is `self.count` long, aliasing rules are uphold by `new`. - unsafe { self.changed.as_mut_slice_unchecked(self.count) } +impl<'w, T> ContiguousMut<'w, T> { + /// Returns the mutable data slice. + #[inline] + pub fn data_slice_mut(&mut self) -> &mut [T] { + self.value } - /// Marks all components as updated - /// - /// **Executes in O(n), where n is the amount of rows.** - pub fn mark_all_as_updated(&mut self) { - let this_run = self.this_run; + /// Returns the immutable data slice. + #[inline] + pub fn data_slice(&self) -> &[T] { + self.value + } - self.changed_by.map(|v| { - for i in 0..self.count { - // SAFETY: `changed_by` slice is `self.count` long, aliasing rules are uphold by `new` - *unsafe { v.get_unchecked(i).deref_mut() } = Location::caller(); - } - }); + /// Returns the immutable added ticks' slice. + #[inline] + pub fn added_ticks_slice(&self) -> &[Tick] { + self.ticks.added + } - for t in self.get_changed_ticks_mut() { - *t = this_run; - } + /// Returns the immutable changed ticks' slice. + #[inline] + pub fn changed_ticks_slice(&self) -> &[Tick] { + self.ticks.changed } -} -impl<'w, const MUTABLE: bool> ContiguousComponentTicks<'w, MUTABLE> { - pub(crate) unsafe fn new( - added: ThinSlicePtr<'w, UnsafeCell>, - changed: ThinSlicePtr<'w, UnsafeCell>, - changed_by: MaybeLocation>>>, - count: usize, - last_run: Tick, - this_run: Tick, - ) -> Self { - Self { - added, - changed, - count, - changed_by, - last_run, - this_run, - } + /// Returns the tick when the system last ran. + #[inline] + pub fn last_run_tick(&self) -> Tick { + self.ticks.last_run } - /// Returns immutable changed ticks slice - pub fn get_changed_ticks(&self) -> &[Tick] { - // SAFETY: `self.changed` is `self.count` long - unsafe { self.changed.cast().as_slice_unchecked(self.count) } + /// Returns the tick of the system's current run. + #[inline] + pub fn this_run_tick(&self) -> Tick { + self.ticks.this_run } - /// Returns immutable added ticks slice - pub fn get_added_ticks(&self) -> &[Tick] { - // SAFETY: `self.added` is `self.count` long - unsafe { self.added.cast().as_slice_unchecked(self.count) } + /// Returns the mutable added ticks' slice. + #[inline] + pub fn added_ticks_slice_mut(&mut self) -> &mut [Tick] { + self.ticks.added } - /// Returns the last tick system ran - pub fn last_run(&self) -> Tick { - self.last_run + /// Returns the mutable changed ticks' slice. + #[inline] + pub fn changed_ticks_slice_mut(&mut self) -> &mut [Tick] { + self.ticks.changed } - /// Returns the current tick - pub fn this_run(&self) -> Tick { - self.this_run + /// Marks all components as updated. + /// + /// **Runs in O(n), where n is the amount of rows** + #[inline] + pub fn mark_all_as_updated(&mut self) { + self.ticks.mark_all_as_updated(); } } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 713000d0c6717..facb1afea978c 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2,7 +2,8 @@ use crate::{ archetype::{Archetype, Archetypes}, bundle::Bundle, change_detection::{ - ComponentTicksMut, ComponentTicksRef, ContiguousComponentTicks, MaybeLocation, Tick, + ComponentTicksMut, ComponentTicksRef, ContiguousComponentTicksMut, + ContiguousComponentTicksRef, ContiguousMut, ContiguousRef, MaybeLocation, Tick, }, component::{Component, ComponentId, Components, Mutable, StorageType}, entity::{Entities, Entity, EntityLocation}, @@ -1817,15 +1818,15 @@ unsafe impl ContiguousQueryData for &T { |table| { // SAFETY: set_table was previously called let table = unsafe { table.debug_checked_unwrap() }; - // UnsafeCell has the same alignment as T because of transparent representation - // (i.e. repr(transparent)) of UnsafeCell - let table = table.cast(); - // SAFETY: Caller ensures `rows` is the amount of rows in the table - unsafe { table.as_slice_unchecked(entities.len()) } + // SAFETY: + // - `table` is `entities.len()` long + // - `UnsafeCell` has the same layout as `T` + unsafe { table.cast().as_slice_unchecked(entities.len()) } }, |_| { #[cfg(debug_assertions)] unreachable!(); + // SAFETY: query is dense #[cfg(not(debug_assertions))] core::hint::unreachable_unchecked(); }, @@ -2063,7 +2064,7 @@ impl ArchetypeQueryData for Ref<'_, T> {} /// SAFETY: Refer to [`&mut T`]'s implementation unsafe impl ContiguousQueryData for Ref<'_, T> { - type Contiguous<'w, 's> = (&'w [T], ContiguousComponentTicks<'w, false>); + type Contiguous<'w, 's> = ContiguousRef<'w, T>; unsafe fn fetch_contiguous<'w, 's>( _state: &'s Self::State, @@ -2076,17 +2077,19 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { let (table_components, added_ticks, changed_ticks, callers) = unsafe { table.debug_checked_unwrap() }; - ( - table_components.cast().as_slice_unchecked(entities.len()), - ContiguousComponentTicks::<'w, false>::new( - added_ticks, - changed_ticks, - callers, - entities.len(), - fetch.last_run, - fetch.this_run, - ), - ) + ContiguousRef { + value: unsafe { table_components.cast().as_slice_unchecked(entities.len()) }, + ticks: unsafe { + ContiguousComponentTicksRef::from_slice_ptrs( + added_ticks, + changed_ticks, + callers, + entities.len(), + fetch.this_run, + fetch.last_run, + ) + }, + } }, |_| { #[cfg(debug_assertions)] @@ -2317,7 +2320,7 @@ impl> ArchetypeQueryData for &mut T {} /// - The first element of [`ContiguousQueryData::Contiguous`] tuple represents all components' values in the set table. /// - The second element of [`ContiguousQueryData::Contiguous`] tuple represents all components' ticks in the set table. unsafe impl> ContiguousQueryData for &mut T { - type Contiguous<'w, 's> = (&'w mut [T], ContiguousComponentTicks<'w, true>); + type Contiguous<'w, 's> = ContiguousMut<'w, T>; unsafe fn fetch_contiguous<'w, 's>( _state: &'s Self::State, @@ -2330,17 +2333,19 @@ unsafe impl> ContiguousQueryData for &mut T { let (table_components, added_ticks, changed_ticks, callers) = unsafe { table.debug_checked_unwrap() }; - ( - table_components.as_mut_slice_unchecked(entities.len()), - ContiguousComponentTicks::<'w, true>::new( - added_ticks, - changed_ticks, - callers, - entities.len(), - fetch.last_run, - fetch.this_run, - ), - ) + ContiguousMut { + value: unsafe { table_components.as_mut_slice_unchecked(entities.len()) }, + ticks: unsafe { + ContiguousComponentTicksMut::from_slice_ptrs( + added_ticks, + changed_ticks, + callers, + entities.len(), + fetch.this_run, + fetch.last_run, + ) + }, + } }, |_| { #[cfg(debug_assertions)] @@ -2474,7 +2479,7 @@ impl> ArchetypeQueryData for Mut<'_, T> {} /// SAFETY: Refer to soundness of `&mut T` implementation unsafe impl<'__w, T: Component> ContiguousQueryData for Mut<'__w, T> { - type Contiguous<'w, 's> = (&'w mut [T], ContiguousComponentTicks<'w, true>); + type Contiguous<'w, 's> = ContiguousMut<'w, T>; unsafe fn fetch_contiguous<'w, 's>( state: &'s Self::State, @@ -3774,10 +3779,10 @@ mod tests { let mut iter = query.contiguous_iter_mut(&mut world).unwrap(); for _ in 0..2 { let mut c = iter.next().unwrap(); - for c in c.0 { + for c in c.data_slice_mut() { c.0 *= 2; } - c.1.mark_all_as_updated(); + c.mark_all_as_updated(); } assert!(iter.next().is_none()); let mut iter = query.contiguous_iter(&world).unwrap(); @@ -3833,8 +3838,8 @@ mod tests { for (c, d) in iter { assert!(c.is_some() || d.is_some()); - let c = c.unwrap_or(&[]); - let d = d.unwrap_or(&[]); + let c = c.unwrap_or_default(); + let d = d.unwrap_or_default(); for i in 0..c.len().max(d.len()) { let c = c.get(i).cloned(); let d = d.get(i).cloned(); @@ -3870,7 +3875,7 @@ mod tests { let mut present = [false; 3]; for (c, d) in iter { - let c = c.unwrap_or(&[]); + let c = c.unwrap_or_default(); for i in 0..d.len() { let c = c.get(i).cloned(); let D(d) = d[i]; diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index 041b6eeed7af5..96524a3ec8c94 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -13,14 +13,14 @@ pub struct HealthDecay(pub f32); fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { // as_contiguous_iter() would return None if query couldn't be iterated contiguously - for ((health, _health_ticks), decay) in query.contiguous_iter_mut().unwrap() { - // all slices returned by component queries are the same size - assert!(health.len() == decay.len()); - for i in 0..health.len() { - health[i].0 *= decay[i].0; + for (mut health, decay) in query.contiguous_iter_mut().unwrap() { + // all data slices returned by component queries are the same size + assert!(health.data_slice().len() == decay.data_slice().len()); + for i in 0..health.data_slice().len() { + health.data_slice_mut()[i].0 *= decay.data_slice()[i].0; } // we could have updated health's ticks but it is unnecessary hence we can make less work - // _health_ticks.mark_all_as_updated(); + // health.mark_all_as_updated(); } } diff --git a/examples/ecs/custom_query_param.rs b/examples/ecs/custom_query_param.rs index be322d2c3ec40..0706c85994ffe 100644 --- a/examples/ecs/custom_query_param.rs +++ b/examples/ecs/custom_query_param.rs @@ -219,7 +219,7 @@ fn print_components_contiguous_iter(query: Query Date: Thu, 22 Jan 2026 18:28:25 +0100 Subject: [PATCH 30/52] fixes --- .../src/change_detection/maybe_location.rs | 7 ------- crates/bevy_ecs/src/change_detection/params.rs | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/maybe_location.rs b/crates/bevy_ecs/src/change_detection/maybe_location.rs index e66b0b1c66d46..45272d090d487 100644 --- a/crates/bevy_ecs/src/change_detection/maybe_location.rs +++ b/crates/bevy_ecs/src/change_detection/maybe_location.rs @@ -83,13 +83,6 @@ impl MaybeLocation { } } - /// Mutates the value of `MaybeLocation` - #[inline] - pub fn mutate(&mut self, _f: impl FnOnce(&mut T)) { - #[cfg(feature = "track_location")] - _f(self.value) - } - /// Converts a pair of `MaybeLocation` values to an `MaybeLocation` of a tuple. #[inline] pub fn zip(self, _other: MaybeLocation) -> MaybeLocation<(T, U)> { diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index f450ef3beeb2d..ebd93e5315313 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -47,6 +47,10 @@ impl<'w> ComponentTicksRef<'w> { pub(crate) struct ContiguousComponentTicksRef<'w> { pub(crate) added: &'w [Tick], pub(crate) changed: &'w [Tick], + #[allow( + unused, + reason = "ZST in release mode, for the back-portability with ComponentTicksRef" + )] pub(crate) changed_by: MaybeLocation<&'w [&'static Location<'static>]>, pub(crate) last_run: Tick, pub(crate) this_run: Tick, @@ -144,7 +148,7 @@ impl<'w> ContiguousComponentTicksMut<'w> { pub fn mark_all_as_updated(&mut self) { let this_run = self.this_run; - self.changed_by.mutate(|v| { + self.changed_by.as_mut().map(|v| { for v in v.iter_mut() { *v = Location::caller(); } @@ -156,6 +160,18 @@ impl<'w> ContiguousComponentTicksMut<'w> { } } +impl<'w> From> for ContiguousComponentTicksRef<'w> { + fn from(value: ContiguousComponentTicksMut<'w>) -> Self { + Self { + added: value.added, + changed: value.changed, + changed_by: value.changed_by.map(|v| &*v), + last_run: value.last_run, + this_run: value.this_run, + } + } +} + /// Shared borrow of a [`Resource`]. /// /// See the [`Resource`] documentation for usage. From 7f55e03ae870c1975f7ca09fcc0b0ae8fb936e0b Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:47:03 +0100 Subject: [PATCH 31/52] debug impls --- crates/bevy_ecs/src/change_detection/params.rs | 12 ++++++++++++ examples/ecs/custom_query_param.rs | 18 ++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index ebd93e5315313..4d72584652af0 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -486,6 +486,12 @@ impl<'w, T> ContiguousRef<'w, T> { } } +impl<'w, T: core::fmt::Debug> core::fmt::Debug for ContiguousRef<'w, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("ContiguousRef").field(&self.value).finish() + } +} + impl<'w, 'a, T> IntoIterator for &'a Ref<'w, T> where &'a T: IntoIterator, @@ -652,6 +658,12 @@ impl<'w, T> ContiguousMut<'w, T> { } } +impl<'w, T: core::fmt::Debug> core::fmt::Debug for ContiguousMut<'w, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("ContiguousMut").field(&self.value).finish() + } +} + impl<'w, T: ?Sized> From> for Ref<'w, T> { fn from(mut_ref: Mut<'w, T>) -> Self { Self { diff --git a/examples/ecs/custom_query_param.rs b/examples/ecs/custom_query_param.rs index 0706c85994ffe..b442bd6ce3d4d 100644 --- a/examples/ecs/custom_query_param.rs +++ b/examples/ecs/custom_query_param.rs @@ -207,7 +207,7 @@ fn print_components_tuple( #[query_data(derive(Debug), contiguous(all))] struct CustomContiguousQuery { entity: Entity, - a: &'static ComponentA, + a: Ref<'static, ComponentA>, b: Option<&'static ComponentB>, generic: GenericQuery, } @@ -216,14 +216,12 @@ fn print_components_contiguous_iter(query: Query = e; - for i in 0..e.entity.len() { - println!("Entity: {:?}", e.entity[i]); - println!("A: {:?}", e.a[i]); - println!("B: {:?}", e.b.as_ref().map(|b| &b[i])); - println!( - "Generic: {:?} {:?}", - e.generic.generic.0[i], e.generic.generic.1[i] - ); - } + println!("Entity: {:?}", e.entity); + println!("A: {:?}", e.a); + println!("B: {:?}", e.b); + println!( + "Generic: {:?} {:?}", + e.generic.generic.0, e.generic.generic.1 + ); } } From 888a47331e2f7be9ca433ec8f8a34db805f63958 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:54:26 +0100 Subject: [PATCH 32/52] safety comments --- crates/bevy_ecs/src/change_detection/params.rs | 14 +++++++++++++- crates/bevy_ecs/src/query/fetch.rs | 10 ++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 4d72584652af0..4c5d721c32953 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -47,7 +47,7 @@ impl<'w> ComponentTicksRef<'w> { pub(crate) struct ContiguousComponentTicksRef<'w> { pub(crate) added: &'w [Tick], pub(crate) changed: &'w [Tick], - #[allow( + #[expect( unused, reason = "ZST in release mode, for the back-portability with ComponentTicksRef" )] @@ -57,6 +57,9 @@ pub(crate) struct ContiguousComponentTicksRef<'w> { } impl<'w> ContiguousComponentTicksRef<'w> { + /// # Safety + /// - The caller must have permission for all given ticks to be read. + /// - `len` must be the length of `added`, `changed` and `changed_by` (unless none) slices. pub(crate) unsafe fn from_slice_ptrs( added: ThinSlicePtr<'w, UnsafeCell>, changed: ThinSlicePtr<'w, UnsafeCell>, @@ -66,8 +69,11 @@ impl<'w> ContiguousComponentTicksRef<'w> { last_run: Tick, ) -> Self { Self { + // SAFETY: The invariants are upheld by the caller. added: unsafe { added.cast().as_slice_unchecked(len) }, + // SAFETY: The invariants are upheld by the caller. changed: unsafe { changed.cast().as_slice_unchecked(len) }, + // SAFETY: The invariants are upheld by the caller. changed_by: changed_by.map(|v| unsafe { v.cast().as_slice_unchecked(len) }), last_run, this_run, @@ -128,6 +134,9 @@ pub(crate) struct ContiguousComponentTicksMut<'w> { } impl<'w> ContiguousComponentTicksMut<'w> { + /// # Safety + /// - The caller must have permission to use all given ticks to be mutated. + /// - `len` must be the length of `added`, `changed` and `changed_by` (unless none) slices. pub(crate) unsafe fn from_slice_ptrs( added: ThinSlicePtr<'w, UnsafeCell>, changed: ThinSlicePtr<'w, UnsafeCell>, @@ -137,8 +146,11 @@ impl<'w> ContiguousComponentTicksMut<'w> { last_run: Tick, ) -> Self { Self { + // SAFETY: The invariants are upheld by the caller. added: unsafe { added.as_mut_slice_unchecked(len) }, + // SAFETY: The invariants are upheld by the caller. changed: unsafe { changed.as_mut_slice_unchecked(len) }, + // SAFETY: The invariants are upheld by the caller. changed_by: changed_by.map(|v| unsafe { v.as_mut_slice_unchecked(len) }), last_run, this_run, diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index facb1afea978c..2c9bf493a7561 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2078,7 +2078,12 @@ unsafe impl ContiguousQueryData for Ref<'_, T> { unsafe { table.debug_checked_unwrap() }; ContiguousRef { + // SAFETY: `entities` has the same length as the rows in the set table. value: unsafe { table_components.cast().as_slice_unchecked(entities.len()) }, + // SAFETY: + // - The caller ensures the permission to access ticks. + // - `entities` has the same length as the rows in the set table hence the + // ticks. ticks: unsafe { ContiguousComponentTicksRef::from_slice_ptrs( added_ticks, @@ -2334,7 +2339,12 @@ unsafe impl> ContiguousQueryData for &mut T { unsafe { table.debug_checked_unwrap() }; ContiguousMut { + // SAFETY: `entities` has the same length as the rows in the set table. value: unsafe { table_components.as_mut_slice_unchecked(entities.len()) }, + // SAFETY: + // - The caller ensures the permission to access ticks. + // - `entities` has the same length as the rows in the set table hence the + // ticks. ticks: unsafe { ContiguousComponentTicksMut::from_slice_ptrs( added_ticks, From 1f765c465f5a4e0fccd9b34f6dd34a6a8d0e932a Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:02:17 +0100 Subject: [PATCH 33/52] example fix --- examples/ecs/contiguous_query.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index 96524a3ec8c94..05488e0d71bfb 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -15,9 +15,9 @@ fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { // as_contiguous_iter() would return None if query couldn't be iterated contiguously for (mut health, decay) in query.contiguous_iter_mut().unwrap() { // all data slices returned by component queries are the same size - assert!(health.data_slice().len() == decay.data_slice().len()); + assert!(health.data_slice().len() == decay.len()); for i in 0..health.data_slice().len() { - health.data_slice_mut()[i].0 *= decay.data_slice()[i].0; + health.data_slice_mut()[i].0 *= decay[i].0; } // we could have updated health's ticks but it is unnecessary hence we can make less work // health.mark_all_as_updated(); From bef611ef8ae785507f2fd0f51858aa3b8ccea4d8 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:09:48 +0100 Subject: [PATCH 34/52] iterators --- examples/ecs/contiguous_query.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index 05488e0d71bfb..1be046d6ccd74 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -16,8 +16,8 @@ fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { for (mut health, decay) in query.contiguous_iter_mut().unwrap() { // all data slices returned by component queries are the same size assert!(health.data_slice().len() == decay.len()); - for i in 0..health.data_slice().len() { - health.data_slice_mut()[i].0 *= decay[i].0; + for (health, decay) in health.data_slice_mut().into_iter().zip(decay) { + health.0 *= decay.0; } // we could have updated health's ticks but it is unnecessary hence we can make less work // health.mark_all_as_updated(); From f8286d097a58d3e1c9b233e5a5ae8b6a16a5bf2c Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:16:48 +0100 Subject: [PATCH 35/52] into_iter->iter_mut --- examples/ecs/contiguous_query.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index 1be046d6ccd74..99c5cd8b7d756 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -16,7 +16,7 @@ fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { for (mut health, decay) in query.contiguous_iter_mut().unwrap() { // all data slices returned by component queries are the same size assert!(health.data_slice().len() == decay.len()); - for (health, decay) in health.data_slice_mut().into_iter().zip(decay) { + for (health, decay) in health.data_slice_mut().iter_mut().zip(decay) { health.0 *= decay.0; } // we could have updated health's ticks but it is unnecessary hence we can make less work From 9b14f5f5c49757b5bcc6b27b8f17ee78357edc15 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:34:13 +0100 Subject: [PATCH 36/52] release note --- examples/ecs/contiguous_query.rs | 2 +- release-content/release-notes/contiguous_access.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index 99c5cd8b7d756..e5cc2ea69daee 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -7,7 +7,7 @@ use bevy::prelude::*; pub struct Health(pub f32); #[derive(Component)] -/// Each tick an entity will have his health multiplied by the factor, which +/// Each tick an entity will have it's health multiplied by the factor, which /// for a big amount of entities can be accelerated using contiguous queries pub struct HealthDecay(pub f32); diff --git a/release-content/release-notes/contiguous_access.md b/release-content/release-notes/contiguous_access.md index 9ba013b0b1b13..0dec76df4e588 100644 --- a/release-content/release-notes/contiguous_access.md +++ b/release-content/release-notes/contiguous_access.md @@ -8,24 +8,24 @@ Enables accessing slices from tables directly via Queries. ## Goals -`QueryIter` has a new method `as_contiguous_iter`, which allows querying contiguously (i.e., over tables). For it to work the query data must implement `ContiguousQueryData` and the query filter `ArchetypeFilter`. When a contiguous iterator is used, the iterator will jump over whole tables, returning corresponding values. Some notable implementors of `ContiguousQueryData` are `&T` and `&mut T`, returning `&[T]` and `(&mut T, ContiguousComponentTicks)` correspondingly, where the latter structure in the latter tuple lets you change update ticks. Some notable implementors of `ArchetypeFilter` are `With` and `Without` and notable structs not implementing it are `Changed` and `Added`. +`Query` and `QueryState` have new methods `contiguous_iter`, `contiguous_iter_mut` and `contiguous_iter_inner`, which allows querying contiguously (i.e., over tables). For it to work the query data must implement `ContiguousQueryData` and the query filter `ArchetypeFilter`. When a contiguous iterator is used, the iterator will jump over whole tables, returning corresponding data. Some notable implementors of `ContiguousQueryData` are `&T` and `&mut T`, returning `&[T]` and `ContiguousMut` correspondingly, where the latter structure lets you get a mutable slice of components as well as corresponding ticks. Some notable implementors of `ArchetypeFilter` are `With` and `Without` and notable types **not implementing** it are `Changed` and `Added`. This is for example useful, when an operation must be applied on a big amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. ### Usage -`QueryIter::as_contiguous_iter` method returns an `Option`, which is only `None`, when the query is not dense (i.e., iterates over archetypes, not over tables). +`Query::contiguous_iter` and `Query::contiguous_iter_mut` return a `Option`, which is only `None`, when the query is not dense (i.e., iterates over archetypes, not over tables). ```rust fn apply_velocity(query: Query<(&Velocity, &mut Position)>) { - // `as_contiguous_iter()` cannot ensure all invariants on the compilation stage, thus + // `contiguous_iter_mut()` cannot ensure all invariants on the compilation stage, thus // when a component uses a sparse set storage, the method will return `None` - for (velocity, (position, mut ticks)) in query.iter_mut().as_contiguous_iter().unwrap() { - for (v, p) in velocity.iter().zip(position.iter_mut()) { + for (velocity, mut position) in query.contiguous_iter_mut().unwrap() { + for (v, p) in velocity.iter().zip(position.data_slice_mut().iter_mut()) { p.0 += v.0; } // sets ticks, which is optional - ticks.mark_all_as_updated(); + position.mark_all_as_updated(); } } ``` From 250a359d5bb07baf0083f78e6479d14dea797f67 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:44:37 +0100 Subject: [PATCH 37/52] reborrow --- crates/bevy_ecs/src/change_detection/params.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 4c5d721c32953..14c00fed7841c 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -461,6 +461,7 @@ impl<'w, T: ?Sized> Ref<'w, T> { } /// Data type returned by [`ContiguousQueryData::fetch_contiguous`](crate::query::ContiguousQueryData::fetch_contiguous) for [`Ref`]. +#[derive(Clone)] pub struct ContiguousRef<'w, T> { pub(crate) value: &'w [T], pub(crate) ticks: ContiguousComponentTicksRef<'w>, @@ -668,6 +669,20 @@ impl<'w, T> ContiguousMut<'w, T> { pub fn mark_all_as_updated(&mut self) { self.ticks.mark_all_as_updated(); } + + /// Returns a `ContiguousMut` with a smaller lifetime. + pub fn reborrow(&mut self) -> ContiguousMut<'_, T> { + ContiguousMut { + value: self.value, + ticks: ContiguousComponentTicksMut { + added: self.ticks.added, + changed: self.ticks.changed, + changed_by: self.ticks.changed_by.as_deref_mut(), + last_run: self.ticks.last_run, + this_run: self.ticks.this_run, + }, + } + } } impl<'w, T: core::fmt::Debug> core::fmt::Debug for ContiguousMut<'w, T> { From 2acfca5e8f4e8e08a419d67607ca510fd91a71d6 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:54:23 +0100 Subject: [PATCH 38/52] documentation --- .../bevy_ecs/src/change_detection/params.rs | 16 ++++-- crates/bevy_ecs/src/query/fetch.rs | 20 +++++-- crates/bevy_ecs/src/system/query.rs | 56 +++++++++++++++++++ examples/ecs/contiguous_query.rs | 17 +++++- .../release-notes/contiguous_access.md | 2 +- 5 files changed, 96 insertions(+), 15 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 14c00fed7841c..c79d4965245d2 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -69,11 +69,13 @@ impl<'w> ContiguousComponentTicksRef<'w> { last_run: Tick, ) -> Self { Self { - // SAFETY: The invariants are upheld by the caller. + // SAFETY: + // - The caller ensures that `len` is the length of the slice. + // - The caller ensures we have permission to read the data. added: unsafe { added.cast().as_slice_unchecked(len) }, - // SAFETY: The invariants are upheld by the caller. + // SAFETY: see above. changed: unsafe { changed.cast().as_slice_unchecked(len) }, - // SAFETY: The invariants are upheld by the caller. + // SAFETY: see above. changed_by: changed_by.map(|v| unsafe { v.cast().as_slice_unchecked(len) }), last_run, this_run, @@ -146,11 +148,13 @@ impl<'w> ContiguousComponentTicksMut<'w> { last_run: Tick, ) -> Self { Self { - // SAFETY: The invariants are upheld by the caller. + // SAFETY: + // - The caller ensures that `len` is the length of the slice. + // - The caller ensures we have permission to mutate the data. added: unsafe { added.as_mut_slice_unchecked(len) }, - // SAFETY: The invariants are upheld by the caller. + // SAFETY: see above. changed: unsafe { changed.as_mut_slice_unchecked(len) }, - // SAFETY: The invariants are upheld by the caller. + // SAFETY: see above. changed_by: changed_by.map(|v| unsafe { v.as_mut_slice_unchecked(len) }), last_run, this_run, diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 2c9bf493a7561..db4fdceced849 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -147,12 +147,12 @@ use variadics_please::all_tuples; /// } /// ``` /// -/// ## Adding contiguous items +/// ## Supporting contiguous iteration /// -/// To create contiguous items additionally, the struct must be marked with the `#[query_data(contiguous(target))]` attribute, +/// To create contiguous items additionally (to support contiguous iteration), the struct must be marked with the `#[query_data(contiguous(target))]` attribute, /// where the target may be `all`, `mutable` or `immutable` (see the table above). /// -/// For mutable queries it may be done like that: +/// For mutable queries it may be done like this: /// ``` /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::query::QueryData; @@ -173,6 +173,9 @@ use variadics_please::all_tuples; /// For immutable queries `contiguous(immutable)` attribute will be **ignored**, meanwhile `contiguous(mutable)` and `contiguous(all)` /// will only generate a contiguous item for the (original) read only version. /// +/// To understand contiguous iteration refer to +/// [`Query::contiguous_iter`](`crate::system::Query::contiguous_iter`) +/// /// ## Adding methods to query items /// /// It is possible to add methods to query items in order to write reusable logic about related components. @@ -386,7 +389,12 @@ pub unsafe trait QueryData: WorldQuery { fn iter_access(state: &Self::State) -> impl Iterator>; } -/// A [`QueryData`] which allows getting a direct access to contiguous chunks of components' values +/// A [`QueryData`] which allows getting a direct access to contiguous chunks of components' +/// values, which may be used to apply simd-operations. +/// +/// Contiguous iteration may be done via: +/// - [`Query::contiguous_iter`](crate::system::Query::contiguous_iter), +/// - [`Query::contiguous_iter_mut`](crate::system::Query::contiguous_iter_mut), /// // NOTE: The safety rules might not be used to optimize the library, it still might be better to ensure // that contiguous query data methods match their non-contiguous versions @@ -1816,7 +1824,7 @@ unsafe impl ContiguousQueryData for &T { ) -> Self::Contiguous<'w, 's> { fetch.components.extract( |table| { - // SAFETY: set_table was previously called + // SAFETY: The caller ensures `set_table` was previously called let table = unsafe { table.debug_checked_unwrap() }; // SAFETY: // - `table` is `entities.len()` long @@ -1826,7 +1834,7 @@ unsafe impl ContiguousQueryData for &T { |_| { #[cfg(debug_assertions)] unreachable!(); - // SAFETY: query is dense + // SAFETY: The caller ensures query is dense #[cfg(not(debug_assertions))] core::hint::unreachable_unchecked(); }, diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 4184fde813298..4a240d66408cb 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1359,6 +1359,33 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`World`](crate::world::World) or [`None`] if the query is not dense hence not contiguously /// iterable. /// + /// Contiguous iteration enables getting slices of contiguously lying components (which lie in the same table), which for example + /// may be used for simd-operations, which may accelerate an algorithm. + /// + /// # Example + /// + /// The following system despawns all entities which health is negative. + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct Health(pub f32); + /// + /// fn despawn_all_dead_entities(mut commands: Commands, query: Query<(Entity, &Health)>) { + /// for (entities, health) in query.contiguous_iter() { + /// // For each entity there is one component, hence it always holds true + /// assert!(entities.size() == health.size()); + /// for (entity, health) in entities.iter().zip(health.iter()) { + /// if health.0 < 0.0 { + /// commands.entity(entity).despawn(); + /// } + /// } + /// } + /// } + /// + /// ``` + /// /// A mutable version: [`Self::contiguous_iter_mut`] pub fn contiguous_iter(&self) -> Option> where @@ -1372,6 +1399,35 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`World`](crate::world::World) or [`None`] if the query is not dense hence not contiguously /// iterable. /// + /// Contiguous iteration enables getting slices of contiguously lying components (which lie in the same table), which for example + /// may be used for simd-operations, which may accelerate an algorithm. + /// + /// # Example + /// + /// The following system applies a "health decay" effect on all entities, which reduces their + /// health by some fraction. + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct Health(pub f32); + /// # + /// # #[derive(Component)] + /// # struct HealthDecay(pub f32); + /// + /// fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { + /// for (mut health, decay) in query.contiguous_iter_mut().unwrap() { + /// // all data slices returned by component queries are the same size + /// assert!(health.data_slice().len() == decay.len()); + /// for (health, decay) in health.data_slice_mut().iter_mut().zip(decay) { + /// health.0 *= decay.0; + /// } + /// // we could have updated health's ticks but it is unnecessary hence we can make less work + /// // health.mark_all_as_updated(); + /// } + /// } + /// ``` /// An immutable version: [`Self::contiguous_iter`] pub fn contiguous_iter_mut(&mut self) -> Option> where diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index e5cc2ea69daee..1eb5be2d7fc07 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -1,4 +1,17 @@ -//! Demonstrates how contiguous queries work +//! Demonstrates how contiguous queries work. +//! +//! Contiguous iteration enables getting slices of contiguously lying components (which lie in the same table), which for example +//! may be used for simd-operations, which may accelerate an algorithm. +//! +//! Contiguous iteration may be used for example via [`Query::contiguous_iter`], [`Query::contiguous_iter_mut`], +//! both of which return an option which is only [`None`] when the query doesn't support contiguous +//! iteration due to it not being dense (iteration happens on archetypes, not tables) or filters not being archetypal. +//! +//! Refer to +//! - [`Query::contiguous_iter`] +//! - [`ContiguousQueryData`](`bevy::ecs::query::ContiguousQueryData`) +//! - [`ArchetypeFilter`](`bevy::ecs::query::ArchetypeFilter`) +//! for further documentation. use bevy::prelude::*; @@ -12,7 +25,7 @@ pub struct Health(pub f32); pub struct HealthDecay(pub f32); fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { - // as_contiguous_iter() would return None if query couldn't be iterated contiguously + // contiguous_iter_mut() would return None if query couldn't be iterated contiguously for (mut health, decay) in query.contiguous_iter_mut().unwrap() { // all data slices returned by component queries are the same size assert!(health.data_slice().len() == decay.len()); diff --git a/release-content/release-notes/contiguous_access.md b/release-content/release-notes/contiguous_access.md index 0dec76df4e588..a22441b4e5726 100644 --- a/release-content/release-notes/contiguous_access.md +++ b/release-content/release-notes/contiguous_access.md @@ -10,7 +10,7 @@ Enables accessing slices from tables directly via Queries. `Query` and `QueryState` have new methods `contiguous_iter`, `contiguous_iter_mut` and `contiguous_iter_inner`, which allows querying contiguously (i.e., over tables). For it to work the query data must implement `ContiguousQueryData` and the query filter `ArchetypeFilter`. When a contiguous iterator is used, the iterator will jump over whole tables, returning corresponding data. Some notable implementors of `ContiguousQueryData` are `&T` and `&mut T`, returning `&[T]` and `ContiguousMut` correspondingly, where the latter structure lets you get a mutable slice of components as well as corresponding ticks. Some notable implementors of `ArchetypeFilter` are `With` and `Without` and notable types **not implementing** it are `Changed` and `Added`. -This is for example useful, when an operation must be applied on a big amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. +For example, this is useful, when an operation must be applied on a big amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. ### Usage From 42733c5d7c5eef3ef217e56affa6698900b4032c Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:55:13 +0100 Subject: [PATCH 39/52] big->large --- release-content/release-notes/contiguous_access.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-content/release-notes/contiguous_access.md b/release-content/release-notes/contiguous_access.md index a22441b4e5726..909dbb52988d7 100644 --- a/release-content/release-notes/contiguous_access.md +++ b/release-content/release-notes/contiguous_access.md @@ -10,7 +10,7 @@ Enables accessing slices from tables directly via Queries. `Query` and `QueryState` have new methods `contiguous_iter`, `contiguous_iter_mut` and `contiguous_iter_inner`, which allows querying contiguously (i.e., over tables). For it to work the query data must implement `ContiguousQueryData` and the query filter `ArchetypeFilter`. When a contiguous iterator is used, the iterator will jump over whole tables, returning corresponding data. Some notable implementors of `ContiguousQueryData` are `&T` and `&mut T`, returning `&[T]` and `ContiguousMut` correspondingly, where the latter structure lets you get a mutable slice of components as well as corresponding ticks. Some notable implementors of `ArchetypeFilter` are `With` and `Without` and notable types **not implementing** it are `Changed` and `Added`. -For example, this is useful, when an operation must be applied on a big amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. +For example, this is useful, when an operation must be applied on a large amount of entities lying in the same tables, which allows for the compiler to auto-vectorize the code, thus speeding it up. ### Usage From 4e1cc105249f143f442cd7cc10b2d5ab3c33c938 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:29:54 +0100 Subject: [PATCH 40/52] docs --- examples/ecs/contiguous_query.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index 1eb5be2d7fc07..436cf61e71def 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -7,11 +7,10 @@ //! both of which return an option which is only [`None`] when the query doesn't support contiguous //! iteration due to it not being dense (iteration happens on archetypes, not tables) or filters not being archetypal. //! -//! Refer to +//! For further documentation refer to: //! - [`Query::contiguous_iter`] //! - [`ContiguousQueryData`](`bevy::ecs::query::ContiguousQueryData`) //! - [`ArchetypeFilter`](`bevy::ecs::query::ArchetypeFilter`) -//! for further documentation. use bevy::prelude::*; From 6e0441fb450433e688f7c80f374e04adc05236e2 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:44:02 +0100 Subject: [PATCH 41/52] docs --- crates/bevy_ecs/src/system/query.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 4a240d66408cb..d14294ab0f14b 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1373,7 +1373,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # struct Health(pub f32); /// /// fn despawn_all_dead_entities(mut commands: Commands, query: Query<(Entity, &Health)>) { - /// for (entities, health) in query.contiguous_iter() { + /// for (entities, health) in query.contiguous_iter().unwrap() { /// // For each entity there is one component, hence it always holds true /// assert!(entities.size() == health.size()); /// for (entity, health) in entities.iter().zip(health.iter()) { From 526680b86c12ad830836f7a0bc7c9e3e7859399e Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:00:40 +0100 Subject: [PATCH 42/52] docs --- crates/bevy_ecs/src/system/query.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index d14294ab0f14b..66cc7a43e1271 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1375,10 +1375,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// fn despawn_all_dead_entities(mut commands: Commands, query: Query<(Entity, &Health)>) { /// for (entities, health) in query.contiguous_iter().unwrap() { /// // For each entity there is one component, hence it always holds true - /// assert!(entities.size() == health.size()); + /// assert!(entities.len() == health.len()); /// for (entity, health) in entities.iter().zip(health.iter()) { /// if health.0 < 0.0 { - /// commands.entity(entity).despawn(); + /// commands.entity(*entity).despawn(); /// } /// } /// } From 31d6f8c656a27a0bcad8243048dec96b9aa210fa Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:55:14 +0100 Subject: [PATCH 43/52] removing unsafe --- crates/bevy_ecs/macros/src/query_data.rs | 3 +- .../bevy_ecs/src/change_detection/params.rs | 30 +++++++++++---- crates/bevy_ecs/src/query/fetch.rs | 37 +++++-------------- 3 files changed, 34 insertions(+), 36 deletions(-) diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index ff081863d4424..e90daf34af457 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -94,8 +94,7 @@ fn contiguous_query_data_impl( user_where_clauses: Option<&WhereClause>, ) -> proc_macro2::TokenStream { quote! { - // SAFETY: Individual `fetch_contiguous` are called. - unsafe impl #user_impl_generics #path::query::ContiguousQueryData for #struct_name #user_ty_generics #user_where_clauses { + impl #user_impl_generics #path::query::ContiguousQueryData for #struct_name #user_ty_generics #user_where_clauses { type Contiguous<'__w, '__s> = #contiguous_item_struct_name #user_ty_generics_with_world_and_state; unsafe fn fetch_contiguous<'__w, '__s>( diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index c79d4965245d2..bbed58b6803c5 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -47,10 +47,6 @@ impl<'w> ComponentTicksRef<'w> { pub(crate) struct ContiguousComponentTicksRef<'w> { pub(crate) added: &'w [Tick], pub(crate) changed: &'w [Tick], - #[expect( - unused, - reason = "ZST in release mode, for the back-portability with ComponentTicksRef" - )] pub(crate) changed_by: MaybeLocation<&'w [&'static Location<'static>]>, pub(crate) last_run: Tick, pub(crate) this_run: Tick, @@ -474,22 +470,28 @@ pub struct ContiguousRef<'w, T> { impl<'w, T> ContiguousRef<'w, T> { /// Returns the data slice. #[inline] - pub fn data_slice(&self) -> &[T] { + pub fn data_slice(&self) -> &'w [T] { self.value } /// Returns the added ticks. #[inline] - pub fn added_ticks_slice(&self) -> &[Tick] { + pub fn added_ticks_slice(&self) -> &'w [Tick] { self.ticks.added } /// Returns the changed ticks. #[inline] - pub fn changed_ticks_slice(&self) -> &[Tick] { + pub fn changed_ticks_slice(&self) -> &'w [Tick] { self.ticks.changed } + /// Returns the changed by ticks. + #[inline] + pub fn changed_by_ticks_slice(&self) -> MaybeLocation<&[&'static Location<'static>]> { + self.ticks.changed_by.as_deref() + } + /// Returns the tick when the system last ran. #[inline] pub fn last_run_tick(&self) -> Tick { @@ -642,6 +644,12 @@ impl<'w, T> ContiguousMut<'w, T> { self.ticks.changed } + /// Returns the mutable changed by ticks' slice + #[inline] + pub fn changed_by_ticks_mut(&self) -> MaybeLocation<&[&'static Location<'static>]> { + self.ticks.changed_by.as_deref() + } + /// Returns the tick when the system last ran. #[inline] pub fn last_run_tick(&self) -> Tick { @@ -666,6 +674,14 @@ impl<'w, T> ContiguousMut<'w, T> { self.ticks.changed } + /// Returns the mutable changed by ticks' slice + #[inline] + pub fn changed_by_ticks_slice_mut( + &mut self, + ) -> MaybeLocation<&mut [&'static Location<'static>]> { + self.ticks.changed_by.as_deref_mut() + } + /// Marks all components as updated. /// /// **Runs in O(n), where n is the amount of rows** diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index db4fdceced849..3178572cdd1b3 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -396,20 +396,14 @@ pub unsafe trait QueryData: WorldQuery { /// - [`Query::contiguous_iter`](crate::system::Query::contiguous_iter), /// - [`Query::contiguous_iter_mut`](crate::system::Query::contiguous_iter_mut), /// -// NOTE: The safety rules might not be used to optimize the library, it still might be better to ensure -// that contiguous query data methods match their non-contiguous versions // NOTE: Even though all component references (&T, &mut T) implement this trait, it won't be executed for // SparseSet components because in that case the query is not dense. -/// # Safety -/// -/// - The result of [`ContiguousQueryData::fetch_contiguous`] must represent the same result as if -/// [`QueryData::fetch`] was executed for each entity of the set table #[diagnostic::on_unimplemented( message = "`{Self}` cannot be iterated contiguously", label = "invalid contiguous `Query` data", note = "if `{Self}` is a custom query type, using `QueryData` derive macro, ensure that the `#[query_data(contiguous(target))]` attribute is added" )] -pub unsafe trait ContiguousQueryData: ArchetypeQueryData { +pub trait ContiguousQueryData: ArchetypeQueryData { /// Item returned by [`ContiguousQueryData::fetch_contiguous`]. /// Represents a contiguous chunk of memory. type Contiguous<'w, 's>; @@ -553,8 +547,7 @@ impl ReleaseStateQueryData for Entity { impl ArchetypeQueryData for Entity {} -/// SAFETY: matches the [`QueryData::fetch`] implementation -unsafe impl ContiguousQueryData for Entity { +impl ContiguousQueryData for Entity { type Contiguous<'w, 's> = &'w [Entity]; unsafe fn fetch_contiguous<'w, 's>( @@ -1813,8 +1806,7 @@ unsafe impl QueryData for &T { } } -/// SAFETY: The result represents all values of [`Self`] in the set table. -unsafe impl ContiguousQueryData for &T { +impl ContiguousQueryData for &T { type Contiguous<'w, 's> = &'w [T]; unsafe fn fetch_contiguous<'w, 's>( @@ -2070,8 +2062,7 @@ impl ReleaseStateQueryData for Ref<'_, T> { impl ArchetypeQueryData for Ref<'_, T> {} -/// SAFETY: Refer to [`&mut T`]'s implementation -unsafe impl ContiguousQueryData for Ref<'_, T> { +impl ContiguousQueryData for Ref<'_, T> { type Contiguous<'w, 's> = ContiguousRef<'w, T>; unsafe fn fetch_contiguous<'w, 's>( @@ -2329,10 +2320,7 @@ impl> ReleaseStateQueryData for &mut T { impl> ArchetypeQueryData for &mut T {} -/// SAFETY: -/// - The first element of [`ContiguousQueryData::Contiguous`] tuple represents all components' values in the set table. -/// - The second element of [`ContiguousQueryData::Contiguous`] tuple represents all components' ticks in the set table. -unsafe impl> ContiguousQueryData for &mut T { +impl> ContiguousQueryData for &mut T { type Contiguous<'w, 's> = ContiguousMut<'w, T>; unsafe fn fetch_contiguous<'w, 's>( @@ -2495,8 +2483,7 @@ impl> ReleaseStateQueryData for Mut<'_, T> { impl> ArchetypeQueryData for Mut<'_, T> {} -/// SAFETY: Refer to soundness of `&mut T` implementation -unsafe impl<'__w, T: Component> ContiguousQueryData for Mut<'__w, T> { +impl<'__w, T: Component> ContiguousQueryData for Mut<'__w, T> { type Contiguous<'w, 's> = ContiguousMut<'w, T>; unsafe fn fetch_contiguous<'w, 's>( @@ -2665,8 +2652,7 @@ impl ReleaseStateQueryData for Option { // so it's always an `ArchetypeQueryData`, even for non-archetypal `T`. impl ArchetypeQueryData for Option {} -// SAFETY: matches the [`QueryData::fetch`] impl -unsafe impl ContiguousQueryData for Option { +impl ContiguousQueryData for Option { type Contiguous<'w, 's> = Option>; unsafe fn fetch_contiguous<'w, 's>( @@ -2859,8 +2845,7 @@ impl ReleaseStateQueryData for Has { impl ArchetypeQueryData for Has {} -/// SAFETY: matches [`QueryData::fetch`] -unsafe impl ContiguousQueryData for Has { +impl ContiguousQueryData for Has { type Contiguous<'w, 's> = bool; unsafe fn fetch_contiguous<'w, 's>( @@ -2980,8 +2965,7 @@ macro_rules! impl_tuple_query_data { 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." )] $(#[$meta])* - // SAFETY: The returned result represents the result of individual fetches. - unsafe impl<$($name: ContiguousQueryData),*> ContiguousQueryData for ($($name,)*) { + impl<$($name: ContiguousQueryData),*> ContiguousQueryData for ($($name,)*) { type Contiguous<'w, 's> = ($($name::Contiguous::<'w, 's>,)*); unsafe fn fetch_contiguous<'w, 's>( @@ -3215,8 +3199,7 @@ macro_rules! impl_anytuple_fetch { 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." )] $(#[$meta])* - // SAFETY: Matches the fetch implementation - unsafe impl<$($name: ContiguousQueryData),*> ContiguousQueryData for AnyOf<($($name,)*)> { + impl<$($name: ContiguousQueryData),*> ContiguousQueryData for AnyOf<($($name,)*)> { type Contiguous<'w, 's> = ($(Option<$name::Contiguous<'w,'s>>,)*); unsafe fn fetch_contiguous<'w, 's>( From ecc13e2e2ae7a6dfc8728d161577cd3020cb8acc Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:00:59 +0100 Subject: [PATCH 44/52] Deref(Mut) --- .../iteration/iter_simple_contiguous.rs | 6 +- .../iteration/iter_simple_contiguous_avx2.rs | 4 +- .../iter_simple_no_detection_contiguous.rs | 7 +- .../bevy_ecs/src/change_detection/params.rs | 93 ++++++++++++++++--- .../bevy_ecs/src/change_detection/traits.rs | 1 + crates/bevy_ecs/src/query/fetch.rs | 5 +- crates/bevy_ecs/src/system/query.rs | 7 +- examples/ecs/contiguous_query.rs | 8 +- .../release-notes/contiguous_access.md | 5 +- 9 files changed, 99 insertions(+), 37 deletions(-) diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs index ae03ac81b1a35..07b5fa86134cb 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous.rs @@ -37,12 +37,10 @@ impl<'w> Benchmark<'w> { pub fn run(&mut self) { let iter = self.1.contiguous_iter_mut(&mut self.0).unwrap(); for (velocity, mut position) in iter { - assert!(velocity.len() == position.data_slice().len()); - for (v, p) in velocity.iter().zip(position.data_slice_mut().iter_mut()) { + assert!(velocity.len() == position.len()); + for (v, p) in velocity.iter().zip(position.iter_mut()) { p.0 += v.0; } - // to match the iter_simple benchmark - position.mark_all_as_updated(); } } } diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs index dc6ea09ba7a12..a0ce817cbcfa1 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_contiguous_avx2.rs @@ -57,10 +57,8 @@ impl<'w> Benchmark<'w> { for (velocity, mut position) in iter { // SAFETY: checked in new unsafe { - exec(position.data_slice_mut(), velocity); + exec(position.as_mut(), velocity); } - // to match the iter_simple benchmark - position.mark_all_as_updated(); } } } diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs index 19722654462c5..8b8eb3d15df15 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_no_detection_contiguous.rs @@ -37,8 +37,11 @@ impl<'w> Benchmark<'w> { pub fn run(&mut self) { let iter = self.1.contiguous_iter_mut(&mut self.0).unwrap(); for (velocity, mut position) in iter { - assert!(velocity.len() == position.data_slice().len()); - for (v, p) in velocity.iter().zip(position.data_slice_mut().iter_mut()) { + assert!(velocity.len() == position.len()); + for (v, p) in velocity + .iter() + .zip(position.bypass_change_detection().iter_mut()) + { p.0 += v.0; } } diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index bbed58b6803c5..d29ae71585639 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -468,12 +468,6 @@ pub struct ContiguousRef<'w, T> { } impl<'w, T> ContiguousRef<'w, T> { - /// Returns the data slice. - #[inline] - pub fn data_slice(&self) -> &'w [T] { - self.value - } - /// Returns the added ticks. #[inline] pub fn added_ticks_slice(&self) -> &'w [Tick] { @@ -505,6 +499,32 @@ impl<'w, T> ContiguousRef<'w, T> { } } +impl<'w, T> Deref for ContiguousRef<'w, T> { + type Target = [T]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.value + } +} + +impl<'w, T> AsRef<[T]> for ContiguousRef<'w, T> { + #[inline] + fn as_ref(&self) -> &[T] { + self.deref() + } +} + +impl<'w, T> IntoIterator for ContiguousRef<'w, T> { + type Item = &'w T; + + type IntoIter = core::slice::Iter<'w, T>; + + fn into_iter(self) -> Self::IntoIter { + self.value.iter() + } +} + impl<'w, T: core::fmt::Debug> core::fmt::Debug for ContiguousRef<'w, T> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("ContiguousRef").field(&self.value).finish() @@ -614,21 +634,24 @@ impl<'w, T: ?Sized> Mut<'w, T> { /// Data type returned by [`ContiguousQueryData::fetch_contiguous`](crate::query::ContiguousQueryData::fetch_contiguous) /// for [`Mut`] and `&mut T` +/// +/// # Warning +/// Implementations of [`DerefMut`], [`AsMut`] and [`IntoIterator`] update change ticks, which may effect performance. pub struct ContiguousMut<'w, T> { pub(crate) value: &'w mut [T], pub(crate) ticks: ContiguousComponentTicksMut<'w>, } impl<'w, T> ContiguousMut<'w, T> { - /// Returns the mutable data slice. - #[inline] - pub fn data_slice_mut(&mut self) -> &mut [T] { - self.value - } - - /// Returns the immutable data slice. + /// Manually bypasses change detection, allowing you to mutate the underlying values without updating the change tick, + /// which may be useful to reduce amount of work to be done. + /// + /// # Warning + /// This is a risky operation, that can have unexpected consequences on any system relying on this code. + /// However, it can be an essential escape hatch when, for example, + /// you are trying to synchronize representations using change detection and need to avoid infinite recursion. #[inline] - pub fn data_slice(&self) -> &[T] { + pub fn bypass_change_detection(&mut self) -> &mut [T] { self.value } @@ -705,6 +728,48 @@ impl<'w, T> ContiguousMut<'w, T> { } } +impl<'w, T> Deref for ContiguousMut<'w, T> { + type Target = [T]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.value + } +} + +impl<'w, T> DerefMut for ContiguousMut<'w, T> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.mark_all_as_updated(); + self.value + } +} + +impl<'w, T> AsRef<[T]> for ContiguousMut<'w, T> { + #[inline] + fn as_ref(&self) -> &[T] { + self.deref() + } +} + +impl<'w, T> AsMut<[T]> for ContiguousMut<'w, T> { + #[inline] + fn as_mut(&mut self) -> &mut [T] { + self.deref_mut() + } +} + +impl<'w, T> IntoIterator for ContiguousMut<'w, T> { + type Item = &'w mut T; + + type IntoIter = core::slice::IterMut<'w, T>; + + fn into_iter(mut self) -> Self::IntoIter { + self.mark_all_as_updated(); + self.value.iter_mut() + } +} + impl<'w, T: core::fmt::Debug> core::fmt::Debug for ContiguousMut<'w, T> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("ContiguousMut").field(&self.value).finish() diff --git a/crates/bevy_ecs/src/change_detection/traits.rs b/crates/bevy_ecs/src/change_detection/traits.rs index 30025551ada58..393bb3d3d0b86 100644 --- a/crates/bevy_ecs/src/change_detection/traits.rs +++ b/crates/bevy_ecs/src/change_detection/traits.rs @@ -121,6 +121,7 @@ pub trait DetectChangesMut: DetectChanges { /// The caveats of [`set_last_changed`](DetectChangesMut::set_last_changed) apply. This modifies both the added and changed ticks together. fn set_last_added(&mut self, last_added: Tick); + // NOTE: if you are changing the following comment also change the [`ContiguousMut::bypass_change_detection`] comment. /// Manually bypasses change detection, allowing you to mutate the underlying value without updating the change tick. /// /// # Warning diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 3178572cdd1b3..cfd3d25333a6a 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -3779,11 +3779,10 @@ mod tests { let mut query = world.query::<&mut C>(); let mut iter = query.contiguous_iter_mut(&mut world).unwrap(); for _ in 0..2 { - let mut c = iter.next().unwrap(); - for c in c.data_slice_mut() { + let c = iter.next().unwrap(); + for c in c { c.0 *= 2; } - c.mark_all_as_updated(); } assert!(iter.next().is_none()); let mut iter = query.contiguous_iter(&world).unwrap(); diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 66cc7a43e1271..075b151e43350 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1419,12 +1419,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { /// for (mut health, decay) in query.contiguous_iter_mut().unwrap() { /// // all data slices returned by component queries are the same size - /// assert!(health.data_slice().len() == decay.len()); - /// for (health, decay) in health.data_slice_mut().iter_mut().zip(decay) { + /// assert!(health.len() == decay.len()); + /// // we could have use health.bypass_change_detection() to do less work. + /// for (health, decay) in health.iter_mut().zip(decay) { /// health.0 *= decay.0; /// } - /// // we could have updated health's ticks but it is unnecessary hence we can make less work - /// // health.mark_all_as_updated(); /// } /// } /// ``` diff --git a/examples/ecs/contiguous_query.rs b/examples/ecs/contiguous_query.rs index 436cf61e71def..a6d5ec7418bcf 100644 --- a/examples/ecs/contiguous_query.rs +++ b/examples/ecs/contiguous_query.rs @@ -27,12 +27,12 @@ fn apply_health_decay(mut query: Query<(&mut Health, &HealthDecay)>) { // contiguous_iter_mut() would return None if query couldn't be iterated contiguously for (mut health, decay) in query.contiguous_iter_mut().unwrap() { // all data slices returned by component queries are the same size - assert!(health.data_slice().len() == decay.len()); - for (health, decay) in health.data_slice_mut().iter_mut().zip(decay) { + assert!(health.len() == decay.len()); + // we could also bypass change detection via bypass_change_detection() because we do not + // use it anyways. + for (health, decay) in health.iter_mut().zip(decay) { health.0 *= decay.0; } - // we could have updated health's ticks but it is unnecessary hence we can make less work - // health.mark_all_as_updated(); } } diff --git a/release-content/release-notes/contiguous_access.md b/release-content/release-notes/contiguous_access.md index 909dbb52988d7..3cbf0fe2d15cf 100644 --- a/release-content/release-notes/contiguous_access.md +++ b/release-content/release-notes/contiguous_access.md @@ -21,11 +21,10 @@ fn apply_velocity(query: Query<(&Velocity, &mut Position)>) { // `contiguous_iter_mut()` cannot ensure all invariants on the compilation stage, thus // when a component uses a sparse set storage, the method will return `None` for (velocity, mut position) in query.contiguous_iter_mut().unwrap() { - for (v, p) in velocity.iter().zip(position.data_slice_mut().iter_mut()) { + // we could also have use position.bypass_change_detection() to do even less work. + for (v, p) in velocity.iter().zip(position.iter_mut()) { p.0 += v.0; } - // sets ticks, which is optional - position.mark_all_as_updated(); } } ``` From 99d116d53538638825311b3e50cf01f4870699ab Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:04:16 +0100 Subject: [PATCH 45/52] typo --- crates/bevy_ecs/src/system/query.rs | 2 +- release-content/release-notes/contiguous_access.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 075b151e43350..c6293ede5c0b1 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1420,7 +1420,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// for (mut health, decay) in query.contiguous_iter_mut().unwrap() { /// // all data slices returned by component queries are the same size /// assert!(health.len() == decay.len()); - /// // we could have use health.bypass_change_detection() to do less work. + /// // we could have used health.bypass_change_detection() to do less work. /// for (health, decay) in health.iter_mut().zip(decay) { /// health.0 *= decay.0; /// } diff --git a/release-content/release-notes/contiguous_access.md b/release-content/release-notes/contiguous_access.md index 3cbf0fe2d15cf..6921cd30c09f3 100644 --- a/release-content/release-notes/contiguous_access.md +++ b/release-content/release-notes/contiguous_access.md @@ -21,7 +21,7 @@ fn apply_velocity(query: Query<(&Velocity, &mut Position)>) { // `contiguous_iter_mut()` cannot ensure all invariants on the compilation stage, thus // when a component uses a sparse set storage, the method will return `None` for (velocity, mut position) in query.contiguous_iter_mut().unwrap() { - // we could also have use position.bypass_change_detection() to do even less work. + // we could also have used position.bypass_change_detection() to do even less work. for (v, p) in velocity.iter().zip(position.iter_mut()) { p.0 += v.0; } From 22612d79b1b3ea71e5bc08bfc9fa90efd52af85d Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:15:07 +0100 Subject: [PATCH 46/52] into_inner --- crates/bevy_ecs/src/change_detection/params.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index d29ae71585639..d23643cd3ab3e 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -468,6 +468,11 @@ pub struct ContiguousRef<'w, T> { } impl<'w, T> ContiguousRef<'w, T> { + /// Returns the reference wrapped by this type. The reference is allowed to outlive `self`, which makes this method more flexible than simply borrowing `self`. + pub fn into_inner(self) -> &'w [T] { + self.value + } + /// Returns the added ticks. #[inline] pub fn added_ticks_slice(&self) -> &'w [Tick] { From 371c52c68de9e0be55535efea619ebf333221960 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:24:31 +0100 Subject: [PATCH 47/52] ContiguousRef::new --- .../bevy_ecs/src/change_detection/params.rs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index d23643cd3ab3e..e8a8d80b45679 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -502,6 +502,40 @@ impl<'w, T> ContiguousRef<'w, T> { pub fn this_run_tick(&self) -> Tick { self.ticks.this_run } + + /// Creates a new `ContiguousRef` using provided values. + /// + /// This is an advanced feature, `ContiguousRef`s are designed to be _created_ by + /// engine-internal code and _consumed_ by end-user code. + /// + /// - `value` - The values wrapped by `ContiguousRef`. + /// - `added` - [`Tick`]s that store the tick when the wrapped value was created. + /// - `changed` - [`Tick`]s that store the last time the wrapped value was changed. + /// - `last_run` - A [`Tick`], occurring before `this_run`, which is used + /// as a reference to determine whether the wrapped value is newly added or changed. + /// - `this_run` - A [`Tick`] corresponding to the current point in time -- "now". + /// - `caller` - [`Location`]s that store the location when the wrapper value was changed. + /// + /// See also: [`Ref::new`] + pub fn new( + value: &'w [T], + added: &'w [Tick], + changed: &'w [Tick], + last_run: Tick, + this_run: Tick, + caller: MaybeLocation<&'w [&'static Location<'static>]>, + ) -> Self { + Self { + value, + ticks: ContiguousComponentTicksRef { + added, + changed, + changed_by: caller, + last_run, + this_run, + }, + } + } } impl<'w, T> Deref for ContiguousRef<'w, T> { From bddcf579c454af408648e6bb22b4c3036f94198b Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:55:21 +0100 Subject: [PATCH 48/52] new ticks methods --- .../bevy_ecs/src/change_detection/params.rs | 162 +++++++++++++++++- crates/bevy_ecs/src/lib.rs | 4 +- 2 files changed, 157 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index e8a8d80b45679..a31c62145c810 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -43,8 +43,14 @@ impl<'w> ComponentTicksRef<'w> { } } +/// Data type storing contiguously lying ticks. +/// +/// Retrievable via [`ContiguousRef::split`] and probably only useful if you want to use the following +/// methods: +/// - [ContiguousComponentTicksRef::is_changed_iter], +/// - [ContiguousComponentTicksRef::is_added_iter] #[derive(Clone)] -pub(crate) struct ContiguousComponentTicksRef<'w> { +pub struct ContiguousComponentTicksRef<'w> { pub(crate) added: &'w [Tick], pub(crate) changed: &'w [Tick], pub(crate) changed_by: MaybeLocation<&'w [&'static Location<'static>]>, @@ -77,6 +83,60 @@ impl<'w> ContiguousComponentTicksRef<'w> { this_run, } } + + /// Returns an iterator where the i-th item corresponds to whether the i-th component was + /// marked as changed. If the value equals [`prim@true`], then the component was changed. + /// + /// # Example + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct A(pub i32); + /// + /// fn some_system(mut query: Query>) { + /// for a in query.contiguous_iter().unwrap() { + /// let (a_values, a_ticks) = ContiguousRef::split(a); + /// for (value, is_changed) in a_values.iter().zip(a_ticks.is_changed_iter()) { + /// if is_changed { + /// // do something + /// } + /// } + /// } + /// } + /// ``` + pub fn is_changed_iter(&self) -> impl Iterator { + self.changed + .iter() + .map(|v| v.is_newer_than(self.last_run, self.this_run)) + } + + /// Returns an iterator where the i-th item corresponds to whether the i-th component was + /// marked as added. If the value equals [`prim@true`], then the component was added. + /// + /// # Example + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct A(pub i32); + /// + /// fn some_system(mut query: Query>) { + /// for a in query.contiguous_iter().unwrap() { + /// let (a_values, a_ticks) = ContiguousRef::split(a); + /// for (value, is_added) in a_values.iter().zip(a_ticks.is_added_iter()) { + /// if is_added { + /// // do something + /// } + /// } + /// } + /// } + /// ``` + pub fn is_added_iter(&self) -> impl Iterator { + self.added + .iter() + .map(|v| v.is_newer_than(self.last_run, self.this_run)) + } } /// Used by mutable query parameters (such as [`Mut`] and [`ResMut`]) @@ -123,7 +183,13 @@ impl<'w> From> for ComponentTicksRef<'w> { } } -pub(crate) struct ContiguousComponentTicksMut<'w> { +/// Data type storing contiguously lying ticks, which may be accessed to mutate. +/// +/// Retrievable via [`ContiguousMut::split`] and probably only useful if you want to use the following +/// methods: +/// - [ContiguousComponentTicksMut::is_changed_iter], +/// - [ContiguousComponentTicksMut::is_added_iter] +pub struct ContiguousComponentTicksMut<'w> { pub(crate) added: &'w mut [Tick], pub(crate) changed: &'w mut [Tick], pub(crate) changed_by: MaybeLocation<&'w mut [&'static Location<'static>]>, @@ -157,7 +223,62 @@ impl<'w> ContiguousComponentTicksMut<'w> { } } - pub fn mark_all_as_updated(&mut self) { + /// Returns an iterator where the i-th item corresponds to whether the i-th component was + /// marked as changed. If the value equals [`prim@true`], then the component was changed. + /// + /// # Example + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct A(pub i32); + /// + /// fn some_system(mut query: Query<&mut A>) { + /// for a in query.contiguous_iter_mut().unwrap() { + /// let (a_values, a_ticks) = ContiguousMut::split(a); + /// for (value, is_changed) in a_values.iter_mut().zip(a_ticks.is_changed_iter()) { + /// if is_changed { + /// value.0 *= 10; + /// } + /// } + /// } + /// } + /// ``` + pub fn is_changed_iter(&self) -> impl Iterator { + self.changed + .iter() + .map(|v| v.is_newer_than(self.last_run, self.this_run)) + } + + /// Returns an iterator where the i-th item corresponds to whether the i-th component was + /// marked as added. If the value equals [`prim@true`], then the component was added. + /// + /// # Example + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct A(pub i32); + /// + /// fn some_system(mut query: Query<&mut A>) { + /// for a in query.contiguous_iter_mut().unwrap() { + /// let (a_values, a_ticks) = ContiguousMut::split(a); + /// for (value, is_added) in a_values.iter_mut().zip(a_ticks.is_added_iter()) { + /// if is_added { + /// value.0 = 10; + /// } + /// } + /// } + /// } + /// ``` + pub fn is_added_iter(&self) -> impl Iterator { + self.added + .iter() + .map(|v| v.is_newer_than(self.last_run, self.this_run)) + } + + /// Marks every tick as changed. + pub fn mark_all_as_changed(&mut self) { let this_run = self.this_run; self.changed_by.as_mut().map(|v| { @@ -460,6 +581,8 @@ impl<'w, T: ?Sized> Ref<'w, T> { } } +/// Contiguous equivalent of [`Ref`]. +/// /// Data type returned by [`ContiguousQueryData::fetch_contiguous`](crate::query::ContiguousQueryData::fetch_contiguous) for [`Ref`]. #[derive(Clone)] pub struct ContiguousRef<'w, T> { @@ -536,6 +659,11 @@ impl<'w, T> ContiguousRef<'w, T> { }, } } + + /// Splits [`ContiguousRef`] into it's inner data types. + pub fn split(this: Self) -> (&'w [T], ContiguousComponentTicksRef<'w>) { + (this.value, this.ticks) + } } impl<'w, T> Deref for ContiguousRef<'w, T> { @@ -744,12 +872,12 @@ impl<'w, T> ContiguousMut<'w, T> { self.ticks.changed_by.as_deref_mut() } - /// Marks all components as updated. + /// Marks all components as changed. /// /// **Runs in O(n), where n is the amount of rows** #[inline] - pub fn mark_all_as_updated(&mut self) { - self.ticks.mark_all_as_updated(); + pub fn mark_all_as_changed(&mut self) { + self.ticks.mark_all_as_changed(); } /// Returns a `ContiguousMut` with a smaller lifetime. @@ -765,6 +893,15 @@ impl<'w, T> ContiguousMut<'w, T> { }, } } + + /// Splits [`ContiguousMut`] into it's inner data types. It may be useful, when you want to + /// have an iterator over component values and check ticks simultaneously. + /// + /// # Warning + /// **Bypasses change detection** + pub fn split(this: Self) -> (&'w mut [T], ContiguousComponentTicksMut<'w>) { + (this.value, this.ticks) + } } impl<'w, T> Deref for ContiguousMut<'w, T> { @@ -779,7 +916,7 @@ impl<'w, T> Deref for ContiguousMut<'w, T> { impl<'w, T> DerefMut for ContiguousMut<'w, T> { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { - self.mark_all_as_updated(); + self.mark_all_as_changed(); self.value } } @@ -804,7 +941,7 @@ impl<'w, T> IntoIterator for ContiguousMut<'w, T> { type IntoIter = core::slice::IterMut<'w, T>; fn into_iter(mut self) -> Self::IntoIter { - self.mark_all_as_updated(); + self.mark_all_as_changed(); self.value.iter_mut() } } @@ -815,6 +952,15 @@ impl<'w, T: core::fmt::Debug> core::fmt::Debug for ContiguousMut<'w, T> { } } +impl<'w, T> From> for ContiguousRef<'w, T> { + fn from(value: ContiguousMut<'w, T>) -> Self { + Self { + value: value.value, + ticks: value.ticks.into(), + } + } +} + impl<'w, T: ?Sized> From> for Ref<'w, T> { fn from(mut_ref: Mut<'w, T>) -> Self { Self { diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 4c8763ad66f64..405f253160c47 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -70,7 +70,9 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ bundle::Bundle, - change_detection::{DetectChanges, DetectChangesMut, Mut, Ref}, + change_detection::{ + ContiguousMut, ContiguousRef, DetectChanges, DetectChangesMut, Mut, Ref, + }, children, component::Component, entity::{ContainsEntity, Entity, EntityMapper}, From 6430029df71b0885c2665cd44d45630088def372 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 01:03:05 +0100 Subject: [PATCH 49/52] docs fix --- crates/bevy_ecs/src/change_detection/params.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index a31c62145c810..753fe424c180d 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -47,8 +47,8 @@ impl<'w> ComponentTicksRef<'w> { /// /// Retrievable via [`ContiguousRef::split`] and probably only useful if you want to use the following /// methods: -/// - [ContiguousComponentTicksRef::is_changed_iter], -/// - [ContiguousComponentTicksRef::is_added_iter] +/// - [`ContiguousComponentTicksRef::is_changed_iter`], +/// - [`ContiguousComponentTicksRef::is_added_iter`] #[derive(Clone)] pub struct ContiguousComponentTicksRef<'w> { pub(crate) added: &'w [Tick], @@ -187,8 +187,8 @@ impl<'w> From> for ComponentTicksRef<'w> { /// /// Retrievable via [`ContiguousMut::split`] and probably only useful if you want to use the following /// methods: -/// - [ContiguousComponentTicksMut::is_changed_iter], -/// - [ContiguousComponentTicksMut::is_added_iter] +/// - [`ContiguousComponentTicksMut::is_changed_iter`], +/// - [`ContiguousComponentTicksMut::is_added_iter`] pub struct ContiguousComponentTicksMut<'w> { pub(crate) added: &'w mut [Tick], pub(crate) changed: &'w mut [Tick], @@ -895,7 +895,9 @@ impl<'w, T> ContiguousMut<'w, T> { } /// Splits [`ContiguousMut`] into it's inner data types. It may be useful, when you want to - /// have an iterator over component values and check ticks simultaneously. + /// have an iterator over component values and check ticks simultaneously (using + /// [`ContiguousComponentTicksMut::is_changed_iter`] and + /// [`ContiguousComponentTicksMut::is_added_iter`]). /// /// # Warning /// **Bypasses change detection** From c4bd5ae3909702d5b8dab83a187ce7df12df4083 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:59:21 +0100 Subject: [PATCH 50/52] split reverse/bypassing change detection --- .../bevy_ecs/src/change_detection/params.rs | 58 +++++++++++++++++-- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 753fe424c180d..597cbc25ad4df 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -626,7 +626,8 @@ impl<'w, T> ContiguousRef<'w, T> { self.ticks.this_run } - /// Creates a new `ContiguousRef` using provided values. + /// Creates a new `ContiguousRef` using provided values or returns [`None`] if lengths of + /// `value`, `added`, `changed` and `changed_by` do not match /// /// This is an advanced feature, `ContiguousRef`s are designed to be _created_ by /// engine-internal code and _consumed_ by end-user code. @@ -647,8 +648,14 @@ impl<'w, T> ContiguousRef<'w, T> { last_run: Tick, this_run: Tick, caller: MaybeLocation<&'w [&'static Location<'static>]>, - ) -> Self { - Self { + ) -> Option { + let eq = value.len() == added.len() + && value.len() == changed.len() + && caller + .map(|v| v.len() == value.len()) + .into_option() + .unwrap_or(true); + eq.then_some(Self { value, ticks: ContiguousComponentTicksRef { added, @@ -657,13 +664,22 @@ impl<'w, T> ContiguousRef<'w, T> { last_run, this_run, }, - } + }) } /// Splits [`ContiguousRef`] into it's inner data types. pub fn split(this: Self) -> (&'w [T], ContiguousComponentTicksRef<'w>) { (this.value, this.ticks) } + + /// Reverse of [`ContiguousRef::split`], constructing a [`ContiguousRef`] using components' + /// values and ticks. + /// + /// Returns [`None`] if lengths of `value` and `ticks` do not match, which doesn't happen if + /// `ticks` and `value` come from the same [`Self::split`] call. + pub fn from_parts(value: &'w [T], ticks: ContiguousComponentTicksRef<'w>) -> Option { + (value.len() == ticks.changed.len()).then_some(Self { value, ticks }) + } } impl<'w, T> Deref for ContiguousRef<'w, T> { @@ -899,11 +915,41 @@ impl<'w, T> ContiguousMut<'w, T> { /// [`ContiguousComponentTicksMut::is_changed_iter`] and /// [`ContiguousComponentTicksMut::is_added_iter`]). /// + /// Variant of [`Self::split`] which bypasses change detection: [`Self::bypass_change_detection_split`]. + /// + /// Reverse of [`Self::split`] is [`Self::from_parts`]. + pub fn split(mut this: Self) -> (&'w mut [T], ContiguousComponentTicksMut<'w>) { + this.mark_all_as_changed(); + (this.value, this.ticks) + } + + /// Splits [`ContiguousMut`] into it's inner data types. It may be useful, when you want to + /// have an iterator over component values and check ticks simultaneously (using + /// [`ContiguousComponentTicksMut::is_changed_iter`] and + /// [`ContiguousComponentTicksMut::is_added_iter`]). + /// + /// Variant of [`Self::bypass_change_detection_split`] which **does not** bypass change detection: [`Self::split`]. + /// + /// Reverse of [`Self::bypass_change_detection_split`] is [`Self::from_parts`]. + /// /// # Warning - /// **Bypasses change detection** - pub fn split(this: Self) -> (&'w mut [T], ContiguousComponentTicksMut<'w>) { + /// **Bypasses change detection**, call [`Self::split`] if you don't want to bypass it. + /// + /// See [`Self::bypass_change_detection`] for further explanations. + pub fn bypass_change_detection_split( + this: Self, + ) -> (&'w mut [T], ContiguousComponentTicksMut<'w>) { (this.value, this.ticks) } + + /// Reverse of [`ContiguousMut::split`] and [`ContiguousMut::bypass_change_detection_split`], + /// constructing a [`ContiguousMut`] using components' values and ticks. + /// + /// Returns [`None`] if lengths of `value` and `ticks` do not match, which doesn't happen if + /// `ticks` and `value` come from the same [`Self::split`] or [`Self::bypass_change_detection_split`] call. + pub fn from_parts(value: &'w mut [T], ticks: ContiguousComponentTicksMut<'w>) -> Option { + (value.len() == ticks.changed.len()).then_some(Self { value, ticks }) + } } impl<'w, T> Deref for ContiguousMut<'w, T> { From 6c0c7716fc8b4eb846bd8b116445dbecb967f6e3 Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:14:16 +0100 Subject: [PATCH 51/52] warning doc on split --- crates/bevy_ecs/src/change_detection/params.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 597cbc25ad4df..ad28d9514dcea 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -918,6 +918,11 @@ impl<'w, T> ContiguousMut<'w, T> { /// Variant of [`Self::split`] which bypasses change detection: [`Self::bypass_change_detection_split`]. /// /// Reverse of [`Self::split`] is [`Self::from_parts`]. + /// + /// # Warning + /// This version updates changed ticks **before** returning, hence + /// [`ContiguousComponentTicksMut::is_changed_iter`] will be useless (the iterator will be filled with + /// [`prim@true`]s). pub fn split(mut this: Self) -> (&'w mut [T], ContiguousComponentTicksMut<'w>) { this.mark_all_as_changed(); (this.value, this.ticks) From c8b1548aa9df47c630cd22d2de2d5849dc07614a Mon Sep 17 00:00:00 2001 From: Jenya705 <51133999+Jenya705@users.noreply.github.com> Date: Sat, 24 Jan 2026 12:10:52 +0100 Subject: [PATCH 52/52] ContiguousComponentTicks(Mut|Ref) methods --- .../bevy_ecs/src/change_detection/params.rs | 203 +++++++++++++++--- 1 file changed, 178 insertions(+), 25 deletions(-) diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index ad28d9514dcea..eca923f6af289 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -84,6 +84,64 @@ impl<'w> ContiguousComponentTicksRef<'w> { } } + /// Creates a new `ContiguousComponentTicksRef` using provided values or returns [`None`] if lengths of + /// `added`, `changed` and `changed_by` do not match + /// + /// This is an advanced feature, `ContiguousComponentTicksRef`s are designed to be _created_ by + /// engine-internal code and _consumed_ by end-user code. + /// + /// - `added` - [`Tick`]s that store the tick when the wrapped value was created. + /// - `changed` - [`Tick`]s that store the last time the wrapped value was changed. + /// - `last_run` - A [`Tick`], occurring before `this_run`, which is used + /// as a reference to determine whether the wrapped value is newly added or changed. + /// - `this_run` - A [`Tick`] corresponding to the current point in time -- "now". + /// - `caller` - [`Location`]s that store the location when the wrapper value was changed. + pub fn new( + added: &'w [Tick], + changed: &'w [Tick], + last_run: Tick, + this_run: Tick, + caller: MaybeLocation<&'w [&'static Location<'static>]>, + ) -> Option { + let eq = added.len() == changed.len() + && caller + .map(|v| v.len() == added.len()) + .into_option() + .unwrap_or(true); + eq.then_some(Self { + added, + changed, + changed_by: caller, + last_run, + this_run, + }) + } + + /// Returns added ticks' slice. + pub fn added(&self) -> &'w [Tick] { + self.added + } + + /// Returns changed ticks' slice. + pub fn changed(&self) -> &'w [Tick] { + self.changed + } + + /// Returns changed by locations' slice. + pub fn changed_by(&self) -> MaybeLocation<&[&'static Location<'static>]> { + self.changed_by.as_deref() + } + + /// Returns the tick the system last ran. + pub fn last_run(&self) -> Tick { + self.last_run + } + + /// Returns the tick of the current system's run. + pub fn this_run(&self) -> Tick { + self.this_run + } + /// Returns an iterator where the i-th item corresponds to whether the i-th component was /// marked as changed. If the value equals [`prim@true`], then the component was changed. /// @@ -223,6 +281,80 @@ impl<'w> ContiguousComponentTicksMut<'w> { } } + /// Creates a new `ContiguousComponentTicksMut` using provided values or returns [`None`] if lengths of + /// `added`, `changed` and `changed_by` do not match + /// + /// This is an advanced feature, `ContiguousComponentTicksMut`s are designed to be _created_ by + /// engine-internal code and _consumed_ by end-user code. + /// + /// - `added` - [`Tick`]s that store the tick when the wrapped value was created. + /// - `changed` - [`Tick`]s that store the last time the wrapped value was changed. + /// - `last_run` - A [`Tick`], occurring before `this_run`, which is used + /// as a reference to determine whether the wrapped value is newly added or changed. + /// - `this_run` - A [`Tick`] corresponding to the current point in time -- "now". + /// - `caller` - [`Location`]s that store the location when the wrapper value was changed. + pub fn new( + added: &'w mut [Tick], + changed: &'w mut [Tick], + last_run: Tick, + this_run: Tick, + caller: MaybeLocation<&'w mut [&'static Location<'static>]>, + ) -> Option { + let eq = added.len() == changed.len() + && caller + .as_ref() + .map(|v| v.len() == added.len()) + .into_option() + .unwrap_or(true); + eq.then_some(Self { + added, + changed, + changed_by: caller, + last_run, + this_run, + }) + } + + /// Returns added ticks' slice. + pub fn added(&self) -> &[Tick] { + self.added + } + + /// Returns changed ticks' slice. + pub fn changed(&self) -> &[Tick] { + self.changed + } + + /// Returns changed by locations' slice. + pub fn changed_by(&self) -> MaybeLocation<&[&'static Location<'static>]> { + self.changed_by.as_deref() + } + + /// Returns mutable added ticks' slice. + pub fn added_mut(&mut self) -> &mut [Tick] { + self.added + } + + /// Returns mutable changed ticks' slice. + pub fn changed_mut(&mut self) -> &mut [Tick] { + self.changed + } + + /// Returns mutable changed by locations' slice. + pub fn changed_by_mut(&mut self) -> MaybeLocation<&mut [&'static Location<'static>]> { + self.changed_by.as_deref_mut() + } + + /// Returns the tick the system last ran. + pub fn last_run(&self) -> Tick { + self.last_run + } + + /// Returns the tick of the current system's run. + pub fn this_run(&self) -> Tick { + self.this_run + } + /// Returns an iterator where the i-th item corresponds to whether the i-th component was /// marked as changed. If the value equals [`prim@true`], then the component was changed. /// @@ -291,6 +423,17 @@ impl<'w> ContiguousComponentTicksMut<'w> { *t = this_run; } } + + /// Returns a `ContiguousComponentTicksMut` with a smaller lifetime. + pub fn reborrow(&mut self) -> ContiguousComponentTicksMut<'_> { + ContiguousComponentTicksMut { + added: self.added, + changed: self.changed, + changed_by: self.changed_by.as_deref_mut(), + last_run: self.last_run, + this_run: self.this_run, + } + } } impl<'w> From> for ContiguousComponentTicksRef<'w> { @@ -639,8 +782,6 @@ impl<'w, T> ContiguousRef<'w, T> { /// as a reference to determine whether the wrapped value is newly added or changed. /// - `this_run` - A [`Tick`] corresponding to the current point in time -- "now". /// - `caller` - [`Location`]s that store the location when the wrapper value was changed. - /// - /// See also: [`Ref::new`] pub fn new( value: &'w [T], added: &'w [Tick], @@ -649,22 +790,10 @@ impl<'w, T> ContiguousRef<'w, T> { this_run: Tick, caller: MaybeLocation<&'w [&'static Location<'static>]>, ) -> Option { - let eq = value.len() == added.len() - && value.len() == changed.len() - && caller - .map(|v| v.len() == value.len()) - .into_option() - .unwrap_or(true); - eq.then_some(Self { - value, - ticks: ContiguousComponentTicksRef { - added, - changed, - changed_by: caller, - last_run, - this_run, - }, - }) + (value.len() == added.len()) + .then(|| ContiguousComponentTicksRef::new(added, changed, last_run, this_run, caller)) + .flatten() + .map(|ticks| Self { value, ticks }) } /// Splits [`ContiguousRef`] into it's inner data types. @@ -896,17 +1025,38 @@ impl<'w, T> ContiguousMut<'w, T> { self.ticks.mark_all_as_changed(); } + /// Creates a new `ContiguousMut` using provided values or returns [`None`] if lengths of + /// `value`, `added`, `changed` and `changed_by` do not match + /// + /// This is an advanced feature, `ContiguousMut`s are designed to be _created_ by + /// engine-internal code and _consumed_ by end-user code. + /// + /// - `value` - The values wrapped by `ContiguousMut`. + /// - `added` - [`Tick`]s that store the tick when the wrapped value was created. + /// - `changed` - [`Tick`]s that store the last time the wrapped value was changed. + /// - `last_run` - A [`Tick`], occurring before `this_run`, which is used + /// as a reference to determine whether the wrapped value is newly added or changed. + /// - `this_run` - A [`Tick`] corresponding to the current point in time -- "now". + /// - `caller` - [`Location`]s that store the location when the wrapper value was changed. + pub fn new( + value: &'w mut [T], + added: &'w mut [Tick], + changed: &'w mut [Tick], + last_run: Tick, + this_run: Tick, + caller: MaybeLocation<&'w mut [&'static Location<'static>]>, + ) -> Option { + (value.len() == added.len()) + .then(|| ContiguousComponentTicksMut::new(added, changed, last_run, this_run, caller)) + .flatten() + .map(|ticks| Self { value, ticks }) + } + /// Returns a `ContiguousMut` with a smaller lifetime. pub fn reborrow(&mut self) -> ContiguousMut<'_, T> { ContiguousMut { value: self.value, - ticks: ContiguousComponentTicksMut { - added: self.ticks.added, - changed: self.ticks.changed, - changed_by: self.ticks.changed_by.as_deref_mut(), - last_run: self.ticks.last_run, - this_run: self.ticks.this_run, - }, + ticks: self.ticks.reborrow(), } } @@ -923,6 +1073,9 @@ impl<'w, T> ContiguousMut<'w, T> { /// This version updates changed ticks **before** returning, hence /// [`ContiguousComponentTicksMut::is_changed_iter`] will be useless (the iterator will be filled with /// [`prim@true`]s). + // NOTE: `ticks_since_insert` will be 0 (because `this.mark_all_as_changed` makes all changed ticks `this_run`), + // `ticks_since_system` won't be 0, `tick` is newer if + // `ticks_since_system` > `ticks_since_insert`, hence it will always be true. pub fn split(mut this: Self) -> (&'w mut [T], ContiguousComponentTicksMut<'w>) { this.mark_all_as_changed(); (this.value, this.ticks)