Skip to content

Commit e320fa0

Browse files
Fix query transmute from table to archetype iteration unsoundness (#14615)
# Objective - Fixes #14348 - Fixes #14528 - Less complex (but also likely less performant) alternative to #14611 ## Solution - Add a `is_dense` field flag to `QueryIter` indicating whether it is dense or not, that is whether it can perform dense iteration or not; - Check this flag any time iteration over a query is performed. --- It would be nice if someone could try benching this change to see if it actually matters. ~Note that this not 100% ready for mergin, since there are a bunch of safety comments on the use of the various `IS_DENSE` for checks that still need to be updated.~ This is ready modulo benchmarks --------- Co-authored-by: Alice Cecile <[email protected]>
1 parent f06cd44 commit e320fa0

File tree

4 files changed

+154
-35
lines changed

4 files changed

+154
-35
lines changed

crates/bevy_ecs/src/query/builder.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::marker::PhantomData;
22

3+
use crate::component::StorageType;
34
use crate::{component::ComponentId, prelude::*};
45

56
use super::{FilteredAccess, QueryData, QueryFilter};
@@ -68,6 +69,26 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
6869
}
6970
}
7071

72+
pub(super) fn is_dense(&self) -> bool {
73+
// Note: `component_id` comes from the user in safe code, so we cannot trust it to
74+
// exist. If it doesn't exist we pessimistically assume it's sparse.
75+
let is_dense = |component_id| {
76+
self.world()
77+
.components()
78+
.get_info(component_id)
79+
.map_or(false, |info| info.storage_type() == StorageType::Table)
80+
};
81+
82+
self.access
83+
.access()
84+
.component_reads_and_writes()
85+
.all(is_dense)
86+
&& self.access.access().archetypal().all(is_dense)
87+
&& !self.access.access().has_read_all_components()
88+
&& self.access.with_filters().all(is_dense)
89+
&& self.access.without_filters().all(is_dense)
90+
}
91+
7192
/// Returns a reference to the world passed to [`Self::new`].
7293
pub fn world(&self) -> &World {
7394
self.world
@@ -396,4 +417,27 @@ mod tests {
396417
assert_eq!(1, b.deref::<B>().0);
397418
}
398419
}
420+
421+
/// Regression test for issue #14348
422+
#[test]
423+
fn builder_static_dense_dynamic_sparse() {
424+
#[derive(Component)]
425+
struct Dense;
426+
427+
#[derive(Component)]
428+
#[component(storage = "SparseSet")]
429+
struct Sparse;
430+
431+
let mut world = World::new();
432+
433+
world.spawn(Dense);
434+
world.spawn((Dense, Sparse));
435+
436+
let mut query = QueryBuilder::<&Dense>::new(&mut world)
437+
.with::<Sparse>()
438+
.build();
439+
440+
let matched = query.iter(&world).count();
441+
assert_eq!(matched, 1);
442+
}
399443
}

crates/bevy_ecs/src/query/iter.rs

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
128128
/// # Safety
129129
/// - all `rows` must be in `[0, table.entity_count)`.
130130
/// - `table` must match D and F
131-
/// - Both `D::IS_DENSE` and `F::IS_DENSE` must be true.
131+
/// - The query iteration must be dense (i.e. `self.query_state.is_dense` must be true).
132132
#[inline]
133133
pub(super) unsafe fn fold_over_table_range<B, Func>(
134134
&mut self,
@@ -183,7 +183,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
183183
/// # Safety
184184
/// - all `indices` must be in `[0, archetype.len())`.
185185
/// - `archetype` must match D and F
186-
/// - Either `D::IS_DENSE` or `F::IS_DENSE` must be false.
186+
/// - The query iteration must not be dense (i.e. `self.query_state.is_dense` must be false).
187187
#[inline]
188188
pub(super) unsafe fn fold_over_archetype_range<B, Func>(
189189
&mut self,
@@ -252,7 +252,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
252252
/// - all `indices` must be in `[0, archetype.len())`.
253253
/// - `archetype` must match D and F
254254
/// - `archetype` must have the same length with it's table.
255-
/// - Either `D::IS_DENSE` or `F::IS_DENSE` must be false.
255+
/// - The query iteration must not be dense (i.e. `self.query_state.is_dense` must be false).
256256
#[inline]
257257
pub(super) unsafe fn fold_over_dense_archetype_range<B, Func>(
258258
&mut self,
@@ -1031,40 +1031,47 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F>
10311031
let Some(item) = self.next() else { break };
10321032
accum = func(accum, item);
10331033
}
1034-
for id in self.cursor.storage_id_iter.clone() {
1035-
if D::IS_DENSE && F::IS_DENSE {
1034+
1035+
if self.cursor.is_dense {
1036+
for id in self.cursor.storage_id_iter.clone() {
1037+
// SAFETY: `self.cursor.is_dense` is true, so storage ids are guaranteed to be table ids.
1038+
let table_id = unsafe { id.table_id };
10361039
// SAFETY: Matched table IDs are guaranteed to still exist.
1037-
let table = unsafe { self.tables.get(id.table_id).debug_checked_unwrap() };
1040+
let table = unsafe { self.tables.get(table_id).debug_checked_unwrap() };
1041+
10381042
accum =
10391043
// SAFETY:
10401044
// - The fetched table matches both D and F
10411045
// - The provided range is equivalent to [0, table.entity_count)
1042-
// - The if block ensures that D::IS_DENSE and F::IS_DENSE are both true
1046+
// - The if block ensures that the query iteration is dense
10431047
unsafe { self.fold_over_table_range(accum, &mut func, table, 0..table.entity_count()) };
1044-
} else {
1045-
let archetype =
1046-
// SAFETY: Matched archetype IDs are guaranteed to still exist.
1047-
unsafe { self.archetypes.get(id.archetype_id).debug_checked_unwrap() };
1048+
}
1049+
} else {
1050+
for id in self.cursor.storage_id_iter.clone() {
1051+
// SAFETY: `self.cursor.is_dense` is false, so storage ids are guaranteed to be archetype ids.
1052+
let archetype_id = unsafe { id.archetype_id };
1053+
// SAFETY: Matched archetype IDs are guaranteed to still exist.
1054+
let archetype = unsafe { self.archetypes.get(archetype_id).debug_checked_unwrap() };
10481055
// SAFETY: Matched table IDs are guaranteed to still exist.
10491056
let table = unsafe { self.tables.get(archetype.table_id()).debug_checked_unwrap() };
10501057

10511058
// When an archetype and its table have equal entity counts, dense iteration can be safely used.
10521059
// this leverages cache locality to optimize performance.
10531060
if table.entity_count() == archetype.len() {
10541061
accum =
1055-
// SAFETY:
1056-
// - The fetched archetype matches both D and F
1057-
// - The provided archetype and its' table have the same length.
1058-
// - The provided range is equivalent to [0, archetype.len)
1059-
// - The if block ensures that ether D::IS_DENSE or F::IS_DENSE are false
1060-
unsafe { self.fold_over_dense_archetype_range(accum, &mut func, archetype,0..archetype.len()) };
1062+
// SAFETY:
1063+
// - The fetched archetype matches both D and F
1064+
// - The provided archetype and its' table have the same length.
1065+
// - The provided range is equivalent to [0, archetype.len)
1066+
// - The if block ensures that the query iteration is not dense.
1067+
unsafe { self.fold_over_dense_archetype_range(accum, &mut func, archetype, 0..archetype.len()) };
10611068
} else {
10621069
accum =
1063-
// SAFETY:
1064-
// - The fetched archetype matches both D and F
1065-
// - The provided range is equivalent to [0, archetype.len)
1066-
// - The if block ensures that ether D::IS_DENSE or F::IS_DENSE are false
1067-
unsafe { self.fold_over_archetype_range(accum, &mut func, archetype,0..archetype.len()) };
1070+
// SAFETY:
1071+
// - The fetched archetype matches both D and F
1072+
// - The provided range is equivalent to [0, archetype.len)
1073+
// - The if block ensures that the query iteration is not dense.
1074+
unsafe { self.fold_over_archetype_range(accum, &mut func, archetype, 0..archetype.len()) };
10681075
}
10691076
}
10701077
}
@@ -1675,6 +1682,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> Debug
16751682
}
16761683

16771684
struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> {
1685+
// whether the query iteration is dense or not. Mirrors QueryState's `is_dense` field.
1686+
is_dense: bool,
16781687
storage_id_iter: std::slice::Iter<'s, StorageId>,
16791688
table_entities: &'w [Entity],
16801689
archetype_entities: &'w [ArchetypeEntity],
@@ -1689,6 +1698,7 @@ struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> {
16891698
impl<D: QueryData, F: QueryFilter> Clone for QueryIterationCursor<'_, '_, D, F> {
16901699
fn clone(&self) -> Self {
16911700
Self {
1701+
is_dense: self.is_dense,
16921702
storage_id_iter: self.storage_id_iter.clone(),
16931703
table_entities: self.table_entities,
16941704
archetype_entities: self.archetype_entities,
@@ -1701,8 +1711,6 @@ impl<D: QueryData, F: QueryFilter> Clone for QueryIterationCursor<'_, '_, D, F>
17011711
}
17021712

17031713
impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
1704-
const IS_DENSE: bool = D::IS_DENSE && F::IS_DENSE;
1705-
17061714
unsafe fn init_empty(
17071715
world: UnsafeWorldCell<'w>,
17081716
query_state: &'s QueryState<D, F>,
@@ -1732,13 +1740,15 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
17321740
table_entities: &[],
17331741
archetype_entities: &[],
17341742
storage_id_iter: query_state.matched_storage_ids.iter(),
1743+
is_dense: query_state.is_dense,
17351744
current_len: 0,
17361745
current_row: 0,
17371746
}
17381747
}
17391748

17401749
fn reborrow(&mut self) -> QueryIterationCursor<'_, 's, D, F> {
17411750
QueryIterationCursor {
1751+
is_dense: self.is_dense,
17421752
fetch: D::shrink_fetch(self.fetch.clone()),
17431753
filter: F::shrink_fetch(self.filter.clone()),
17441754
table_entities: self.table_entities,
@@ -1754,7 +1764,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
17541764
unsafe fn peek_last(&mut self) -> Option<D::Item<'w>> {
17551765
if self.current_row > 0 {
17561766
let index = self.current_row - 1;
1757-
if Self::IS_DENSE {
1767+
if self.is_dense {
17581768
let entity = self.table_entities.get_unchecked(index);
17591769
Some(D::fetch(
17601770
&mut self.fetch,
@@ -1780,7 +1790,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
17801790
/// will be **the exact count of remaining values**.
17811791
fn max_remaining(&self, tables: &'w Tables, archetypes: &'w Archetypes) -> usize {
17821792
let ids = self.storage_id_iter.clone();
1783-
let remaining_matched: usize = if Self::IS_DENSE {
1793+
let remaining_matched: usize = if self.is_dense {
17841794
// SAFETY: The if check ensures that storage_id_iter stores TableIds
17851795
unsafe { ids.map(|id| tables[id.table_id].entity_count()).sum() }
17861796
} else {
@@ -1803,7 +1813,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> {
18031813
archetypes: &'w Archetypes,
18041814
query_state: &'s QueryState<D, F>,
18051815
) -> Option<D::Item<'w>> {
1806-
if Self::IS_DENSE {
1816+
if self.is_dense {
18071817
loop {
18081818
// we are on the beginning of the query, or finished processing a table, so skip to the next
18091819
if self.current_row == self.current_len {

crates/bevy_ecs/src/query/par_iter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> {
126126
fn get_batch_size(&self, thread_count: usize) -> usize {
127127
let max_items = || {
128128
let id_iter = self.state.matched_storage_ids.iter();
129-
if D::IS_DENSE && F::IS_DENSE {
129+
if self.state.is_dense {
130130
// SAFETY: We only access table metadata.
131131
let tables = unsafe { &self.world.world_metadata().storages().tables };
132132
id_iter

0 commit comments

Comments
 (0)