This commit is contained in:
Chris Russell 2025-07-16 17:24:17 -04:00 committed by GitHub
commit 1d705f922a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 89 additions and 13 deletions

View File

@ -81,15 +81,17 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
.is_some_and(|info| info.storage_type() == StorageType::Table) .is_some_and(|info| info.storage_type() == StorageType::Table)
}; };
let Ok(component_accesses) = self.access.access().try_iter_component_access() else { // Use dense iteration if possible, but fall back to sparse if we need to.
// Access is unbounded, pessimistically assume it's sparse. // Both `D` and `F` must allow dense iteration, just as for queries without dynamic filters.
return false; // All `with` and `without` filters must be dense to ensure that we match all archetypes in a table.
}; // We also need to ensure that any sparse set components in `access.required` cause sparse iteration,
// but anything that adds a `required` component also adds a `with` filter.
component_accesses //
.map(|access| *access.index()) // Note that `EntityRef` and `EntityMut` types, including `FilteredEntityRef` and `FilteredEntityMut`, have `D::IS_DENSE = true`.
.all(is_dense) // Calling `builder.data::<&Sparse>()` will add a filter and force sparse iteration,
&& !self.access.access().has_read_all_components() // but calling `builder.data::<Option<&Sparse>>()` will still allow them to use dense iteration!
D::IS_DENSE
&& F::IS_DENSE
&& self.access.with_filters().all(is_dense) && self.access.with_filters().all(is_dense)
&& self.access.without_filters().all(is_dense) && self.access.without_filters().all(is_dense)
} }
@ -528,4 +530,32 @@ mod tests {
let matched = query.iter(&world).count(); let matched = query.iter(&world).count();
assert_eq!(matched, 1); assert_eq!(matched, 1);
} }
#[test]
fn builder_dynamic_can_be_dense() {
#[derive(Component)]
#[component(storage = "SparseSet")]
struct Sparse;
let mut world = World::new();
// FilteredEntityRef and FilteredEntityMut are dense by default
let query = QueryBuilder::<FilteredEntityRef>::new(&mut world).build();
assert!(query.is_dense);
let query = QueryBuilder::<FilteredEntityMut>::new(&mut world).build();
assert!(query.is_dense);
// Adding a required sparse term makes the query sparse
let query = QueryBuilder::<FilteredEntityRef>::new(&mut world)
.data::<&Sparse>()
.build();
assert!(!query.is_dense);
// Adding an optional sparse term lets it remain dense
let query = QueryBuilder::<FilteredEntityRef>::new(&mut world)
.data::<Option<&Sparse>>()
.build();
assert!(query.is_dense);
}
} }

View File

@ -929,7 +929,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> {
fetch fetch
} }
const IS_DENSE: bool = false; const IS_DENSE: bool = true;
unsafe fn init_fetch<'w, 's>( unsafe fn init_fetch<'w, 's>(
world: UnsafeWorldCell<'w>, world: UnsafeWorldCell<'w>,
@ -1053,7 +1053,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> {
fetch fetch
} }
const IS_DENSE: bool = false; const IS_DENSE: bool = true;
unsafe fn init_fetch<'w, 's>( unsafe fn init_fetch<'w, 's>(
world: UnsafeWorldCell<'w>, world: UnsafeWorldCell<'w>,

View File

@ -676,11 +676,30 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
DebugName::type_name::<(NewD, NewF)>(), DebugName::type_name::<(D, F)>() DebugName::type_name::<(NewD, NewF)>(), DebugName::type_name::<(D, F)>()
); );
// For transmuted queries, the dense-ness of the query is equal to the dense-ness of the original query.
//
// We ensure soundness using `FilteredAccess::required`.
//
// Any `WorldQuery` implementations that rely on a query being sparse for soundness,
// including `&`, `&mut`, `Ref`, and `Mut`, will add a sparse set component to the `required` set.
// (`Option<&Sparse>` and `Has<Sparse>` will incorrectly report a component as never being present
// when doing dense iteration, but are not unsound. See https://github.com/bevyengine/bevy/issues/16397)
//
// And any query with a sparse set component in the `required` set must have `is_dense = false`.
// For static queries, the `WorldQuery` implementations ensure this.
// For dynamic queries, anything that adds a `required` component also adds a `with` filter.
//
// The `component_access.is_subset()` check ensures that if the new query has a sparse set component in the `required` set,
// then the original query must also have had that component in the `required` set.
// Therefore, if the `WorldQuery` implementations rely on a query being sparse for soundness,
// then there was a sparse set component in the `required` set, and the query has `is_dense = false`.
let is_dense = self.is_dense;
QueryState { QueryState {
world_id: self.world_id, world_id: self.world_id,
archetype_generation: self.archetype_generation, archetype_generation: self.archetype_generation,
matched_storage_ids: self.matched_storage_ids.clone(), matched_storage_ids: self.matched_storage_ids.clone(),
is_dense: self.is_dense, is_dense,
fetch_state, fetch_state,
filter_state, filter_state,
component_access: self_access, component_access: self_access,
@ -1778,7 +1797,7 @@ mod tests {
entity_disabling::DefaultQueryFilters, entity_disabling::DefaultQueryFilters,
prelude::*, prelude::*,
system::{QueryLens, RunSystemOnce}, system::{QueryLens, RunSystemOnce},
world::{FilteredEntityMut, FilteredEntityRef}, world::{EntityRef, FilteredEntityMut, FilteredEntityRef},
}; };
#[test] #[test]
@ -2085,6 +2104,33 @@ mod tests {
assert_eq!(matched, 2); assert_eq!(matched, 2);
} }
#[test]
fn dense_query_over_option_is_buggy() {
#[derive(Component)]
#[component(storage = "SparseSet")]
struct Sparse;
let mut world = World::new();
world.spawn(Sparse);
let mut query =
QueryState::<EntityRef>::new(&mut world).transmute::<Option<&Sparse>>(&world);
// EntityRef always performs dense iteration
// But `Option<&Sparse>` will incorrectly report a component as never being present when doing dense iteration
// See https://github.com/bevyengine/bevy/issues/16397
assert!(query.is_dense);
let matched = query.iter(&world).filter(Option::is_some).count();
assert_eq!(matched, 0);
let mut query = QueryState::<EntityRef>::new(&mut world).transmute::<Has<Sparse>>(&world);
// EntityRef always performs dense iteration
// But `Has<Sparse>` will incorrectly report a component as never being present when doing dense iteration
// See https://github.com/bevyengine/bevy/issues/16397
assert!(query.is_dense);
let matched = query.iter(&world).filter(|&has| has).count();
assert_eq!(matched, 0);
}
#[test] #[test]
fn join() { fn join() {
let mut world = World::new(); let mut world = World::new();