Skip to content

Commit 71fccb2

Browse files
vladbat00MinerSebasalice-i-cecile
authored
Improve or-with disjoint checks (#7085)
# Objective This PR attempts to improve query compatibility checks in scenarios involving `Or` filters. Currently, for the following two disjoint queries, Bevy will throw a panic: ``` fn sys(_: Query<&mut C, Or<(With<A>, With<B>)>>, _: Query<&mut C, (Without<A>, Without<B>)>) {} ``` This PR addresses this particular scenario. ## Solution `FilteredAccess::with` now stores a vector of `AccessFilters` (representing a pair of `with` and `without` bitsets), where each member represents an `Or` "variant". Filters like `(With<A>, Or<(With<B>, Without<C>)>` are expected to be expanded into `A * B + A * !C`. When calculating whether queries are compatible, every `AccessFilters` of a query is tested for incompatibility with every `AccessFilters` of another query. --- ## Changelog - Improved system and query data access compatibility checks in scenarios involving `Or` filters --------- Co-authored-by: MinerSebas <66798382+MinerSebas@users.noreply.github.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
1 parent c488b70 commit 71fccb2

File tree

4 files changed

+298
-76
lines changed

4 files changed

+298
-76
lines changed

crates/bevy_ecs/src/query/access.rs

Lines changed: 172 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ struct FormattedBitSet<'a, T: SparseSetIndex> {
2525
bit_set: &'a FixedBitSet,
2626
_marker: PhantomData<T>,
2727
}
28+
2829
impl<'a, T: SparseSetIndex> FormattedBitSet<'a, T> {
2930
fn new(bit_set: &'a FixedBitSet) -> Self {
3031
Self {
@@ -33,6 +34,7 @@ impl<'a, T: SparseSetIndex> FormattedBitSet<'a, T> {
3334
}
3435
}
3536
}
37+
3638
impl<'a, T: SparseSetIndex + fmt::Debug> fmt::Debug for FormattedBitSet<'a, T> {
3739
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3840
f.debug_list()
@@ -69,6 +71,7 @@ impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for Access<T> {
6971
.finish()
7072
}
7173
}
74+
7275
impl<T: SparseSetIndex> Default for Access<T> {
7376
fn default() -> Self {
7477
Self::new()
@@ -213,31 +216,22 @@ impl<T: SparseSetIndex> Access<T> {
213216
/// is read/write `T`, read `U`. It must still have a read `U` access otherwise the following
214217
/// queries would be incorrectly considered disjoint:
215218
/// - `Query<&mut T>` read/write `T`
216-
/// - `Query<Option<&T>` accesses nothing
219+
/// - `Query<Option<&T>>` accesses nothing
217220
///
218221
/// See comments the `WorldQuery` impls of `AnyOf`/`Option`/`Or` for more information.
219-
#[derive(Clone, Eq, PartialEq)]
222+
#[derive(Debug, Clone, Eq, PartialEq)]
220223
pub struct FilteredAccess<T: SparseSetIndex> {
221224
access: Access<T>,
222-
with: FixedBitSet,
223-
without: FixedBitSet,
224-
}
225-
impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for FilteredAccess<T> {
226-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227-
f.debug_struct("FilteredAccess")
228-
.field("access", &self.access)
229-
.field("with", &FormattedBitSet::<T>::new(&self.with))
230-
.field("without", &FormattedBitSet::<T>::new(&self.without))
231-
.finish()
232-
}
225+
// An array of filter sets to express `With` or `Without` clauses in disjunctive normal form, for example: `Or<(With<A>, With<B>)>`.
226+
// Filters like `(With<A>, Or<(With<B>, Without<C>)>` are expanded into `Or<((With<A>, With<B>), (With<A>, Without<C>))>`.
227+
filter_sets: Vec<AccessFilters<T>>,
233228
}
234229

235230
impl<T: SparseSetIndex> Default for FilteredAccess<T> {
236231
fn default() -> Self {
237232
Self {
238233
access: Access::default(),
239-
with: Default::default(),
240-
without: Default::default(),
234+
filter_sets: vec![AccessFilters::default()],
241235
}
242236
}
243237
}
@@ -266,30 +260,46 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
266260
/// Adds access to the element given by `index`.
267261
pub fn add_read(&mut self, index: T) {
268262
self.access.add_read(index.clone());
269-
self.add_with(index);
263+
self.and_with(index);
270264
}
271265

272266
/// Adds exclusive access to the element given by `index`.
273267
pub fn add_write(&mut self, index: T) {
274268
self.access.add_write(index.clone());
275-
self.add_with(index);
269+
self.and_with(index);
276270
}
277271

278-
/// Retains only combinations where the element given by `index` is also present.
279-
pub fn add_with(&mut self, index: T) {
280-
self.with.grow(index.sparse_set_index() + 1);
281-
self.with.insert(index.sparse_set_index());
272+
/// Adds a `With` filter: corresponds to a conjunction (AND) operation.
273+
///
274+
/// Suppose we begin with `Or<(With<A>, With<B>)>`, which is represented by an array of two `AccessFilter` instances.
275+
/// Adding `AND With<C>` via this method transforms it into the equivalent of `Or<((With<A>, With<C>), (With<B>, With<C>))>`.
276+
pub fn and_with(&mut self, index: T) {
277+
let index = index.sparse_set_index();
278+
for filter in &mut self.filter_sets {
279+
filter.with.grow(index + 1);
280+
filter.with.insert(index);
281+
}
282282
}
283283

284-
/// Retains only combinations where the element given by `index` is not present.
285-
pub fn add_without(&mut self, index: T) {
286-
self.without.grow(index.sparse_set_index() + 1);
287-
self.without.insert(index.sparse_set_index());
284+
/// Adds a `Without` filter: corresponds to a conjunction (AND) operation.
285+
///
286+
/// Suppose we begin with `Or<(With<A>, With<B>)>`, which is represented by an array of two `AccessFilter` instances.
287+
/// Adding `AND Without<C>` via this method transforms it into the equivalent of `Or<((With<A>, Without<C>), (With<B>, Without<C>))>`.
288+
pub fn and_without(&mut self, index: T) {
289+
let index = index.sparse_set_index();
290+
for filter in &mut self.filter_sets {
291+
filter.without.grow(index + 1);
292+
filter.without.insert(index);
293+
}
288294
}
289295

290-
pub fn extend_intersect_filter(&mut self, other: &FilteredAccess<T>) {
291-
self.without.intersect_with(&other.without);
292-
self.with.intersect_with(&other.with);
296+
/// Appends an array of filters: corresponds to a disjunction (OR) operation.
297+
///
298+
/// As the underlying array of filters represents a disjunction,
299+
/// where each element (`AccessFilters`) represents a conjunction,
300+
/// we can simply append to the array.
301+
pub fn append_or(&mut self, other: &FilteredAccess<T>) {
302+
self.filter_sets.append(&mut other.filter_sets.clone());
293303
}
294304

295305
pub fn extend_access(&mut self, other: &FilteredAccess<T>) {
@@ -298,9 +308,23 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
298308

299309
/// Returns `true` if this and `other` can be active at the same time.
300310
pub fn is_compatible(&self, other: &FilteredAccess<T>) -> bool {
301-
self.access.is_compatible(&other.access)
302-
|| !self.with.is_disjoint(&other.without)
303-
|| !other.with.is_disjoint(&self.without)
311+
if self.access.is_compatible(&other.access) {
312+
return true;
313+
}
314+
315+
// If the access instances are incompatible, we want to check that whether filters can
316+
// guarantee that queries are disjoint.
317+
// Since the `filter_sets` array represents a Disjunctive Normal Form formula ("ORs of ANDs"),
318+
// we need to make sure that each filter set (ANDs) rule out every filter set from the `other` instance.
319+
//
320+
// For example, `Query<&mut C, Or<(With<A>, Without<B>)>>` is compatible `Query<&mut C, (With<B>, Without<A>)>`,
321+
// but `Query<&mut C, Or<(Without<A>, Without<B>)>>` isn't compatible with `Query<&mut C, Or<(With<A>, With<B>)>>`.
322+
self.filter_sets.iter().all(|filter| {
323+
other
324+
.filter_sets
325+
.iter()
326+
.all(|other_filter| filter.is_ruled_out_by(other_filter))
327+
})
304328
}
305329

306330
/// Returns a vector of elements that this and `other` cannot access at the same time.
@@ -313,10 +337,34 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
313337
}
314338

315339
/// Adds all access and filters from `other`.
316-
pub fn extend(&mut self, access: &FilteredAccess<T>) {
317-
self.access.extend(&access.access);
318-
self.with.union_with(&access.with);
319-
self.without.union_with(&access.without);
340+
///
341+
/// Corresponds to a conjunction operation (AND) for filters.
342+
///
343+
/// Extending `Or<(With<A>, Without<B>)>` with `Or<(With<C>, Without<D>)>` will result in
344+
/// `Or<((With<A>, With<C>), (With<A>, Without<D>), (Without<B>, With<C>), (Without<B>, Without<D>))>`.
345+
pub fn extend(&mut self, other: &FilteredAccess<T>) {
346+
self.access.extend(&other.access);
347+
348+
// We can avoid allocating a new array of bitsets if `other` contains just a single set of filters:
349+
// in this case we can short-circuit by performing an in-place union for each bitset.
350+
if other.filter_sets.len() == 1 {
351+
for filter in &mut self.filter_sets {
352+
filter.with.union_with(&other.filter_sets[0].with);
353+
filter.without.union_with(&other.filter_sets[0].without);
354+
}
355+
return;
356+
}
357+
358+
let mut new_filters = Vec::with_capacity(self.filter_sets.len() * other.filter_sets.len());
359+
for filter in &self.filter_sets {
360+
for other_filter in &other.filter_sets {
361+
let mut new_filter = filter.clone();
362+
new_filter.with.union_with(&other_filter.with);
363+
new_filter.without.union_with(&other_filter.without);
364+
new_filters.push(new_filter);
365+
}
366+
}
367+
self.filter_sets = new_filters;
320368
}
321369

322370
/// Sets the underlying unfiltered access as having access to all indexed elements.
@@ -325,6 +373,43 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
325373
}
326374
}
327375

376+
#[derive(Clone, Eq, PartialEq)]
377+
struct AccessFilters<T> {
378+
with: FixedBitSet,
379+
without: FixedBitSet,
380+
_index_type: PhantomData<T>,
381+
}
382+
383+
impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for AccessFilters<T> {
384+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385+
f.debug_struct("AccessFilters")
386+
.field("with", &FormattedBitSet::<T>::new(&self.with))
387+
.field("without", &FormattedBitSet::<T>::new(&self.without))
388+
.finish()
389+
}
390+
}
391+
392+
impl<T: SparseSetIndex> Default for AccessFilters<T> {
393+
fn default() -> Self {
394+
Self {
395+
with: FixedBitSet::default(),
396+
without: FixedBitSet::default(),
397+
_index_type: PhantomData,
398+
}
399+
}
400+
}
401+
402+
impl<T: SparseSetIndex> AccessFilters<T> {
403+
fn is_ruled_out_by(&self, other: &Self) -> bool {
404+
// Although not technically complete, we don't consider the case when `AccessFilters`'s
405+
// `without` bitset contradicts its own `with` bitset (e.g. `(With<A>, Without<A>)`).
406+
// Such query would be considered compatible with any other query, but as it's almost
407+
// always an error, we ignore this case instead of treating such query as compatible
408+
// with others.
409+
!self.with.is_disjoint(&other.without) || !self.without.is_disjoint(&other.with)
410+
}
411+
}
412+
328413
/// A collection of [`FilteredAccess`] instances.
329414
///
330415
/// Used internally to statically check if systems have conflicting access.
@@ -441,7 +526,10 @@ impl<T: SparseSetIndex> Default for FilteredAccessSet<T> {
441526

442527
#[cfg(test)]
443528
mod tests {
529+
use crate::query::access::AccessFilters;
444530
use crate::query::{Access, FilteredAccess, FilteredAccessSet};
531+
use fixedbitset::FixedBitSet;
532+
use std::marker::PhantomData;
445533

446534
#[test]
447535
fn read_all_access_conflicts() {
@@ -514,22 +602,67 @@ mod tests {
514602
let mut access_a = FilteredAccess::<usize>::default();
515603
access_a.add_read(0);
516604
access_a.add_read(1);
517-
access_a.add_with(2);
605+
access_a.and_with(2);
518606

519607
let mut access_b = FilteredAccess::<usize>::default();
520608
access_b.add_read(0);
521609
access_b.add_write(3);
522-
access_b.add_without(4);
610+
access_b.and_without(4);
523611

524612
access_a.extend(&access_b);
525613

526614
let mut expected = FilteredAccess::<usize>::default();
527615
expected.add_read(0);
528616
expected.add_read(1);
529-
expected.add_with(2);
617+
expected.and_with(2);
530618
expected.add_write(3);
531-
expected.add_without(4);
619+
expected.and_without(4);
532620

533621
assert!(access_a.eq(&expected));
534622
}
623+
624+
#[test]
625+
fn filtered_access_extend_or() {
626+
let mut access_a = FilteredAccess::<usize>::default();
627+
// Exclusive access to `(&mut A, &mut B)`.
628+
access_a.add_write(0);
629+
access_a.add_write(1);
630+
631+
// Filter by `With<C>`.
632+
let mut access_b = FilteredAccess::<usize>::default();
633+
access_b.and_with(2);
634+
635+
// Filter by `(With<D>, Without<E>)`.
636+
let mut access_c = FilteredAccess::<usize>::default();
637+
access_c.and_with(3);
638+
access_c.and_without(4);
639+
640+
// Turns `access_b` into `Or<(With<C>, (With<D>, Without<D>))>`.
641+
access_b.append_or(&access_c);
642+
// Applies the filters to the initial query, which corresponds to the FilteredAccess'
643+
// representation of `Query<(&mut A, &mut B), Or<(With<C>, (With<D>, Without<E>))>>`.
644+
access_a.extend(&access_b);
645+
646+
// Construct the expected `FilteredAccess` struct.
647+
// The intention here is to test that exclusive access implied by `add_write`
648+
// forms correct normalized access structs when extended with `Or` filters.
649+
let mut expected = FilteredAccess::<usize>::default();
650+
expected.add_write(0);
651+
expected.add_write(1);
652+
// The resulted access is expected to represent `Or<((With<A>, With<B>, With<C>), (With<A>, With<B>, With<D>, Without<E>))>`.
653+
expected.filter_sets = vec![
654+
AccessFilters {
655+
with: FixedBitSet::with_capacity_and_blocks(3, [0b111]),
656+
without: FixedBitSet::default(),
657+
_index_type: PhantomData,
658+
},
659+
AccessFilters {
660+
with: FixedBitSet::with_capacity_and_blocks(4, [0b1011]),
661+
without: FixedBitSet::with_capacity_and_blocks(5, [0b10000]),
662+
_index_type: PhantomData,
663+
},
664+
];
665+
666+
assert_eq!(access_a, expected);
667+
}
535668
}

crates/bevy_ecs/src/query/fetch.rs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1281,34 +1281,21 @@ macro_rules! impl_anytuple_fetch {
12811281
fn update_component_access(state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {
12821282
let ($($name,)*) = state;
12831283

1284-
// We do not unconditionally add `$name`'s `with`/`without` accesses to `_access`
1285-
// as this would be unsound. For example the following two queries should conflict:
1286-
// - Query<(AnyOf<(&A, ())>, &mut B)>
1287-
// - Query<&mut B, Without<A>>
1288-
//
1289-
// If we were to unconditionally add `$name`'s `with`/`without` accesses then `AnyOf<(&A, ())>`
1290-
// would have a `With<A>` access which is incorrect as this `WorldQuery` will match entities that
1291-
// do not have the `A` component. This is the same logic as the `Or<...>: WorldQuery` impl.
1292-
//
1293-
// The correct thing to do here is to only add a `with`/`without` access to `_access` if all
1294-
// `$name` params have that `with`/`without` access. More jargony put- we add the intersection
1295-
// of all `with`/`without` accesses of the `$name` params to `_access`.
1296-
let mut _intersected_access = _access.clone();
1284+
let mut _new_access = _access.clone();
12971285
let mut _not_first = false;
12981286
$(
12991287
if _not_first {
13001288
let mut intermediate = _access.clone();
13011289
$name::update_component_access($name, &mut intermediate);
1302-
_intersected_access.extend_intersect_filter(&intermediate);
1303-
_intersected_access.extend_access(&intermediate);
1290+
_new_access.append_or(&intermediate);
1291+
_new_access.extend_access(&intermediate);
13041292
} else {
1305-
1306-
$name::update_component_access($name, &mut _intersected_access);
1293+
$name::update_component_access($name, &mut _new_access);
13071294
_not_first = true;
13081295
}
13091296
)*
13101297

1311-
*_access = _intersected_access;
1298+
*_access = _new_access;
13121299
}
13131300

13141301
fn update_archetype_component_access(state: &Self::State, _archetype: &Archetype, _access: &mut Access<ArchetypeComponentId>) {

0 commit comments

Comments
 (0)