Skip to content

Commit f47c65e

Browse files
nicopapcart
authored andcommitted
Fix FilteredAccessSet get_conflicts inconsistency (#5105)
# Objective * Enable `Res` and `Query` parameter mutual exclusion * Required for #5080 The `FilteredAccessSet::get_conflicts` methods didn't work properly with `Res` and `ResMut` parameters. Because those added their access by using the `combined_access_mut` method and directly modifying the global access state of the FilteredAccessSet. This caused an inconsistency, because get_conflicts assumes that ALL added access have a corresponding `FilteredAccess` added to the `filtered_accesses` field. In practice, that means that SystemParam that adds their access through the `Access` returned by `combined_access_mut` and the ones that add their access using the `add` method lived in two different universes. As a result, they could never be mutually exclusive. ## Solution This commit fixes it by removing the `combined_access_mut` method. This ensures that the `combined_access` field of FilteredAccessSet is always updated consistently with the addition of a filter. When checking for filtered access, it is now possible to account for `Res` and `ResMut` invalid access. This is currently not needed, but might be in the future. We add the `add_unfiltered_{read,write}` methods to replace previous usages of `combined_access_mut`. We also add improved Debug implementations on FixedBitSet so that their meaning is much clearer in debug output. --- ## Changelog * Fix `Res` and `Query` parameter never being mutually exclusive. ## Migration Guide Note: this mostly changes ECS internals, but since the API is public, it is technically breaking: * Removed `FilteredAccessSet::combined_access_mut` * Replace _immutable_ usage of those by `combined_access` * For _mutable_ usages, use the new `add_unfiltered_{read,write}` methods instead of `combined_access_mut` followed by `add_{read,write}`
1 parent e30019b commit f47c65e

File tree

3 files changed

+157
-31
lines changed

3 files changed

+157
-31
lines changed

crates/bevy_ecs/src/query/access.rs

Lines changed: 136 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,51 @@
11
use crate::storage::SparseSetIndex;
22
use bevy_utils::HashSet;
3+
use core::fmt;
34
use fixedbitset::FixedBitSet;
45
use std::marker::PhantomData;
56

7+
/// A wrapper struct to make Debug representations of [`FixedBitSet`] easier
8+
/// to read, when used to store [`SparseSetIndex`].
9+
///
10+
/// Instead of the raw integer representation of the `FixedBitSet`, the list of
11+
/// `T` valid for [`SparseSetIndex`] is shown.
12+
///
13+
/// Normal `FixedBitSet` `Debug` output:
14+
/// ```text
15+
/// read_and_writes: FixedBitSet { data: [ 160 ], length: 8 }
16+
/// ```
17+
///
18+
/// Which, unless you are a computer, doesn't help much understand what's in
19+
/// the set. With `FormattedBitSet`, we convert the present set entries into
20+
/// what they stand for, it is much clearer what is going on:
21+
/// ```text
22+
/// read_and_writes: [ ComponentId(5), ComponentId(7) ]
23+
/// ```
24+
struct FormattedBitSet<'a, T: SparseSetIndex> {
25+
bit_set: &'a FixedBitSet,
26+
_marker: PhantomData<T>,
27+
}
28+
impl<'a, T: SparseSetIndex> FormattedBitSet<'a, T> {
29+
fn new(bit_set: &'a FixedBitSet) -> Self {
30+
Self {
31+
bit_set,
32+
_marker: PhantomData,
33+
}
34+
}
35+
}
36+
impl<'a, T: SparseSetIndex + fmt::Debug> fmt::Debug for FormattedBitSet<'a, T> {
37+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38+
f.debug_list()
39+
.entries(self.bit_set.ones().map(T::get_sparse_set_index))
40+
.finish()
41+
}
42+
}
43+
644
/// Tracks read and write access to specific elements in a collection.
745
///
846
/// Used internally to ensure soundness during system initialization and execution.
947
/// See the [`is_compatible`](Access::is_compatible) and [`get_conflicts`](Access::get_conflicts) functions.
10-
#[derive(Debug, Clone, Eq, PartialEq)]
48+
#[derive(Clone, Eq, PartialEq)]
1149
pub struct Access<T: SparseSetIndex> {
1250
/// All accessed elements.
1351
reads_and_writes: FixedBitSet,
@@ -19,6 +57,18 @@ pub struct Access<T: SparseSetIndex> {
1957
marker: PhantomData<T>,
2058
}
2159

60+
impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for Access<T> {
61+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62+
f.debug_struct("Access")
63+
.field(
64+
"read_and_writes",
65+
&FormattedBitSet::<T>::new(&self.reads_and_writes),
66+
)
67+
.field("writes", &FormattedBitSet::<T>::new(&self.writes))
68+
.field("reads_all", &self.reads_all)
69+
.finish()
70+
}
71+
}
2272
impl<T: SparseSetIndex> Default for Access<T> {
2373
fn default() -> Self {
2474
Self {
@@ -55,11 +105,7 @@ impl<T: SparseSetIndex> Access<T> {
55105

56106
/// Returns `true` if this can access the element given by `index`.
57107
pub fn has_read(&self, index: T) -> bool {
58-
if self.reads_all {
59-
true
60-
} else {
61-
self.reads_and_writes.contains(index.sparse_set_index())
62-
}
108+
self.reads_all || self.reads_and_writes.contains(index.sparse_set_index())
63109
}
64110

65111
/// Returns `true` if this can exclusively access the element given by `index`.
@@ -106,7 +152,7 @@ impl<T: SparseSetIndex> Access<T> {
106152
}
107153

108154
self.writes.is_disjoint(&other.reads_and_writes)
109-
&& self.reads_and_writes.is_disjoint(&other.writes)
155+
&& other.writes.is_disjoint(&self.reads_and_writes)
110156
}
111157

112158
/// Returns a vector of elements that the access and `other` cannot access at the same time.
@@ -153,7 +199,7 @@ impl<T: SparseSetIndex> Access<T> {
153199
/// `with` access.
154200
///
155201
/// For example consider `Query<Option<&T>>` this only has a `read` of `T` as doing
156-
/// otherwise would allow for queries to be considered disjoint that actually aren't:
202+
/// otherwise would allow for queries to be considered disjoint when they shouldn't:
157203
/// - `Query<(&mut T, Option<&U>)>` read/write `T`, read `U`, with `U`
158204
/// - `Query<&mut T, Without<U>>` read/write `T`, without `U`
159205
/// from this we could reasonably conclude that the queries are disjoint but they aren't.
@@ -165,12 +211,21 @@ impl<T: SparseSetIndex> Access<T> {
165211
/// - `Query<Option<&T>` accesses nothing
166212
///
167213
/// See comments the `WorldQuery` impls of `AnyOf`/`Option`/`Or` for more information.
168-
#[derive(Debug, Clone, Eq, PartialEq)]
214+
#[derive(Clone, Eq, PartialEq)]
169215
pub struct FilteredAccess<T: SparseSetIndex> {
170216
access: Access<T>,
171217
with: FixedBitSet,
172218
without: FixedBitSet,
173219
}
220+
impl<T: SparseSetIndex + fmt::Debug> fmt::Debug for FilteredAccess<T> {
221+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222+
f.debug_struct("FilteredAccess")
223+
.field("access", &self.access)
224+
.field("with", &FormattedBitSet::<T>::new(&self.with))
225+
.field("without", &FormattedBitSet::<T>::new(&self.without))
226+
.finish()
227+
}
228+
}
174229

175230
impl<T: SparseSetIndex> Default for FilteredAccess<T> {
176231
fn default() -> Self {
@@ -238,12 +293,9 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
238293

239294
/// Returns `true` if this and `other` can be active at the same time.
240295
pub fn is_compatible(&self, other: &FilteredAccess<T>) -> bool {
241-
if self.access.is_compatible(&other.access) {
242-
true
243-
} else {
244-
self.with.intersection(&other.without).next().is_some()
245-
|| self.without.intersection(&other.with).next().is_some()
246-
}
296+
self.access.is_compatible(&other.access)
297+
|| !self.with.is_disjoint(&other.without)
298+
|| !other.with.is_disjoint(&self.without)
247299
}
248300

249301
/// Returns a vector of elements that this and `other` cannot access at the same time.
@@ -271,6 +323,10 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
271323
/// A collection of [`FilteredAccess`] instances.
272324
///
273325
/// Used internally to statically check if systems have conflicting access.
326+
///
327+
/// It stores multiple sets of accesses.
328+
/// - A "combined" set, which is the access of all filters in this set combined.
329+
/// - The set of access of each individual filters in this set.
274330
#[derive(Debug, Clone)]
275331
pub struct FilteredAccessSet<T: SparseSetIndex> {
276332
combined_access: Access<T>,
@@ -284,13 +340,18 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
284340
&self.combined_access
285341
}
286342

287-
/// Returns a mutable reference to the unfiltered access of the entire set.
288-
#[inline]
289-
pub fn combined_access_mut(&mut self) -> &mut Access<T> {
290-
&mut self.combined_access
291-
}
292-
293343
/// Returns `true` if this and `other` can be active at the same time.
344+
///
345+
/// Access conflict resolution happen in two steps:
346+
/// 1. A "coarse" check, if there is no mutual unfiltered conflict between
347+
/// `self` and `other`, we already know that the two access sets are
348+
/// compatible.
349+
/// 2. A "fine grained" check, it kicks in when the "coarse" check fails.
350+
/// the two access sets might still be compatible if some of the accesses
351+
/// are restricted with the `With` or `Without` filters so that access is
352+
/// mutually exclusive. The fine grained phase iterates over all filters in
353+
/// the `self` set and compares it to all the filters in the `other` set,
354+
/// making sure they are all mutually compatible.
294355
pub fn is_compatible(&self, other: &FilteredAccessSet<T>) -> bool {
295356
if self.combined_access.is_compatible(other.combined_access()) {
296357
return true;
@@ -302,7 +363,6 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
302363
}
303364
}
304365
}
305-
306366
true
307367
}
308368

@@ -338,6 +398,20 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
338398
self.filtered_accesses.push(filtered_access);
339399
}
340400

401+
/// Adds a read access without filters to the set.
402+
pub(crate) fn add_unfiltered_read(&mut self, index: T) {
403+
let mut filter = FilteredAccess::default();
404+
filter.add_read(index);
405+
self.add(filter);
406+
}
407+
408+
/// Adds a write access without filters to the set.
409+
pub(crate) fn add_unfiltered_write(&mut self, index: T) {
410+
let mut filter = FilteredAccess::default();
411+
filter.add_write(index);
412+
self.add(filter);
413+
}
414+
341415
pub fn extend(&mut self, filtered_access_set: FilteredAccessSet<T>) {
342416
self.combined_access
343417
.extend(&filtered_access_set.combined_access);
@@ -362,7 +436,30 @@ impl<T: SparseSetIndex> Default for FilteredAccessSet<T> {
362436

363437
#[cfg(test)]
364438
mod tests {
365-
use crate::query::{Access, FilteredAccess};
439+
use crate::query::{Access, FilteredAccess, FilteredAccessSet};
440+
441+
#[test]
442+
fn read_all_access_conflicts() {
443+
// read_all / single write
444+
let mut access_a = Access::<usize>::default();
445+
access_a.grow(10);
446+
access_a.add_write(0);
447+
448+
let mut access_b = Access::<usize>::default();
449+
access_b.read_all();
450+
451+
assert!(!access_b.is_compatible(&access_a));
452+
453+
// read_all / read_all
454+
let mut access_a = Access::<usize>::default();
455+
access_a.grow(10);
456+
access_a.read_all();
457+
458+
let mut access_b = Access::<usize>::default();
459+
access_b.read_all();
460+
461+
assert!(access_b.is_compatible(&access_a));
462+
}
366463

367464
#[test]
368465
fn access_get_conflicts() {
@@ -391,6 +488,22 @@ mod tests {
391488
assert_eq!(access_d.get_conflicts(&access_c), vec![0]);
392489
}
393490

491+
#[test]
492+
fn filtered_combined_access() {
493+
let mut access_a = FilteredAccessSet::<usize>::default();
494+
access_a.add_unfiltered_read(1);
495+
496+
let mut filter_b = FilteredAccess::<usize>::default();
497+
filter_b.add_write(1);
498+
499+
let conflicts = access_a.get_conflicts_single(&filter_b);
500+
assert_eq!(
501+
&conflicts,
502+
&[1_usize],
503+
"access_a: {access_a:?}, filter_b: {filter_b:?}"
504+
);
505+
}
506+
394507
#[test]
395508
fn filtered_access_extend() {
396509
let mut access_a = FilteredAccess::<usize>::default();

crates/bevy_ecs/src/storage/sparse_set.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,11 @@ impl<I: SparseSetIndex, V> SparseSet<I, V> {
370370
}
371371
}
372372

373+
/// Represents something that can be stored in a [`SparseSet`] as an integer.
374+
///
375+
/// Ideally, the `usize` values should be very small (ie: incremented starting from
376+
/// zero), as the number of bits needed to represent a `SparseSetIndex` in a `FixedBitSet`
377+
/// is proportional to the **value** of those `usize`.
373378
pub trait SparseSetIndex: Clone + PartialEq + Eq + Hash {
374379
fn sparse_set_index(&self) -> usize;
375380
fn get_sparse_set_index(value: usize) -> Self;

crates/bevy_ecs/src/system/system_param.rs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -372,14 +372,16 @@ impl<'a, T: Resource> SystemParam for Res<'a, T> {
372372
unsafe impl<T: Resource> SystemParamState for ResState<T> {
373373
fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self {
374374
let component_id = world.initialize_resource::<T>();
375-
let combined_access = system_meta.component_access_set.combined_access_mut();
375+
let combined_access = system_meta.component_access_set.combined_access();
376376
assert!(
377377
!combined_access.has_write(component_id),
378378
"error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access.",
379379
std::any::type_name::<T>(),
380380
system_meta.name,
381381
);
382-
combined_access.add_read(component_id);
382+
system_meta
383+
.component_access_set
384+
.add_unfiltered_read(component_id);
383385

384386
let archetype_component_id = world
385387
.get_resource_archetype_component_id(component_id)
@@ -479,7 +481,7 @@ impl<'a, T: Resource> SystemParam for ResMut<'a, T> {
479481
unsafe impl<T: Resource> SystemParamState for ResMutState<T> {
480482
fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self {
481483
let component_id = world.initialize_resource::<T>();
482-
let combined_access = system_meta.component_access_set.combined_access_mut();
484+
let combined_access = system_meta.component_access_set.combined_access();
483485
if combined_access.has_write(component_id) {
484486
panic!(
485487
"error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access.",
@@ -489,7 +491,9 @@ unsafe impl<T: Resource> SystemParamState for ResMutState<T> {
489491
"error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access.",
490492
std::any::type_name::<T>(), system_meta.name);
491493
}
492-
combined_access.add_write(component_id);
494+
system_meta
495+
.component_access_set
496+
.add_unfiltered_write(component_id);
493497

494498
let archetype_component_id = world
495499
.get_resource_archetype_component_id(component_id)
@@ -963,14 +967,16 @@ unsafe impl<T: 'static> SystemParamState for NonSendState<T> {
963967
system_meta.set_non_send();
964968

965969
let component_id = world.initialize_non_send_resource::<T>();
966-
let combined_access = system_meta.component_access_set.combined_access_mut();
970+
let combined_access = system_meta.component_access_set.combined_access();
967971
assert!(
968972
!combined_access.has_write(component_id),
969973
"error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access.",
970974
std::any::type_name::<T>(),
971975
system_meta.name,
972976
);
973-
combined_access.add_read(component_id);
977+
system_meta
978+
.component_access_set
979+
.add_unfiltered_read(component_id);
974980

975981
let archetype_component_id = world
976982
.get_resource_archetype_component_id(component_id)
@@ -1075,7 +1081,7 @@ unsafe impl<T: 'static> SystemParamState for NonSendMutState<T> {
10751081
system_meta.set_non_send();
10761082

10771083
let component_id = world.initialize_non_send_resource::<T>();
1078-
let combined_access = system_meta.component_access_set.combined_access_mut();
1084+
let combined_access = system_meta.component_access_set.combined_access();
10791085
if combined_access.has_write(component_id) {
10801086
panic!(
10811087
"error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access.",
@@ -1085,7 +1091,9 @@ unsafe impl<T: 'static> SystemParamState for NonSendMutState<T> {
10851091
"error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access.",
10861092
std::any::type_name::<T>(), system_meta.name);
10871093
}
1088-
combined_access.add_write(component_id);
1094+
system_meta
1095+
.component_access_set
1096+
.add_unfiltered_write(component_id);
10891097

10901098
let archetype_component_id = world
10911099
.get_resource_archetype_component_id(component_id)

0 commit comments

Comments
 (0)