Skip to content

Commit 771f8cd

Browse files
maniwaniexjam
authored andcommitted
Add comparison methods to FilteredAccessSet (bevyengine#4211)
# Objective - (Eventually) reduce noise in reporting access conflicts between unordered systems. - `SystemStage` only looks at unfiltered `ComponentId` access, any conflicts reported are potentially `false`. - the systems could still be accessing disjoint archetypes - Comparing systems' filtered access sets can maybe avoid that (for statically known component types). - bevyengine#4204 ## Solution - Modify `SparseSetIndex` trait to require `PartialEq`, `Eq`, and `Hash` (all internal types except `BundleId` already did). - Add `is_compatible` and `get_conflicts` methods to `FilteredAccessSet<T>` - (existing method renamed to `get_conflicts_single`) - Add docs for those and all the other methods while I'm at it.
1 parent 5a193ee commit 771f8cd

File tree

4 files changed

+123
-55
lines changed

4 files changed

+123
-55
lines changed

crates/bevy_ecs/src/bundle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ macro_rules! tuple_impl {
129129

130130
all_tuples!(tuple_impl, 0, 15, C);
131131

132-
#[derive(Debug, Clone, Copy)]
132+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
133133
pub struct BundleId(usize);
134134

135135
impl BundleId {

crates/bevy_ecs/src/query/access.rs

Lines changed: 118 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ use bevy_utils::HashSet;
33
use fixedbitset::FixedBitSet;
44
use std::marker::PhantomData;
55

6-
/// `Access` keeps track of read and write accesses to values within a collection.
6+
/// Tracks read and write access to specific elements in a collection.
77
///
8-
/// This is used for ensuring systems are executed soundly.
9-
#[derive(Debug, Eq, PartialEq, Clone)]
8+
/// Used internally to ensure soundness during system initialization and execution.
9+
/// See the [`is_compatible`](Access::is_compatible) and [`get_conflicts`](Access::get_conflicts) functions.
10+
#[derive(Debug, Clone, Eq, PartialEq)]
1011
pub struct Access<T: SparseSetIndex> {
11-
reads_all: bool,
12-
/// A combined set of T read and write accesses.
12+
/// All accessed elements.
1313
reads_and_writes: FixedBitSet,
14+
/// The exclusively-accessed elements.
1415
writes: FixedBitSet,
16+
/// Is `true` if this has access to all elements in the collection?
17+
/// This field is a performance optimization for `&World` (also harder to mess up for soundness).
18+
reads_all: bool,
1519
marker: PhantomData<T>,
1620
}
1721

@@ -27,26 +31,29 @@ impl<T: SparseSetIndex> Default for Access<T> {
2731
}
2832

2933
impl<T: SparseSetIndex> Access<T> {
30-
pub fn grow(&mut self, bits: usize) {
31-
self.reads_and_writes.grow(bits);
32-
self.writes.grow(bits);
34+
/// Increases the set capacity to the specified amount.
35+
///
36+
/// Does nothing if `capacity` is less than or equal to the current value.
37+
pub fn grow(&mut self, capacity: usize) {
38+
self.reads_and_writes.grow(capacity);
39+
self.writes.grow(capacity);
3340
}
3441

35-
/// Adds a read access for the given index.
42+
/// Adds access to the element given by `index`.
3643
pub fn add_read(&mut self, index: T) {
3744
self.reads_and_writes.grow(index.sparse_set_index() + 1);
3845
self.reads_and_writes.insert(index.sparse_set_index());
3946
}
4047

41-
/// Adds a write access for the given index.
48+
/// Adds exclusive access to the element given by `index`.
4249
pub fn add_write(&mut self, index: T) {
4350
self.reads_and_writes.grow(index.sparse_set_index() + 1);
44-
self.writes.grow(index.sparse_set_index() + 1);
4551
self.reads_and_writes.insert(index.sparse_set_index());
52+
self.writes.grow(index.sparse_set_index() + 1);
4653
self.writes.insert(index.sparse_set_index());
4754
}
4855

49-
/// Returns true if this `Access` contains a read access for the given index.
56+
/// Returns `true` if this can access the element given by `index`.
5057
pub fn has_read(&self, index: T) -> bool {
5158
if self.reads_all {
5259
true
@@ -55,51 +62,54 @@ impl<T: SparseSetIndex> Access<T> {
5562
}
5663
}
5764

58-
/// Returns true if this `Access` contains a write access for the given index.
65+
/// Returns `true` if this can exclusively access the element given by `index`.
5966
pub fn has_write(&self, index: T) -> bool {
6067
self.writes.contains(index.sparse_set_index())
6168
}
6269

63-
/// Sets this `Access` to having read access for all indices.
70+
/// Sets this as having access to all indexed elements (i.e. `&World`).
6471
pub fn read_all(&mut self) {
6572
self.reads_all = true;
6673
}
6774

68-
/// Returns true if this `Access` has read access to all indices.
69-
pub fn reads_all(&self) -> bool {
75+
/// Returns `true` if this has access to all indexed elements (i.e. `&World`).
76+
pub fn has_read_all(&self) -> bool {
7077
self.reads_all
7178
}
7279

73-
/// Clears all recorded accesses.
80+
/// Removes all accesses.
7481
pub fn clear(&mut self) {
7582
self.reads_all = false;
7683
self.reads_and_writes.clear();
7784
self.writes.clear();
7885
}
7986

80-
/// Extends this `Access` with another, copying all accesses of `other` into this.
87+
/// Adds all access from `other`.
8188
pub fn extend(&mut self, other: &Access<T>) {
8289
self.reads_all = self.reads_all || other.reads_all;
8390
self.reads_and_writes.union_with(&other.reads_and_writes);
8491
self.writes.union_with(&other.writes);
8592
}
8693

87-
/// Returns true if this `Access` is compatible with `other`.
94+
/// Returns `true` if the access and `other` can be active at the same time.
8895
///
89-
/// Two `Access` instances are incompatible with each other if one `Access` has a write for
90-
/// which the other also has a write or a read.
96+
/// `Access` instances are incompatible if one can write
97+
/// an element that the other can read or write.
9198
pub fn is_compatible(&self, other: &Access<T>) -> bool {
99+
// Only systems that do not write data are compatible with systems that operate on `&World`.
92100
if self.reads_all {
93-
0 == other.writes.count_ones(..)
94-
} else if other.reads_all {
95-
0 == self.writes.count_ones(..)
96-
} else {
97-
self.writes.is_disjoint(&other.reads_and_writes)
98-
&& self.reads_and_writes.is_disjoint(&other.writes)
101+
return other.writes.count_ones(..) == 0;
102+
}
103+
104+
if other.reads_all {
105+
return self.writes.count_ones(..) == 0;
99106
}
107+
108+
self.writes.is_disjoint(&other.reads_and_writes)
109+
&& self.reads_and_writes.is_disjoint(&other.writes)
100110
}
101111

102-
/// Calculates conflicting accesses between this `Access` and `other`.
112+
/// Returns a vector of elements that the access and `other` cannot access at the same time.
103113
pub fn get_conflicts(&self, other: &Access<T>) -> Vec<T> {
104114
let mut conflicts = FixedBitSet::default();
105115
if self.reads_all {
@@ -117,20 +127,28 @@ impl<T: SparseSetIndex> Access<T> {
117127
.collect()
118128
}
119129

120-
/// Returns all read accesses.
130+
/// Returns the indices of the elements this has access to.
131+
pub fn reads_and_writes(&self) -> impl Iterator<Item = T> + '_ {
132+
self.reads_and_writes.ones().map(T::get_sparse_set_index)
133+
}
134+
135+
/// Returns the indices of the elements this has non-exclusive access to.
121136
pub fn reads(&self) -> impl Iterator<Item = T> + '_ {
122137
self.reads_and_writes
123138
.difference(&self.writes)
124139
.map(T::get_sparse_set_index)
125140
}
126141

127-
/// Returns all write accesses.
142+
/// Returns the indices of the elements this has exclusive access to.
128143
pub fn writes(&self) -> impl Iterator<Item = T> + '_ {
129144
self.writes.ones().map(T::get_sparse_set_index)
130145
}
131146
}
132147

133-
#[derive(Clone, Eq, PartialEq, Debug)]
148+
/// An [`Access`] that has been filtered to include and exclude certain combinations of elements.
149+
///
150+
/// Used internally to statically check if queries are disjoint.
151+
#[derive(Debug, Clone, Eq, PartialEq)]
134152
pub struct FilteredAccess<T: SparseSetIndex> {
135153
access: Access<T>,
136154
with: FixedBitSet,
@@ -156,31 +174,43 @@ impl<T: SparseSetIndex> From<FilteredAccess<T>> for FilteredAccessSet<T> {
156174
}
157175

158176
impl<T: SparseSetIndex> FilteredAccess<T> {
177+
/// Returns a reference to the underlying unfiltered access.
159178
#[inline]
160179
pub fn access(&self) -> &Access<T> {
161180
&self.access
162181
}
163182

183+
/// Returns a mutable reference to the underlying unfiltered access.
184+
#[inline]
185+
pub fn access_mut(&mut self) -> &mut Access<T> {
186+
&mut self.access
187+
}
188+
189+
/// Adds access to the element given by `index`.
164190
pub fn add_read(&mut self, index: T) {
165191
self.access.add_read(index.clone());
166192
self.add_with(index);
167193
}
168194

195+
/// Adds exclusive access to the element given by `index`.
169196
pub fn add_write(&mut self, index: T) {
170197
self.access.add_write(index.clone());
171198
self.add_with(index);
172199
}
173200

201+
/// Retains only combinations where the element given by `index` is also present.
174202
pub fn add_with(&mut self, index: T) {
175203
self.with.grow(index.sparse_set_index() + 1);
176204
self.with.insert(index.sparse_set_index());
177205
}
178206

207+
/// Retains only combinations where the element given by `index` is not present.
179208
pub fn add_without(&mut self, index: T) {
180209
self.without.grow(index.sparse_set_index() + 1);
181210
self.without.insert(index.sparse_set_index());
182211
}
183212

213+
/// Returns `true` if this and `other` can be active at the same time.
184214
pub fn is_compatible(&self, other: &FilteredAccess<T>) -> bool {
185215
if self.access.is_compatible(&other.access) {
186216
true
@@ -190,56 +220,94 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
190220
}
191221
}
192222

223+
/// Returns a vector of elements that this and `other` cannot access at the same time.
224+
pub fn get_conflicts(&self, other: &FilteredAccess<T>) -> Vec<T> {
225+
if !self.is_compatible(other) {
226+
// filters are disjoint, so we can just look at the unfiltered intersection
227+
return self.access.get_conflicts(&other.access);
228+
}
229+
Vec::new()
230+
}
231+
232+
/// Adds all access and filters from `other`.
193233
pub fn extend(&mut self, access: &FilteredAccess<T>) {
194234
self.access.extend(&access.access);
195235
self.with.union_with(&access.with);
196236
self.without.union_with(&access.without);
197237
}
198238

239+
/// Sets the underlying unfiltered access as having access to all indexed elements.
199240
pub fn read_all(&mut self) {
200241
self.access.read_all();
201242
}
202243
}
203-
#[derive(Clone, Debug)]
244+
245+
/// A collection of [`FilteredAccess`] instances.
246+
///
247+
/// Used internally to statically check if systems have conflicting access.
248+
#[derive(Debug, Clone)]
204249
pub struct FilteredAccessSet<T: SparseSetIndex> {
205250
combined_access: Access<T>,
206251
filtered_accesses: Vec<FilteredAccess<T>>,
207252
}
208253

209254
impl<T: SparseSetIndex> FilteredAccessSet<T> {
255+
/// Returns a reference to the unfiltered access of the entire set.
210256
#[inline]
211257
pub fn combined_access(&self) -> &Access<T> {
212258
&self.combined_access
213259
}
214260

261+
/// Returns a mutable reference to the unfiltered access of the entire set.
215262
#[inline]
216263
pub fn combined_access_mut(&mut self) -> &mut Access<T> {
217264
&mut self.combined_access
218265
}
219266

220-
pub fn get_conflicts(&self, filtered_access: &FilteredAccess<T>) -> Vec<T> {
221-
// if combined unfiltered access is incompatible, check each filtered access for
222-
// compatibility
223-
let mut conflicts = HashSet::<usize>::default();
224-
if !filtered_access.access.is_compatible(&self.combined_access) {
225-
for current_filtered_access in &self.filtered_accesses {
226-
if !current_filtered_access.is_compatible(filtered_access) {
227-
conflicts.extend(
228-
current_filtered_access
229-
.access
230-
.get_conflicts(&filtered_access.access)
231-
.iter()
232-
.map(|ind| ind.sparse_set_index()),
233-
);
267+
/// Returns `true` if this and `other` can be active at the same time.
268+
pub fn is_compatible(&self, other: &FilteredAccessSet<T>) -> bool {
269+
if self.combined_access.is_compatible(other.combined_access()) {
270+
return true;
271+
} else {
272+
for filtered in self.filtered_accesses.iter() {
273+
for other_filtered in other.filtered_accesses.iter() {
274+
if !filtered.is_compatible(other_filtered) {
275+
return false;
276+
}
234277
}
235278
}
236279
}
237-
conflicts
238-
.iter()
239-
.map(|ind| T::get_sparse_set_index(*ind))
240-
.collect()
280+
281+
true
282+
}
283+
284+
/// Returns a vector of elements that this set and `other` cannot access at the same time.
285+
pub fn get_conflicts(&self, other: &FilteredAccessSet<T>) -> Vec<T> {
286+
// if the unfiltered access is incompatible, must check each pair
287+
let mut conflicts = HashSet::new();
288+
if !self.combined_access.is_compatible(other.combined_access()) {
289+
for filtered in self.filtered_accesses.iter() {
290+
for other_filtered in other.filtered_accesses.iter() {
291+
conflicts.extend(filtered.get_conflicts(other_filtered).into_iter());
292+
}
293+
}
294+
}
295+
conflicts.into_iter().collect()
296+
}
297+
298+
/// Returns a vector of elements that this set and `other` cannot access at the same time.
299+
pub fn get_conflicts_single(&self, filtered_access: &FilteredAccess<T>) -> Vec<T> {
300+
// if the unfiltered access is incompatible, must check each pair
301+
let mut conflicts = HashSet::new();
302+
if !self.combined_access.is_compatible(filtered_access.access()) {
303+
for filtered in self.filtered_accesses.iter() {
304+
conflicts.extend(filtered.get_conflicts(filtered_access).into_iter());
305+
}
306+
}
307+
conflicts.into_iter().collect()
241308
}
242309

310+
/// Adds the filtered access to the set.
243311
pub fn add(&mut self, filtered_access: FilteredAccess<T>) {
244312
self.combined_access.extend(&filtered_access.access);
245313
self.filtered_accesses.push(filtered_access);

crates/bevy_ecs/src/storage/sparse_set.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
storage::BlobVec,
55
};
66
use bevy_ptr::{OwningPtr, Ptr};
7-
use std::{cell::UnsafeCell, marker::PhantomData};
7+
use std::{cell::UnsafeCell, hash::Hash, marker::PhantomData};
88

99
#[derive(Debug)]
1010
pub struct SparseArray<I, V = I> {
@@ -372,7 +372,7 @@ impl<I: SparseSetIndex, V> SparseSet<I, V> {
372372
}
373373
}
374374

375-
pub trait SparseSetIndex: Clone {
375+
pub trait SparseSetIndex: Clone + PartialEq + Eq + Hash {
376376
fn sparse_set_index(&self) -> usize;
377377
fn get_sparse_set_index(value: usize) -> Self;
378378
}

crates/bevy_ecs/src/system/system_param.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ fn assert_component_access_compatibility(
154154
current: &FilteredAccess<ComponentId>,
155155
world: &World,
156156
) {
157-
let mut conflicts = system_access.get_conflicts(current);
157+
let mut conflicts = system_access.get_conflicts_single(current);
158158
if conflicts.is_empty() {
159159
return;
160160
}
@@ -531,7 +531,7 @@ unsafe impl<'w, 's> SystemParamState for WorldState {
531531
filtered_access.read_all();
532532
if !system_meta
533533
.component_access_set
534-
.get_conflicts(&filtered_access)
534+
.get_conflicts_single(&filtered_access)
535535
.is_empty()
536536
{
537537
panic!("&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules");

0 commit comments

Comments
 (0)