From cfe55be63c0fcf65396f86676edfdc71f2c1259c Mon Sep 17 00:00:00 2001 From: janis Date: Mon, 6 Oct 2025 14:06:52 +0200 Subject: [PATCH 01/10] add swap functions to table --- crates/bevy_ecs/src/storage/blob_array.rs | 24 +++++++-- crates/bevy_ecs/src/storage/table/column.rs | 17 ++++++ crates/bevy_ecs/src/storage/table/mod.rs | 52 ++++++++++++++++++- crates/bevy_ecs/src/storage/thin_array_ptr.rs | 18 +++++++ 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index 3ef2e5a90d007..76bf0fc02b429 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -403,7 +403,7 @@ impl BlobArray { self.get_unchecked_mut(index_to_keep).promote() } - /// The same as [`Self::swap_remove_unchecked`] but the two elements must non-overlapping. + /// This method will swap two elements in the array. /// /// # Safety /// - `index_to_keep` must be safe to access (within the bounds of the length of the array). @@ -413,11 +413,11 @@ impl BlobArray { /// 1) initialize a different value in `index_to_keep` /// 2) update the saved length of the array if `index_to_keep` was the last element. #[inline] - pub unsafe fn swap_remove_unchecked_nonoverlapping( + pub unsafe fn swap_unchecked_nonoverlapping( &mut self, index_to_remove: usize, index_to_keep: usize, - ) -> OwningPtr<'_> { + ) { #[cfg(debug_assertions)] { debug_assert!(self.capacity > index_to_keep); @@ -430,6 +430,24 @@ impl BlobArray { self.get_unchecked_mut(index_to_remove).as_ptr(), self.item_layout.size(), ); + } + + /// The same as [`Self::swap_remove_unchecked`] but the two elements must non-overlapping. + /// + /// # Safety + /// - `index_to_keep` must be safe to access (within the bounds of the length of the array). + /// - `index_to_remove` must be safe to access (within the bounds of the length of the array). + /// - `index_to_remove` != `index_to_keep` + /// - The caller should address the inconsistent state of the array that has occurred after the swap, either: + /// 1) initialize a different value in `index_to_keep` + /// 2) update the saved length of the array if `index_to_keep` was the last element. + #[inline] + pub unsafe fn swap_remove_unchecked_nonoverlapping( + &mut self, + index_to_remove: usize, + index_to_keep: usize, + ) -> OwningPtr<'_> { + self.swap_unchecked_nonoverlapping(index_to_remove, index_to_keep); // Now the element that used to be in index `index_to_remove` is now in index `index_to_keep` (after swap) // If we are storing ZSTs than the index doesn't actually matter because the size is 0. self.get_unchecked_mut(index_to_keep).promote() diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index 7211d04cae8ba..a4db5f5ec0304 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -43,6 +43,23 @@ impl Column { } } + /// Swap the two elements. + /// + /// # Safety + /// - `a.as_usize()` < `len` + /// - `b.as_usize()` < `len` + /// - `a` != `b` + pub(crate) unsafe fn swap_unchecked(&mut self, a: TableRow, b: TableRow) { + let a = a.index(); + let b = b.index(); + self.data.swap_unchecked_nonoverlapping(a, b); + self.added_ticks.swap_unchecked_nonoverlapping(a, b); + self.changed_ticks.swap_unchecked_nonoverlapping(a, b); + self.changed_by.as_mut().map(|changed_by| { + changed_by.swap_unchecked_nonoverlapping(a, b); + }); + } + /// Swap-remove and drop the removed element, but the component at `row` must not be the last element. /// /// # Safety diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 974583f1f04a0..89eb7ec0016cd 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -12,7 +12,7 @@ pub use column::*; use core::{ cell::UnsafeCell, num::NonZeroUsize, - ops::{Index, IndexMut}, + ops::{Index, IndexMut, Range}, panic::Location, }; use nonmax::NonMaxU32; @@ -166,6 +166,7 @@ impl TableBuilder { Table { columns: self.columns.into_immutable(), entities: self.entities, + disabled_entities: 0, } } } @@ -190,13 +191,27 @@ impl TableBuilder { pub struct Table { columns: ImmutableSparseSet, entities: Vec, + disabled_entities: u32, } impl Table { /// Fetches a read-only slice of the entities stored within the [`Table`]. #[inline] pub fn entities(&self) -> &[Entity] { - &self.entities + &self.entities[self.disabled_entities as usize..] + } + + /// Get the valid table rows (i.e. non-disabled entities). + #[inline] + pub fn table_rows(&self) -> Range { + // SAFETY: + // - No entity row may be in more than one table row at once, so there are no duplicates, + // and there can not be an entity row of u32::MAX. Therefore, this can not be max either. + // - self.disabled_entities <= self.entity_count() + unsafe { + TableRow::new(NonMaxU32::new_unchecked(self.disabled_entities)) + ..TableRow::new(NonMaxU32::new_unchecked(self.entity_count())) + } } /// Get the capacity of this table, in entities. @@ -206,6 +221,39 @@ impl Table { self.entities.capacity() } + /// Disables the entity at the given row, moving it to the back of the table and returns the entities that were swapped (if any) together with their new `TableRow`. + /// + /// # Safety + /// `row` must be in-bounds (`row.as_usize()` < `self.len()`) and not disabled. + pub(crate) unsafe fn swap_disable_unchecked( + &mut self, + row: TableRow, + ) -> Option<((Entity, TableRow), (Entity, TableRow))> { + debug_assert!(row.index_u32() < self.entity_count()); + debug_assert!(row.index_u32() >= self.disabled_entities); + + if row.index_u32() != 0 { + // SAFETY: `self.disabled_entities` is always less than `u32::MAX`, + // as guaranteed by `allocate`. + let other = TableRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) }); + + for col in self.columns.values_mut() { + col.swap_unchecked(row, other); + } + + self.entities.swap(row.index(), other.index()); + self.disabled_entities += 1; + + Some(( + (*self.entities.get_unchecked(other.index()), other), + (*self.entities.get_unchecked(row.index()), row), + )) + } else { + self.disabled_entities += 1; + None + } + } + /// Removes the entity at the given row and returns the entity swapped in to replace it (if an /// entity was swapped in) /// diff --git a/crates/bevy_ecs/src/storage/thin_array_ptr.rs b/crates/bevy_ecs/src/storage/thin_array_ptr.rs index d2b3ec4af5a9a..d7d6a9c7a4613 100644 --- a/crates/bevy_ecs/src/storage/thin_array_ptr.rs +++ b/crates/bevy_ecs/src/storage/thin_array_ptr.rs @@ -170,6 +170,24 @@ impl ThinArrayPtr { } } + /// Perform a [`swap`](https://doc.rust-lang.org/std/primitive.slice.html#method.swap). + /// + /// # Safety + /// - `a` must be safe to access (within the bounds of the length of the array). + /// - `b` must be safe to access (within the bounds of the length of the array). + /// - `a` != `b` + #[inline] + pub unsafe fn swap_unchecked_nonoverlapping(&mut self, a: usize, b: usize) { + #[cfg(debug_assertions)] + { + debug_assert!(self.capacity > a); + debug_assert!(self.capacity > b); + debug_assert_ne!(a, b); + } + let base_ptr = self.data.as_ptr(); + ptr::copy_nonoverlapping(base_ptr.add(a), base_ptr.add(b), 1); + } + /// Perform a [`swap-remove`](https://doc.rust-lang.org/std/vec/struct.Vec.html#method.swap_remove) and return the removed value. /// /// # Safety From 473b03255a04604fdfdb6f1717666a063b573684 Mon Sep 17 00:00:00 2001 From: janis Date: Mon, 6 Oct 2025 16:25:33 +0200 Subject: [PATCH 02/10] adjust table/archetype entity_count/len calls and indices into tables --- crates/bevy_ecs/src/archetype.rs | 19 +++++- crates/bevy_ecs/src/query/fetch.rs | 24 +++----- crates/bevy_ecs/src/query/iter.rs | 72 +++++++++++------------ crates/bevy_ecs/src/query/par_iter.rs | 2 +- crates/bevy_ecs/src/query/state.rs | 23 ++++---- crates/bevy_ecs/src/storage/blob_array.rs | 51 +++++++++++++++- crates/bevy_ecs/src/storage/table/mod.rs | 59 ++++++++++--------- 7 files changed, 154 insertions(+), 96 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 1885c801e8802..412b5ba2b6689 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -32,7 +32,7 @@ use alloc::{boxed::Box, vec::Vec}; use bevy_platform::collections::{hash_map::Entry, HashMap}; use core::{ hash::Hash, - ops::{Index, IndexMut, RangeFrom}, + ops::{Index, IndexMut, Range, RangeFrom}, }; use nonmax::NonMaxU32; @@ -394,6 +394,7 @@ pub struct Archetype { table_id: TableId, edges: Edges, entities: Vec, + disabled_entities: u32, components: ImmutableSparseSet, pub(crate) flags: ArchetypeFlags, } @@ -453,6 +454,7 @@ impl Archetype { id, table_id, entities: Vec::new(), + disabled_entities: 0, components: archetype_components.into_immutable(), edges: Default::default(), flags, @@ -485,6 +487,15 @@ impl Archetype { &self.entities } + /// Get the valid table rows (i.e. non-disabled entities). + #[inline] + pub fn archetype_rows(&self) -> Range { + // - No entity row may be in more than one table row at once, so there are no duplicates, + // and there can not be an entity row of u32::MAX. Therefore, this can not be max either. + // - self.disabled_entities <= self.len() + self.disabled_entities..self.len() + } + /// Fetches the entities contained in this archetype. #[inline] pub fn entities_with_location(&self) -> impl Iterator { @@ -650,6 +661,12 @@ impl Archetype { self.entities.len() as u32 } + /// Gets the number of entities that belong to the archetype, without disabled entities. + #[inline] + pub fn entity_count(&self) -> u32 { + self.len() - self.disabled_entities + } + /// Checks if the archetype has any entities. #[inline] pub fn is_empty(&self) -> bool { diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 7b1ff2387f1d4..52390917ef216 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1725,15 +1725,11 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { ) { let column = table.get_column(component_id).debug_checked_unwrap(); let table_data = Some(( - column.get_data_slice(table.entity_count() as usize).into(), + column.get_data_slice(table.len() as usize).into(), + column.get_added_ticks_slice(table.len() as usize).into(), + column.get_changed_ticks_slice(table.len() as usize).into(), column - .get_added_ticks_slice(table.entity_count() as usize) - .into(), - column - .get_changed_ticks_slice(table.entity_count() as usize) - .into(), - column - .get_changed_by_slice(table.entity_count() as usize) + .get_changed_by_slice(table.len() as usize) .map(Into::into), )); // SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table @@ -1931,15 +1927,11 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { ) { let column = table.get_column(component_id).debug_checked_unwrap(); let table_data = Some(( - column.get_data_slice(table.entity_count() as usize).into(), - column - .get_added_ticks_slice(table.entity_count() as usize) - .into(), - column - .get_changed_ticks_slice(table.entity_count() as usize) - .into(), + column.get_data_slice(table.len() as usize).into(), + column.get_added_ticks_slice(table.len() as usize).into(), + column.get_changed_ticks_slice(table.len() as usize).into(), column - .get_changed_by_slice(table.entity_count() as usize) + .get_changed_by_slice(table.len() as usize) .map(Into::into), )); // SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index cab2ee9c9391d..c9a5fdd3c2f7f 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -148,7 +148,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // SAFETY: Matched table IDs are guaranteed to still exist. let table = unsafe { self.tables.get(table_id).debug_checked_unwrap() }; - let range = range.unwrap_or(0..table.entity_count()); + let range = range.unwrap_or(table.table_rows()); accum = // SAFETY: // - The fetched table matches both D and F @@ -163,11 +163,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // SAFETY: Matched table IDs are guaranteed to still exist. let table = unsafe { self.tables.get(archetype.table_id()).debug_checked_unwrap() }; - let range = range.unwrap_or(0..archetype.len()); + let range = range.unwrap_or(archetype.archetype_rows()); // When an archetype and its table have equal entity counts, dense iteration can be safely used. // this leverages cache locality to optimize performance. - if table.entity_count() == archetype.len() { + if table.entity_count() == archetype.entity_count() { accum = // SAFETY: // - The fetched archetype matches both D and F @@ -345,7 +345,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } let table = self.tables.get(archetype.table_id()).debug_checked_unwrap(); debug_assert!( - archetype.len() == table.entity_count(), + archetype.entity_count() == table.entity_count(), "archetype and its table must have the same length. " ); @@ -930,14 +930,14 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> { let mut accum = init; // Empty any remaining uniterated values from the current table/archetype - while self.cursor.current_row != self.cursor.current_len { + while !self.cursor.current_range.is_empty() { let Some(item) = self.next() else { break }; accum = func(accum, item); } for id in self.cursor.storage_id_iter.clone().copied() { // SAFETY: - // - The range(None) is equivalent to [0, storage.entity_count) + // - The range(None) is equivalent to storage.rows accum = unsafe { self.fold_over_storage_range(accum, &mut func, id, None) }; } accum @@ -2365,10 +2365,8 @@ struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { archetype_entities: &'w [ArchetypeEntity], fetch: D::Fetch<'w>, filter: F::Fetch<'w>, - // length of the table or length of the archetype, depending on whether both `D`'s and `F`'s fetches are dense - current_len: u32, - // either table row or archetype index, depending on whether both `D`'s and `F`'s fetches are dense - current_row: u32, + // remaining range of the table or archetype being iterated over, depending on whether both `D`'s and `F`'s fetches are dense + current_range: Range, } impl Clone for QueryIterationCursor<'_, '_, D, F> { @@ -2380,8 +2378,7 @@ impl Clone for QueryIterationCursor<'_, '_, D, F> archetype_entities: self.archetype_entities, fetch: self.fetch.clone(), filter: self.filter.clone(), - current_len: self.current_len, - current_row: self.current_row, + current_range: self.current_range.clone(), } } } @@ -2420,8 +2417,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { archetype_entities: &[], storage_id_iter: query_state.matched_storage_ids.iter(), is_dense: query_state.is_dense, - current_len: 0, - current_row: 0, + current_range: 0..0, } } @@ -2433,8 +2429,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { table_entities: self.table_entities, archetype_entities: self.archetype_entities, storage_id_iter: self.storage_id_iter.clone(), - current_len: self.current_len, - current_row: self.current_row, + current_range: 0..0, } } @@ -2445,8 +2440,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// dropped to prevent aliasing mutable references. #[inline] unsafe fn peek_last(&mut self, query_state: &'s QueryState) -> Option> { - if self.current_row > 0 { - let index = self.current_row - 1; + if self.current_range.start > 0 { + let index = self.current_range.start - 1; if self.is_dense { // SAFETY: This must have been called previously in `next` as `current_row > 0` let entity = unsafe { self.table_entities.get_unchecked(index as usize) }; @@ -2494,9 +2489,12 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { unsafe { ids.map(|id| tables[id.table_id].entity_count()).sum() } } else { // SAFETY: The if check ensures that storage_id_iter stores ArchetypeIds - unsafe { ids.map(|id| archetypes[id.archetype_id].len()).sum() } + unsafe { + ids.map(|id| archetypes[id.archetype_id].entity_count()) + .sum() + } }; - remaining_matched + self.current_len - self.current_row + remaining_matched + self.current_range.len() as u32 } // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: @@ -2517,7 +2515,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { if self.is_dense { 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 { + let Some(next) = self.current_range.next() else { let table_id = self.storage_id_iter.next()?.table_id; let table = tables.get(table_id).debug_checked_unwrap(); if table.is_empty() { @@ -2530,18 +2528,17 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { 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; - } + self.current_range = table.table_rows(); + + continue; + }; // SAFETY: set_table was called prior. // `current_row` is a table row in range of the current table, because if it was not, then the above would have been executed. - let entity = - unsafe { self.table_entities.get_unchecked(self.current_row as usize) }; + let entity = unsafe { self.table_entities.get_unchecked(next as usize) }; // SAFETY: The row is less than the u32 len, so it must not be max. - let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(self.current_row)) }; + let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(next)) }; if !F::filter_fetch(&query_state.filter_state, &mut self.filter, *entity, row) { - self.current_row += 1; continue; } @@ -2553,12 +2550,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { let item = unsafe { D::fetch(&query_state.fetch_state, &mut self.fetch, *entity, row) }; - self.current_row += 1; return Some(item); } } else { loop { - if self.current_row == self.current_len { + let Some(next) = self.current_range.next() else { let archetype_id = self.storage_id_iter.next()?.archetype_id; let archetype = archetypes.get(archetype_id).debug_checked_unwrap(); if archetype.is_empty() { @@ -2582,23 +2578,21 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { ); } self.archetype_entities = archetype.entities(); - self.current_len = archetype.len(); - self.current_row = 0; - } + self.current_range = archetype.archetype_rows(); + + continue; + }; // SAFETY: set_archetype was called prior. // `current_row` is an archetype index row in range of the current archetype, because if it was not, then the if above would have been executed. - let archetype_entity = unsafe { - self.archetype_entities - .get_unchecked(self.current_row as usize) - }; + let archetype_entity = + unsafe { self.archetype_entities.get_unchecked(next as usize) }; if !F::filter_fetch( &query_state.filter_state, &mut self.filter, archetype_entity.id(), archetype_entity.table_row(), ) { - self.current_row += 1; continue; } @@ -2615,7 +2609,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { archetype_entity.table_row(), ) }; - self.current_row += 1; + return Some(item); } } diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index b8d8618fa5bf1..f100b59d4ea9e 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -144,7 +144,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { let archetypes = &self.world.archetypes(); id_iter // SAFETY: The if check ensures that matched_storage_ids stores ArchetypeIds - .map(|id| unsafe { archetypes[id.archetype_id].len() }) + .map(|id| unsafe { archetypes[id.archetype_id].entity_count() }) .max() } .map(|v| v as usize) diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 09821a718c668..4d79f8e966d2b 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1417,6 +1417,8 @@ impl QueryState { bevy_tasks::ComputeTaskPool::get().scope(|scope| { // SAFETY: We only access table data that has been registered in `self.component_access`. + + use core::ops::Range; let tables = unsafe { &world.storages().tables }; let archetypes = world.archetypes(); let mut batch_queue = ArrayVec::new(); @@ -1444,8 +1446,9 @@ impl QueryState { }; // submit single storage larger than batch_size - let submit_single = |count, storage_id: StorageId| { - for offset in (0..count).step_by(batch_size as usize) { + let submit_single = |range: Range, storage_id: StorageId| { + let count = range.len() as u32; + for offset in range.step_by(batch_size as usize) { let mut func = func.clone(); let init_accum = init_accum.clone(); let len = batch_size.min(count - offset); @@ -1461,29 +1464,29 @@ impl QueryState { } }; - let storage_entity_count = |storage_id: StorageId| -> u32 { + let storage_entity_rows = |storage_id: StorageId| -> Range { if self.is_dense { - tables[storage_id.table_id].entity_count() + tables[storage_id.table_id].table_rows() } else { - archetypes[storage_id.archetype_id].len() + archetypes[storage_id.archetype_id].archetype_rows() } }; for storage_id in &self.matched_storage_ids { - let count = storage_entity_count(*storage_id); + let range = storage_entity_rows(*storage_id); // skip empty storage - if count == 0 { + if range.is_empty() { continue; } // immediately submit large storage - if count >= batch_size { - submit_single(count, *storage_id); + if range.len() as u32 >= batch_size { + submit_single(range, *storage_id); continue; } // merge small storage batch_queue.push(*storage_id); - queue_entity_count += count; + queue_entity_count += range.len() as u32; // submit batch_queue if queue_entity_count >= batch_size || batch_queue.is_full() { diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index 76bf0fc02b429..5382956281bfa 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -1,7 +1,13 @@ use alloc::alloc::handle_alloc_error; use bevy_ptr::{OwningPtr, Ptr, PtrMut}; use bevy_utils::OnDrop; -use core::{alloc::Layout, cell::UnsafeCell, num::NonZeroUsize, ptr::NonNull}; +use core::{ + alloc::Layout, + cell::UnsafeCell, + num::NonZeroUsize, + ops::{Bound, Range, RangeBounds}, + ptr::NonNull, +}; /// A flat, type-erased data storage type. /// @@ -188,6 +194,49 @@ impl BlobArray { } } + /// Clears the array, i.e. removing (and dropping) all of the elements in the range. + /// Note that this method has no effect on the allocated capacity of the vector. + /// + /// Note that this method will behave exactly the same as [`Vec::drain`]. + /// + /// # Safety + /// - For every element with index `i`, if `range.contains(i)`: It must be safe to call [`Self::get_unchecked_mut`] with `i`. + /// (If the safety requirements of every method that has been used on `Self` have been fulfilled, the caller just needs to ensure that `range` is correct.) + /// + /// [`Vec::drain`]: alloc::vec::Vec::drain + pub unsafe fn clear_range(&mut self, range: impl RangeBounds) { + let map_bound_or = |bounds: Bound<&usize>, or: usize, start: bool| match (bounds, start) { + (Bound::Included(&b), true) => b, + (Bound::Included(&b), false) => b.checked_add(1).expect("range end overflowed"), + (Bound::Excluded(&b), true) => b.checked_add(1).expect("range start overflowed"), + (Bound::Excluded(&b), false) => b, + (Bound::Unbounded, _) => or, + }; + + let range = map_bound_or(range.start_bound(), 0, true) + ..map_bound_or(range.end_bound(), self.capacity, false); + + #[cfg(debug_assertions)] + debug_assert!(self.capacity >= range.end); + if let Some(drop) = self.drop { + // We set `self.drop` to `None` before dropping elements for unwind safety. This ensures we don't + // accidentally drop elements twice in the event of a drop impl panicking. + self.drop = None; + let size = self.item_layout.size(); + for i in range { + // SAFETY: + // * 0 <= `i` < `len`, so `i * size` must be in bounds for the allocation. + // * `size` is a multiple of the erased type's alignment, + // so adding a multiple of `size` will preserve alignment. + // * The item is left unreachable so it can be safely promoted to an `OwningPtr`. + let item = unsafe { self.get_ptr_mut().byte_add(i * size).promote() }; + // SAFETY: `item` was obtained from this `BlobArray`, so its underlying type must match `drop`. + unsafe { drop(item) }; + } + self.drop = Some(drop); + } + } + /// Because this method needs parameters, it can't be the implementation of the `Drop` trait. /// The owner of this [`BlobArray`] must call this method with the correct information. /// diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 89eb7ec0016cd..a0125cee1909a 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -203,15 +203,11 @@ impl Table { /// Get the valid table rows (i.e. non-disabled entities). #[inline] - pub fn table_rows(&self) -> Range { - // SAFETY: + pub fn table_rows(&self) -> Range { // - No entity row may be in more than one table row at once, so there are no duplicates, // and there can not be an entity row of u32::MAX. Therefore, this can not be max either. // - self.disabled_entities <= self.entity_count() - unsafe { - TableRow::new(NonMaxU32::new_unchecked(self.disabled_entities)) - ..TableRow::new(NonMaxU32::new_unchecked(self.entity_count())) - } + self.disabled_entities..self.len() } /// Get the capacity of this table, in entities. @@ -229,7 +225,7 @@ impl Table { &mut self, row: TableRow, ) -> Option<((Entity, TableRow), (Entity, TableRow))> { - debug_assert!(row.index_u32() < self.entity_count()); + debug_assert!(row.index_u32() < self.len()); debug_assert!(row.index_u32() >= self.disabled_entities); if row.index_u32() != 0 { @@ -260,8 +256,8 @@ impl Table { /// # Safety /// `row` must be in-bounds (`row.as_usize()` < `self.len()`) pub(crate) unsafe fn swap_remove_unchecked(&mut self, row: TableRow) -> Option { - debug_assert!(row.index_u32() < self.entity_count()); - let last_element_index = self.entity_count() - 1; + debug_assert!(row.index_u32() < self.len()); + let last_element_index = self.len() - 1; if row.index_u32() != last_element_index { // Instead of checking this condition on every `swap_remove` call, we // check it here and use `swap_remove_nonoverlapping`. @@ -312,8 +308,8 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.index_u32() < self.entity_count()); - let last_element_index = self.entity_count() - 1; + debug_assert!(row.index_u32() < self.len()); + let last_element_index = self.len() - 1; let is_last = row.index_u32() == last_element_index; let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { @@ -355,8 +351,8 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.index_u32() < self.entity_count()); - let last_element_index = self.entity_count() - 1; + debug_assert!(row.index_u32() < self.len()); + let last_element_index = self.len() - 1; let is_last = row.index_u32() == last_element_index; let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { @@ -398,8 +394,8 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.index_u32() < self.entity_count()); - let last_element_index = self.entity_count() - 1; + debug_assert!(row.index_u32() < self.len()); + let last_element_index = self.len() - 1; let is_last = row.index_u32() == last_element_index; let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { @@ -429,7 +425,7 @@ impl Table { component_id: ComponentId, ) -> Option<&[UnsafeCell]> { self.get_column(component_id) - .map(|col| col.get_data_slice(self.entity_count() as usize)) + .map(|col| col.get_data_slice(self.len() as usize)) } /// Get the added ticks of the column matching `component_id` as a slice. @@ -439,7 +435,7 @@ impl Table { ) -> Option<&[UnsafeCell]> { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the ticks array - .map(|col| unsafe { col.get_added_ticks_slice(self.entity_count() as usize) }) + .map(|col| unsafe { col.get_added_ticks_slice(self.len() as usize) }) } /// Get the changed ticks of the column matching `component_id` as a slice. @@ -449,7 +445,7 @@ impl Table { ) -> Option<&[UnsafeCell]> { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the ticks array - .map(|col| unsafe { col.get_changed_ticks_slice(self.entity_count() as usize) }) + .map(|col| unsafe { col.get_changed_ticks_slice(self.len() as usize) }) } /// Fetches the calling locations that last changed the each component @@ -460,7 +456,7 @@ impl Table { MaybeLocation::new_with_flattened(|| { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the locations array - .map(|col| unsafe { col.get_changed_by_slice(self.entity_count() as usize) }) + .map(|col| unsafe { col.get_changed_by_slice(self.len() as usize) }) }) } @@ -470,7 +466,7 @@ impl Table { component_id: ComponentId, row: TableRow, ) -> Option<&UnsafeCell> { - (row.index_u32() < self.entity_count()).then_some( + (row.index_u32() < self.len()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? @@ -486,7 +482,7 @@ impl Table { component_id: ComponentId, row: TableRow, ) -> Option<&UnsafeCell> { - (row.index_u32() < self.entity_count()).then_some( + (row.index_u32() < self.len()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? @@ -503,7 +499,7 @@ impl Table { row: TableRow, ) -> MaybeLocation>>> { MaybeLocation::new_with_flattened(|| { - (row.index_u32() < self.entity_count()).then_some( + (row.index_u32() < self.len()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? @@ -563,7 +559,7 @@ impl Table { /// Reserves `additional` elements worth of capacity within the table. pub(crate) fn reserve(&mut self, additional: usize) { - if (self.capacity() - self.entity_count() as usize) < additional { + if (self.capacity() - self.len() as usize) < additional { let column_cap = self.capacity(); self.entities.reserve(additional); @@ -652,7 +648,7 @@ impl Table { /// The allocated row must be written to immediately with valid values in each column pub(crate) unsafe fn allocate(&mut self, entity: Entity) -> TableRow { self.reserve(1); - let len = self.entity_count(); + let len = self.len(); // SAFETY: No entity row may be in more than one table row at once, so there are no duplicates, // and there can not be an entity row of u32::MAX. Therefore, this can not be max either. let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(len)) }; @@ -674,9 +670,15 @@ impl Table { row } - /// Gets the number of entities currently being stored in the table. + /// Gets the number of entities currently being stored in the table, without disabled entities. #[inline] pub fn entity_count(&self) -> u32 { + self.len() - self.disabled_entities + } + + /// Gets the total number of entities currently being stored in the table. + #[inline] + pub fn len(&self) -> u32 { // No entity may have more than one table row, so there are no duplicates, // and there may only ever be u32::MAX entities, so the length never exceeds u32's capacity. self.entities.len() as u32 @@ -711,7 +713,7 @@ impl Table { /// Call [`Tick::check_tick`] on all of the ticks in the [`Table`] pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { - let len = self.entity_count() as usize; + let len = self.len() as usize; for col in self.columns.values_mut() { // SAFETY: `len` is the actual length of the column unsafe { col.check_change_ticks(len, check) }; @@ -728,7 +730,7 @@ impl Table { /// # Panics /// - Panics if any of the components in any of the columns panics while being dropped. pub(crate) fn clear(&mut self) { - let len = self.entity_count() as usize; + let len = self.len() as usize; // We must clear the entities first, because in the drop function causes a panic, it will result in a double free of the columns. self.entities.clear(); for column in self.columns.values_mut() { @@ -902,12 +904,13 @@ impl IndexMut for Tables { impl Drop for Table { fn drop(&mut self) { - let len = self.entity_count() as usize; + let len = self.len() as usize; let cap = self.capacity(); self.entities.clear(); for col in self.columns.values_mut() { // SAFETY: `cap` and `len` are correct. `col` is never accessed again after this call. unsafe { + // TODO: don't drop disabled entities components col.drop(cap, len); } } From 06a9b627cf270fc9fc57920416d3d37c5a5da568 Mon Sep 17 00:00:00 2001 From: janis Date: Mon, 6 Oct 2025 18:46:47 +0200 Subject: [PATCH 03/10] disabling --- crates/bevy_ecs/src/archetype.rs | 47 ++++++++++++++++ crates/bevy_ecs/src/storage/table/mod.rs | 24 ++++---- crates/bevy_ecs/src/world/entity_ref.rs | 70 ++++++++++++++++++++++++ crates/bevy_ecs/src/world/mod.rs | 9 ++- 4 files changed, 139 insertions(+), 11 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 412b5ba2b6689..c42f7e5bd3751 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -327,6 +327,7 @@ impl Edges { } /// Metadata about an [`Entity`] in a [`Archetype`]. +#[derive(Clone, Copy)] pub struct ArchetypeEntity { entity: Entity, table_row: TableRow, @@ -634,6 +635,52 @@ impl Archetype { self.entities.reserve(additional); } + /// Disables the entity at `row` by swapping it with the first enabled + /// entity. Returns the swapped entities with their respective table and + /// archetype rows, or `None` if no swap occurred. + /// + /// # Panics + /// This function will panic if `row >= self.entities.len()` + #[inline] + pub(crate) fn swap_disable( + &mut self, + row: ArchetypeRow, + ) -> ( + (ArchetypeEntity, ArchetypeRow), + Option<(ArchetypeEntity, ArchetypeRow)>, + ) { + debug_assert!(row.index_u32() < self.len()); + debug_assert!(row.index_u32() >= self.disabled_entities); + + if row.index_u32() == self.disabled_entities { + // no need to swap, just increment the disabled count + self.disabled_entities += 1; + + // SAFETY: `row` is guaranteed to be in-bounds. + ( + (unsafe { *self.entities.get_unchecked(row.index()) }, row), + None, + ) + } else { + // SAFETY: `self.disabled_entities` is always less than `u32::MAX`, as guaranteed by `allocate`. + let disabled_row = + ArchetypeRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) }); + + self.entities.swap(row.index(), disabled_row.index()); + + // SAFETY: Both `row` and `other` are guaranteed to be in-bounds. + unsafe { + ( + ( + *self.entities.get_unchecked(disabled_row.index()), + disabled_row, + ), + Some((*self.entities.get_unchecked(row.index()), row)), + ) + } + } + } + /// Removes the entity at `row` by swapping it out. Returns the table row the entity is stored /// in. /// diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index a0125cee1909a..1cb1b56971a19 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -224,29 +224,33 @@ impl Table { pub(crate) unsafe fn swap_disable_unchecked( &mut self, row: TableRow, - ) -> Option<((Entity, TableRow), (Entity, TableRow))> { + ) -> ((Entity, TableRow), Option<(Entity, TableRow)>) { debug_assert!(row.index_u32() < self.len()); debug_assert!(row.index_u32() >= self.disabled_entities); - if row.index_u32() != 0 { + if row.index_u32() != self.disabled_entities { // SAFETY: `self.disabled_entities` is always less than `u32::MAX`, // as guaranteed by `allocate`. - let other = TableRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) }); + let disabled_row = + TableRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) }); for col in self.columns.values_mut() { - col.swap_unchecked(row, other); + col.swap_unchecked(row, disabled_row); } - self.entities.swap(row.index(), other.index()); + self.entities.swap(row.index(), disabled_row.index()); self.disabled_entities += 1; - Some(( - (*self.entities.get_unchecked(other.index()), other), - (*self.entities.get_unchecked(row.index()), row), - )) + ( + ( + *self.entities.get_unchecked(disabled_row.index()), + disabled_row, + ), + Some((*self.entities.get_unchecked(row.index()), row)), + ) } else { self.disabled_entities += 1; - None + ((*self.entities.get_unchecked(row.index()), row), None) } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 3b003ef86ef1a..a82ad984a7e31 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2562,6 +2562,76 @@ impl<'w> EntityWorldMut<'w> { self.despawn_with_caller(MaybeLocation::caller()); } + /// Disable the current entity. + /// + pub fn disable(self) -> EntityLocation { + let world = self.world; + + let location = world + .entities + .get(self.entity) + .expect("entity should exist at this point."); + + let location = { + let archetype = &mut world.archetypes[location.archetype_id]; + let ((disabled_arch, archetype_row), swapped_archetype) = + archetype.swap_disable(location.archetype_row); + + // set the correct entity location for the swapped entity; the disabled is set to `None` + if let Some((entity, archetype_row)) = swapped_archetype { + let entity = entity.id(); + let swapped_location = world.entities.get(entity).unwrap(); + + // SAFETY: TODO + unsafe { + world.entities.set( + entity.index(), + Some(EntityLocation { + archetype_row, + ..swapped_location + }), + ); + } + } + + // SAFETY: TODO + let ((disabled_table, table_row), swapped_table) = unsafe { + world.storages.tables[archetype.table_id()] + .swap_disable_unchecked(disabled_arch.table_row()) + }; + + if let Some((entity, table_row)) = swapped_table { + let swapped_location = world.entities.get(entity).unwrap(); + + // SAFETY: TODO + unsafe { + world.entities.set( + entity.index(), + Some(EntityLocation { + table_row, + ..swapped_location + }), + ); + } + } + + assert_eq!(disabled_table, disabled_arch.id()); + + EntityLocation { + archetype_row, + table_row, + ..location + } + }; + + // SAFETY: TODO + unsafe { + world.entities.set(self.entity.index(), None); + } + + location + } + pub(crate) fn despawn_with_caller(self, caller: MaybeLocation) { let location = self.location(); let world = self.world; diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index c126efcd8d839..d26e0e7e0bd1b 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -41,7 +41,7 @@ use crate::{ ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, - entity::{Entities, Entity, EntityDoesNotExistError}, + entity::{Entities, Entity, EntityDoesNotExistError, EntityLocation}, entity_disabling::DefaultQueryFilters, error::{DefaultErrorHandler, ErrorHandler}, lifecycle::{ComponentHooks, RemovedComponentMessages, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, @@ -1439,6 +1439,13 @@ impl World { Ok(()) } + pub fn disable(&mut self, entity: Entity) -> Result { + self.flush(); + + let entity = self.get_entity_mut(entity)?; + Ok(entity.disable()) + } + /// Clears the internal component tracker state. /// /// The world maintains some internal state about changed and removed components. This state From 52b5b27ffe89eb489c3946b76740f47364f137b7 Mon Sep 17 00:00:00 2001 From: janis Date: Mon, 6 Oct 2025 23:54:32 +0200 Subject: [PATCH 04/10] component_scope --- crates/bevy_ecs/src/archetype.rs | 44 ++++++++++--- crates/bevy_ecs/src/entity/mod.rs | 7 ++ crates/bevy_ecs/src/query/iter.rs | 40 +++++------- crates/bevy_ecs/src/storage/table/mod.rs | 34 +++++++--- crates/bevy_ecs/src/world/entity_ref.rs | 11 ++-- crates/bevy_ecs/src/world/mod.rs | 82 +++++++++++++++++++++++- 6 files changed, 172 insertions(+), 46 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index c42f7e5bd3751..9dee98223ba6d 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -395,7 +395,7 @@ pub struct Archetype { table_id: TableId, edges: Edges, entities: Vec, - disabled_entities: u32, + pub(crate) disabled_entities: u32, components: ImmutableSparseSet, pub(crate) flags: ArchetypeFlags, } @@ -488,6 +488,12 @@ impl Archetype { &self.entities } + /// Fetches the disabled entities contained in this archetype. + #[inline] + pub fn disabled_entities(&self) -> &[ArchetypeEntity] { + &self.entities[..self.disabled_entities as usize] + } + /// Get the valid table rows (i.e. non-disabled entities). #[inline] pub fn archetype_rows(&self) -> Range { @@ -499,7 +505,10 @@ impl Archetype { /// Fetches the entities contained in this archetype. #[inline] - pub fn entities_with_location(&self) -> impl Iterator { + pub fn entities_with_location( + &self, + ) -> impl Iterator + DoubleEndedIterator + ExactSizeIterator + { self.entities.iter().enumerate().map( |(archetype_row, &ArchetypeEntity { entity, table_row })| { ( @@ -635,9 +644,11 @@ impl Archetype { self.entities.reserve(additional); } - /// Disables the entity at `row` by swapping it with the first enabled - /// entity. Returns the swapped entities with their respective table and - /// archetype rows, or `None` if no swap occurred. + /// Disables or enables the entity at `row` by swapping it with the first + /// enabled or disabled entity. Returns the swapped entities with their + /// respective table and archetype rows, or `None` if no swap occurred. If a + /// swap occurred, the caller is responsible for updating the entity's + /// location. /// /// # Panics /// This function will panic if `row >= self.entities.len()` @@ -650,12 +661,29 @@ impl Archetype { Option<(ArchetypeEntity, ArchetypeRow)>, ) { debug_assert!(row.index_u32() < self.len()); - debug_assert!(row.index_u32() >= self.disabled_entities); - if row.index_u32() == self.disabled_entities { - // no need to swap, just increment the disabled count + let disabled_row = if row.index_u32() >= self.disabled_entities { + // the entity is currently enabled, swap it with the first enabled entity: + + // SAFETY: `self.disabled_entities` is always less than `u32::MAX`, + // as guaranteed by `allocate`. + let disabled_row = + ArchetypeRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) }); self.disabled_entities += 1; + disabled_row + } else { + self.disabled_entities -= 1; + // the entity is currently disabled, swap it with the last disabled entity: + + // SAFETY: `self.disabled_entities` is always less than `u32::MAX`, + // as guaranteed by `allocate`. + ArchetypeRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) }) + }; + + if row == disabled_row { + // the entity is already in the correct position, no swap needed + // SAFETY: `row` is guaranteed to be in-bounds. ( (unsafe { *self.entities.get_unchecked(row.index()) }, row), diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 7611a8dd6b8a5..05656cdb80bed 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -1318,6 +1318,13 @@ pub struct EntityLocation { /// It is also useful for reserving an id; commands will often allocate an `Entity` but not provide it a location until the command is applied. pub type EntityIdLocation = Option; +/// An [`Entity`] that has been disabled. This entity will not be found by queries or accessible via other ECS operations, but it's components are still stored in the ECS. +/// This is useful for temporarily disabling an entity without fully despawning it or invoking archetype moves. This entity keeps track of its location, so it can be re-enabled later. +pub struct DisabledEntity { + pub entity: Entity, + pub location: EntityLocation, +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index c9a5fdd3c2f7f..92e8f073d4492 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -2746,19 +2746,17 @@ mod tests { let mut query = world.query::<&A>(); let mut iter = query.iter(&world); println!( - "archetype_entities: {} table_entities: {} current_len: {} current_row: {}", + "archetype_entities: {} table_entities: {} current_range: {:?}", iter.cursor.archetype_entities.len(), iter.cursor.table_entities.len(), - iter.cursor.current_len, - iter.cursor.current_row + iter.cursor.current_range, ); _ = iter.next(); println!( - "archetype_entities: {} table_entities: {} current_len: {} current_row: {}", + "archetype_entities: {} table_entities: {} current_range: {:?}", iter.cursor.archetype_entities.len(), iter.cursor.table_entities.len(), - iter.cursor.current_len, - iter.cursor.current_row + iter.cursor.current_range, ); println!("{}", iter.sort::().len()); } @@ -2776,19 +2774,17 @@ mod tests { let mut query = world.query::<&Sparse>(); let mut iter = query.iter(&world); println!( - "before_next_call: archetype_entities: {} table_entities: {} current_len: {} current_row: {}", + "before_next_call: archetype_entities: {} table_entities: {} current_range: {:?}", iter.cursor.archetype_entities.len(), iter.cursor.table_entities.len(), - iter.cursor.current_len, - iter.cursor.current_row + iter.cursor.current_range, ); _ = iter.next(); println!( - "after_next_call: archetype_entities: {} table_entities: {} current_len: {} current_row: {}", + "after_next_call: archetype_entities: {} table_entities: {} current_range: {:?}", iter.cursor.archetype_entities.len(), iter.cursor.table_entities.len(), - iter.cursor.current_len, - iter.cursor.current_row + iter.cursor.current_range, ); println!("{}", iter.sort::().len()); } @@ -2801,19 +2797,17 @@ mod tests { let mut query = world.query::<(&A, &Sparse)>(); let mut iter = query.iter(&world); println!( - "before_next_call: archetype_entities: {} table_entities: {} current_len: {} current_row: {}", + "before_next_call: archetype_entities: {} table_entities: {} current_range: {:?}", iter.cursor.archetype_entities.len(), iter.cursor.table_entities.len(), - iter.cursor.current_len, - iter.cursor.current_row + iter.cursor.current_range, ); _ = iter.next(); println!( - "after_next_call: archetype_entities: {} table_entities: {} current_len: {} current_row: {}", + "before_next_call: archetype_entities: {} table_entities: {} current_range: {:?}", iter.cursor.archetype_entities.len(), iter.cursor.table_entities.len(), - iter.cursor.current_len, - iter.cursor.current_row + iter.cursor.current_range, ); println!("{}", iter.sort::().len()); } @@ -2829,20 +2823,18 @@ mod tests { let mut query = world.query::<(&A, &Sparse)>(); let mut iter = query.iter(&world); println!( - "before_next_call: archetype_entities: {} table_entities: {} current_len: {} current_row: {}", + "before_next_call: archetype_entities: {} table_entities: {} current_range: {:?}", iter.cursor.archetype_entities.len(), iter.cursor.table_entities.len(), - iter.cursor.current_len, - iter.cursor.current_row + iter.cursor.current_range, ); assert!(iter.cursor.table_entities.len() | iter.cursor.archetype_entities.len() == 0); _ = iter.next(); println!( - "after_next_call: archetype_entities: {} table_entities: {} current_len: {} current_row: {}", + "before_next_call: archetype_entities: {} table_entities: {} current_range: {:?}", iter.cursor.archetype_entities.len(), iter.cursor.table_entities.len(), - iter.cursor.current_len, - iter.cursor.current_row + iter.cursor.current_range, ); assert!( ( diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 1cb1b56971a19..bec42ea6011b1 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -198,7 +198,7 @@ impl Table { /// Fetches a read-only slice of the entities stored within the [`Table`]. #[inline] pub fn entities(&self) -> &[Entity] { - &self.entities[self.disabled_entities as usize..] + &self.entities } /// Get the valid table rows (i.e. non-disabled entities). @@ -217,7 +217,11 @@ impl Table { self.entities.capacity() } - /// Disables the entity at the given row, moving it to the back of the table and returns the entities that were swapped (if any) together with their new `TableRow`. + /// Disables or enables the entity at `row` by swapping it with the first + /// enabled or disabled entity. Returns the swapped entities with their + /// respective table rows, or `None` if no swap occurred. If a + /// swap occurred, the caller is responsible for updating the entity's + /// location. /// /// # Safety /// `row` must be in-bounds (`row.as_usize()` < `self.len()`) and not disabled. @@ -226,20 +230,37 @@ impl Table { row: TableRow, ) -> ((Entity, TableRow), Option<(Entity, TableRow)>) { debug_assert!(row.index_u32() < self.len()); - debug_assert!(row.index_u32() >= self.disabled_entities); - if row.index_u32() != self.disabled_entities { + let disabled_row = if row.index_u32() >= self.disabled_entities { + // the entity is currently enabled, swap it with the first enabled entity: + // SAFETY: `self.disabled_entities` is always less than `u32::MAX`, // as guaranteed by `allocate`. let disabled_row = TableRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) }); + self.disabled_entities += 1; + + disabled_row + } else { + self.disabled_entities -= 1; + // the entity is currently disabled, swap it with the last disabled entity: + + // SAFETY: `self.disabled_entities` is always less than `u32::MAX`, + // as guaranteed by `allocate`. + TableRow::new(unsafe { NonMaxU32::new_unchecked(self.disabled_entities) }) + }; + if row == disabled_row { + // the entity is already in the correct position, no swap needed + + // SAFETY: TODO + ((*self.entities.get_unchecked(row.index()), row), None) + } else { for col in self.columns.values_mut() { col.swap_unchecked(row, disabled_row); } self.entities.swap(row.index(), disabled_row.index()); - self.disabled_entities += 1; ( ( @@ -248,9 +269,6 @@ impl Table { ), Some((*self.entities.get_unchecked(row.index()), row)), ) - } else { - self.disabled_entities += 1; - ((*self.entities.get_unchecked(row.index()), row), None) } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index a82ad984a7e31..dbcc7e271e67d 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -6,8 +6,8 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::{Component, ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick}, entity::{ - ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, - EntityIdLocation, EntityLocation, OptIn, OptOut, + ContainsEntity, DisabledEntity, Entity, EntityCloner, EntityClonerBuilder, + EntityEquivalent, EntityIdLocation, EntityLocation, OptIn, OptOut, }, event::{EntityComponentsTrigger, EntityEvent}, lifecycle::{Despawn, Remove, Replace, DESPAWN, REMOVE, REPLACE}, @@ -2564,7 +2564,7 @@ impl<'w> EntityWorldMut<'w> { /// Disable the current entity. /// - pub fn disable(self) -> EntityLocation { + pub fn disable(self) -> DisabledEntity { let world = self.world; let location = world @@ -2629,7 +2629,10 @@ impl<'w> EntityWorldMut<'w> { world.entities.set(self.entity.index(), None); } - location + DisabledEntity { + entity: self.entity, + location, + } } pub(crate) fn despawn_with_caller(self, caller: MaybeLocation) { diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index d26e0e7e0bd1b..675080879c172 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -41,7 +41,7 @@ use crate::{ ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, - entity::{Entities, Entity, EntityDoesNotExistError, EntityLocation}, + entity::{DisabledEntity, Entities, Entity, EntityDoesNotExistError}, entity_disabling::DefaultQueryFilters, error::{DefaultErrorHandler, ErrorHandler}, lifecycle::{ComponentHooks, RemovedComponentMessages, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, @@ -1439,13 +1439,91 @@ impl World { Ok(()) } - pub fn disable(&mut self, entity: Entity) -> Result { + /// Disables the given `entity`, returning a [`DisabledEntity`] if the + /// entity exists and was enabled. + pub fn disable(&mut self, entity: Entity) -> Result { self.flush(); let entity = self.get_entity_mut(entity)?; Ok(entity.disable()) } + /// Re-enables a previously disabled entity, returning an [`EntityWorldMut`] + /// to it. + pub fn enable(&mut self, disabled: DisabledEntity) -> EntityWorldMut<'_> { + self.flush(); + + let archetype = &self.archetypes[disabled.location.archetype_id]; + // Find the location of the disabled entity in the archetype by + // searching backwards through the disabled entities. + // The disabled entity is most likely the last disabled entity, as + // is the case when disabled for `entity_scope`. + let location = archetype + .entities_with_location() + .take(archetype.disabled_entities as usize) + .rev() + .find(|(e, _)| *e == disabled.entity) + .map(|(_, location)| location); + + // SAFETY: disabled entity existed when it was disabled, and wasn't + // despawned in the meantime. + unsafe { + self.entities.set(disabled.entity.index(), location); + } + + self.entity_mut(disabled.entity) + } + + pub fn component_scope< + C: Component, + R, + F: FnOnce(Entity, &mut C, &mut World) -> R, + >( + &mut self, + entity: Entity, + f: F, + ) { + self.try_component_scope(entity, f).unwrap_or_else(|| { + panic!( + "component {} does not exist for entity {entity}", + DebugName::type_name::() + ) + }); + } + + pub fn try_component_scope< + C: Component, + R, + F: FnOnce(Entity, &mut C, &mut World) -> R, + >( + &mut self, + entity: Entity, + f: F, + ) -> Option { + self.flush(); + + let mut entity_mut = self.get_entity_mut(entity).ok()?; + let mut component = { + let component = entity_mut.get_mut::()?; + // SAFETY: TODO + unsafe { core::ptr::read::(&raw const *component) } + }; + + let disabled = entity_mut.disable(); + + let out = f(entity, &mut component, self); + + let mut entity_mut = self.enable(disabled); + + let mut component_mut = entity_mut.get_mut::().unwrap(); + // SAFETY: TODO + unsafe { + core::ptr::write::(&raw mut *component_mut, component); + } + + Some(out) + } + /// Clears the internal component tracker state. /// /// The world maintains some internal state about changed and removed components. This state From 820feeaf283099ecac3b21ecbb4605e8e88a8dde Mon Sep 17 00:00:00 2001 From: janis Date: Tue, 7 Oct 2025 00:02:04 +0200 Subject: [PATCH 05/10] docs, match resource_scope param ordering --- crates/bevy_ecs/src/world/mod.rs | 52 ++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 675080879c172..394b02d882e2f 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1474,10 +1474,41 @@ impl World { self.entity_mut(disabled.entity) } + /// Temporarily disables the requested entity from this [`World`], runs + /// custom user code with a mutable reference to the entity's component of + /// type `C`, then re-enables the entity before returning. + /// + /// This enables safe simultaneous mutable access to both a component of an + /// entity and the rest of the [`World`]. + /// + /// # Panics + /// + /// Panics if the resource does not exist. + /// Use [`try_component_scope`](Self::try_component_scope) instead if you want to handle this case. + /// Panics if the world is replaced during the execution of the closure. + /// + /// # Example + /// ``` + /// use bevy_ecs::prelude::*; + /// #[derive(Component)] + /// struct A(u32); + /// #[derive(Component)] + /// struct B(u32); + /// let mut world = World::new(); + /// let scoped_entity = world.spawn(A(1)).id(); + /// let entity = world.spawn(B(1)).id(); + /// + /// world.component_scope(scoped_entity, |world, _, a: &mut A| { + /// assert!(world.get_mut::(scoped_entity).is_none()); + /// let b = world.get_mut::(entity).unwrap(); + /// a.0 += b.0; + /// }); + /// assert_eq!(world.get::(scoped_entity).unwrap().0, 2); + /// ``` pub fn component_scope< C: Component, R, - F: FnOnce(Entity, &mut C, &mut World) -> R, + F: FnOnce(&mut World, Entity, &mut C) -> R, >( &mut self, entity: Entity, @@ -1491,10 +1522,18 @@ impl World { }); } + /// Temporarily disables the requested entity from this [`World`], runs + /// custom user code with a mutable reference to the entity's component of + /// type `C`, then re-enables the entity before returning. + /// + /// This enables safe simultaneous mutable access to both a component of an + /// entity and the rest of the [`World`]. + /// + /// See also [`component_scope`](Self::component_scope). pub fn try_component_scope< C: Component, R, - F: FnOnce(Entity, &mut C, &mut World) -> R, + F: FnOnce(&mut World, Entity, &mut C) -> R, >( &mut self, entity: Entity, @@ -1502,6 +1541,7 @@ impl World { ) -> Option { self.flush(); + let world_id = self.id(); let mut entity_mut = self.get_entity_mut(entity).ok()?; let mut component = { let component = entity_mut.get_mut::()?; @@ -1511,7 +1551,13 @@ impl World { let disabled = entity_mut.disable(); - let out = f(entity, &mut component, self); + let out = f(self, entity, &mut component); + + assert_eq!( + self.id(), + world_id, + "World was replaced during component scope" + ); let mut entity_mut = self.enable(disabled); From cb54e7d0bdff4229866d86bd71f60068ca067971 Mon Sep 17 00:00:00 2001 From: janis Date: Tue, 7 Oct 2025 00:41:29 +0200 Subject: [PATCH 06/10] skip disabled entities in scenes --- crates/bevy_ecs/src/archetype.rs | 9 ++++----- crates/bevy_scene/src/dynamic_scene.rs | 2 +- crates/bevy_scene/src/scene.rs | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 9dee98223ba6d..8a80c096fbefe 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -488,10 +488,10 @@ impl Archetype { &self.entities } - /// Fetches the disabled entities contained in this archetype. + /// Fetches the enabled entities contained in this archetype. #[inline] - pub fn disabled_entities(&self) -> &[ArchetypeEntity] { - &self.entities[..self.disabled_entities as usize] + pub fn enabled_entities(&self) -> &[ArchetypeEntity] { + &self.entities[self.disabled_entities as usize..] } /// Get the valid table rows (i.e. non-disabled entities). @@ -507,8 +507,7 @@ impl Archetype { #[inline] pub fn entities_with_location( &self, - ) -> impl Iterator + DoubleEndedIterator + ExactSizeIterator - { + ) -> impl DoubleEndedIterator + ExactSizeIterator { self.entities.iter().enumerate().map( |(archetype_row, &ArchetypeEntity { entity, table_row })| { ( diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index f9ef1616ee4e6..00e1e75902fba 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -60,7 +60,7 @@ impl DynamicScene { world .archetypes() .iter() - .flat_map(bevy_ecs::archetype::Archetype::entities) + .flat_map(bevy_ecs::archetype::Archetype::enabled_entities) .map(bevy_ecs::archetype::ArchetypeEntity::id), ) .extract_resources() diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index db92bdc468431..53798ad4b7349 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -106,7 +106,7 @@ impl Scene { // Ensure that all scene entities have been allocated in the destination // world before handling components that may contain references that need mapping. for archetype in self.world.archetypes().iter() { - for scene_entity in archetype.entities() { + for scene_entity in archetype.enabled_entities() { entity_map .entry(scene_entity.id()) .or_insert_with(|| world.spawn_empty().id()); @@ -114,7 +114,7 @@ impl Scene { } for archetype in self.world.archetypes().iter() { - for scene_entity in archetype.entities() { + for scene_entity in archetype.enabled_entities() { let entity = *entity_map .get(&scene_entity.id()) .expect("should have previously spawned an entity"); From 609de312e6134384932c3a76cd58bccf37d78a35 Mon Sep 17 00:00:00 2001 From: janis Date: Tue, 7 Oct 2025 01:28:13 +0200 Subject: [PATCH 07/10] ci fix to run tests, come back to this commit in the future --- crates/bevy_ecs/src/entity/mod.rs | 3 ++ crates/bevy_ecs/src/storage/blob_array.rs | 38 ++------------------- crates/bevy_ecs/src/storage/table/column.rs | 10 +++--- crates/bevy_ecs/src/storage/table/mod.rs | 2 +- 4 files changed, 13 insertions(+), 40 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 05656cdb80bed..430f65818e491 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -1321,7 +1321,10 @@ pub type EntityIdLocation = Option; /// An [`Entity`] that has been disabled. This entity will not be found by queries or accessible via other ECS operations, but it's components are still stored in the ECS. /// This is useful for temporarily disabling an entity without fully despawning it or invoking archetype moves. This entity keeps track of its location, so it can be re-enabled later. pub struct DisabledEntity { + /// The disabled entity. pub entity: Entity, + /// The location of the entity after it was disabled. + /// This may not necessarily be it's location when it is re-enabled. pub location: EntityLocation, } diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index 5382956281bfa..cb29e07bf6e85 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -5,7 +5,7 @@ use core::{ alloc::Layout, cell::UnsafeCell, num::NonZeroUsize, - ops::{Bound, Range, RangeBounds}, + ops::{Bound, RangeBounds}, ptr::NonNull, }; @@ -162,38 +162,6 @@ impl BlobArray { } } - /// Clears the array, i.e. removing (and dropping) all of the elements. - /// Note that this method has no effect on the allocated capacity of the vector. - /// - /// Note that this method will behave exactly the same as [`Vec::clear`]. - /// - /// # Safety - /// - For every element with index `i`, if `i` < `len`: It must be safe to call [`Self::get_unchecked_mut`] with `i`. - /// (If the safety requirements of every method that has been used on `Self` have been fulfilled, the caller just needs to ensure that `len` is correct.) - /// - /// [`Vec::clear`]: alloc::vec::Vec::clear - pub unsafe fn clear(&mut self, len: usize) { - #[cfg(debug_assertions)] - debug_assert!(self.capacity >= len); - if let Some(drop) = self.drop { - // We set `self.drop` to `None` before dropping elements for unwind safety. This ensures we don't - // accidentally drop elements twice in the event of a drop impl panicking. - self.drop = None; - let size = self.item_layout.size(); - for i in 0..len { - // SAFETY: - // * 0 <= `i` < `len`, so `i * size` must be in bounds for the allocation. - // * `size` is a multiple of the erased type's alignment, - // so adding a multiple of `size` will preserve alignment. - // * The item is left unreachable so it can be safely promoted to an `OwningPtr`. - let item = unsafe { self.get_ptr_mut().byte_add(i * size).promote() }; - // SAFETY: `item` was obtained from this `BlobArray`, so its underlying type must match `drop`. - unsafe { drop(item) }; - } - self.drop = Some(drop); - } - } - /// Clears the array, i.e. removing (and dropping) all of the elements in the range. /// Note that this method has no effect on the allocated capacity of the vector. /// @@ -243,11 +211,11 @@ impl BlobArray { /// # Safety /// - `cap` and `len` are indeed the capacity and length of this [`BlobArray`] /// - This [`BlobArray`] mustn't be used after calling this method. - pub unsafe fn drop(&mut self, cap: usize, len: usize) { + pub unsafe fn drop(&mut self, cap: usize, range: impl RangeBounds) { #[cfg(debug_assertions)] debug_assert_eq!(self.capacity, cap); if cap != 0 { - self.clear(len); + self.clear_range(range); if !self.is_zst() { let layout = array_layout(&self.item_layout, cap).expect("array layout should be valid"); diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index a4db5f5ec0304..a69a51002b34a 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -3,7 +3,9 @@ use crate::{ change_detection::MaybeLocation, storage::{blob_array::BlobArray, thin_array_ptr::ThinArrayPtr}, }; -use core::{mem::needs_drop, panic::Location}; +use alloc::vec::Vec; +use bevy_ptr::PtrMut; +use core::{mem::needs_drop, ops::RangeBounds, panic::Location}; /// A type-erased contiguous container for data of a homogeneous type. /// @@ -312,7 +314,7 @@ impl Column { pub(crate) unsafe fn clear(&mut self, len: usize) { self.added_ticks.clear_elements(len); self.changed_ticks.clear_elements(len); - self.data.clear(len); + self.data.clear_range(..len); self.changed_by .as_mut() .map(|changed_by| changed_by.clear_elements(len)); @@ -325,10 +327,10 @@ impl Column { /// - `len` is indeed the length of the column /// - `cap` is indeed the capacity of the column /// - the data stored in `self` will never be used again - pub(crate) unsafe fn drop(&mut self, cap: usize, len: usize) { + pub(crate) unsafe fn drop(&mut self, cap: usize, len: usize, range: impl RangeBounds) { self.added_ticks.drop(cap, len); self.changed_ticks.drop(cap, len); - self.data.drop(cap, len); + self.data.drop(cap, range); self.changed_by .as_mut() .map(|changed_by| changed_by.drop(cap, len)); diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index bec42ea6011b1..0364d7ee47136 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -933,7 +933,7 @@ impl Drop for Table { // SAFETY: `cap` and `len` are correct. `col` is never accessed again after this call. unsafe { // TODO: don't drop disabled entities components - col.drop(cap, len); + col.drop(cap, len, self.disabled_entities as usize..len); } } } From b3eff89dd51c680071614ac481bc2d713ae8ae23 Mon Sep 17 00:00:00 2001 From: janis Date: Tue, 7 Oct 2025 01:50:20 +0200 Subject: [PATCH 08/10] ci fix (for realsies) (revisit this) --- crates/bevy_ecs/src/storage/resource.rs | 3 ++- crates/bevy_ecs/src/storage/sparse_set.rs | 2 +- crates/bevy_ecs/src/storage/table/column.rs | 2 -- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index 093d12a2ea2ae..c7797051c6573 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -51,7 +51,8 @@ impl Drop for ResourceData { // been dropped. The validate_access call above will check that the // data is dropped on the thread it was inserted from. unsafe { - self.data.drop(1, self.is_present().into()); + let len: usize = self.is_present().into(); + self.data.drop(1, ..len); } } } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index f1a7900e675fb..bb5dbee49c0db 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -447,7 +447,7 @@ impl Drop for ComponentSparseSet { self.entities.clear(); // SAFETY: `cap` and `len` are correct. `dense` is never accessed again after this call. unsafe { - self.dense.drop(self.entities.capacity(), len); + self.dense.drop(self.entities.capacity(), len, ..len); } } } diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index a69a51002b34a..57e5883e3442a 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -3,8 +3,6 @@ use crate::{ change_detection::MaybeLocation, storage::{blob_array::BlobArray, thin_array_ptr::ThinArrayPtr}, }; -use alloc::vec::Vec; -use bevy_ptr::PtrMut; use core::{mem::needs_drop, ops::RangeBounds, panic::Location}; /// A type-erased contiguous container for data of a homogeneous type. From f98a8c31b126cf3d1e1edab498ea4bab41934bfc Mon Sep 17 00:00:00 2001 From: janis Date: Tue, 7 Oct 2025 01:55:57 +0200 Subject: [PATCH 09/10] clippy --- crates/bevy_ecs/src/query/state.rs | 4 ++-- crates/bevy_ecs/src/storage/blob_array.rs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 4d79f8e966d2b..5e6905291f376 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1416,9 +1416,9 @@ impl QueryState { use arrayvec::ArrayVec; bevy_tasks::ComputeTaskPool::get().scope(|scope| { - // SAFETY: We only access table data that has been registered in `self.component_access`. - use core::ops::Range; + + // SAFETY: We only access table data that has been registered in `self.component_access`. let tables = unsafe { &world.storages().tables }; let archetypes = world.archetypes(); let mut batch_queue = ArrayVec::new(); diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index cb29e07bf6e85..015031c93309d 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -174,10 +174,9 @@ impl BlobArray { /// [`Vec::drain`]: alloc::vec::Vec::drain pub unsafe fn clear_range(&mut self, range: impl RangeBounds) { let map_bound_or = |bounds: Bound<&usize>, or: usize, start: bool| match (bounds, start) { - (Bound::Included(&b), true) => b, + (Bound::Included(&b), true) | (Bound::Excluded(&b), false) => b, (Bound::Included(&b), false) => b.checked_add(1).expect("range end overflowed"), (Bound::Excluded(&b), true) => b.checked_add(1).expect("range start overflowed"), - (Bound::Excluded(&b), false) => b, (Bound::Unbounded, _) => or, }; From 968f45527aa165af754fa3860d044b6a90e92251 Mon Sep 17 00:00:00 2001 From: janis Date: Tue, 7 Oct 2025 02:47:04 +0200 Subject: [PATCH 10/10] set archetype table row --- crates/bevy_ecs/src/world/entity_ref.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index dbcc7e271e67d..120657d2b66c1 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2600,6 +2600,8 @@ impl<'w> EntityWorldMut<'w> { .swap_disable_unchecked(disabled_arch.table_row()) }; + archetype.set_entity_table_row(archetype_row, table_row); + if let Some((entity, table_row)) = swapped_table { let swapped_location = world.entities.get(entity).unwrap(); @@ -2613,6 +2615,7 @@ impl<'w> EntityWorldMut<'w> { }), ); } + archetype.set_entity_table_row(swapped_location.archetype_row, table_row); } assert_eq!(disabled_table, disabled_arch.id());