Fix soudness issue with Conflicts involving read_all and write_all (#14579)

# Objective

- Fixes https://github.com/bevyengine/bevy/issues/14575
- There is a soundness issue because we use `conflicts()` to check for
system ambiguities + soundness issues. However since the current
conflicts is a `Vec<T>`, we cannot express conflicts where there is no
specific `ComponentId` at fault. For example `q1: Query<EntityMut>, q2:
Query<EntityMut>`
There was a TODO to handle the `write_all` case but it was never
resolved


## Solution

- Introduce an `AccessConflict` enum that is either a list of specific
ids that are conflicting or `All` if all component ids are conflicting

## Testing

- Introduced a new unit test to check for the `EntityMut` case

## Migration guide

The `get_conflicts` method of `Access` now returns an `AccessConflict`
enum instead of simply a `Vec` of `ComponentId`s that are causing the
access conflict. This can be useful in cases where there are no
particular `ComponentId`s conflicting, but instead **all** of them are;
for example `fn system(q1: Query<EntityMut>, q2: Query<EntityRef>)`
This commit is contained in:
Periwink 2024-08-06 06:55:31 -04:00 committed by GitHub
parent 3360b45153
commit e85c072372
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 177 additions and 49 deletions

View File

@ -74,6 +74,7 @@ pub mod prelude {
mod tests { mod tests {
use crate as bevy_ecs; use crate as bevy_ecs;
use crate::prelude::Or; use crate::prelude::Or;
use crate::world::EntityMut;
use crate::{ use crate::{
bundle::Bundle, bundle::Bundle,
change_detection::Ref, change_detection::Ref,
@ -1389,6 +1390,26 @@ mod tests {
world.query::<(&mut A, EntityRef)>(); world.query::<(&mut A, EntityRef)>();
} }
#[test]
#[should_panic]
fn entity_ref_and_entity_mut_query_panic() {
let mut world = World::new();
world.query::<(EntityRef, EntityMut)>();
}
#[test]
#[should_panic]
fn entity_mut_and_entity_mut_query_panic() {
let mut world = World::new();
world.query::<(EntityMut, EntityMut)>();
}
#[test]
fn entity_ref_and_entity_ref_query_no_panic() {
let mut world = World::new();
world.query::<(EntityRef, EntityRef)>();
}
#[test] #[test]
#[should_panic] #[should_panic]
fn mut_and_mut_query_panic() { fn mut_and_mut_query_panic() {

View File

@ -1,7 +1,7 @@
use crate::storage::SparseSetIndex; use crate::storage::SparseSetIndex;
use bevy_utils::HashSet;
use core::fmt; use core::fmt;
use fixedbitset::FixedBitSet; use fixedbitset::FixedBitSet;
use std::fmt::Debug;
use std::marker::PhantomData; use std::marker::PhantomData;
/// A wrapper struct to make Debug representations of [`FixedBitSet`] easier /// A wrapper struct to make Debug representations of [`FixedBitSet`] easier
@ -484,15 +484,19 @@ impl<T: SparseSetIndex> Access<T> {
} }
/// Returns a vector of elements that the access and `other` cannot access at the same time. /// Returns a vector of elements that the access and `other` cannot access at the same time.
pub fn get_conflicts(&self, other: &Access<T>) -> Vec<T> { pub fn get_conflicts(&self, other: &Access<T>) -> AccessConflicts {
let mut conflicts = FixedBitSet::default(); let mut conflicts = FixedBitSet::new();
if self.reads_all_components { if self.reads_all_components {
// QUESTION: How to handle `other.writes_all`? if other.writes_all_components {
return AccessConflicts::All;
}
conflicts.extend(other.component_writes.ones()); conflicts.extend(other.component_writes.ones());
} }
if other.reads_all_components { if other.reads_all_components {
// QUESTION: How to handle `self.writes_all`. if self.writes_all_components {
return AccessConflicts::All;
}
conflicts.extend(self.component_writes.ones()); conflicts.extend(self.component_writes.ones());
} }
@ -504,15 +508,18 @@ impl<T: SparseSetIndex> Access<T> {
conflicts.extend(self.component_read_and_writes.ones()); conflicts.extend(self.component_read_and_writes.ones());
} }
if self.reads_all_resources { if self.reads_all_resources {
// QUESTION: How to handle `other.writes_all`? if other.reads_all_resources {
return AccessConflicts::All;
}
conflicts.extend(other.resource_writes.ones()); conflicts.extend(other.resource_writes.ones());
} }
if other.reads_all_resources { if other.reads_all_resources {
// QUESTION: How to handle `self.writes_all`. if self.reads_all_resources {
return AccessConflicts::All;
}
conflicts.extend(self.resource_writes.ones()); conflicts.extend(self.resource_writes.ones());
} }
if self.writes_all_resources { if self.writes_all_resources {
conflicts.extend(other.resource_read_and_writes.ones()); conflicts.extend(other.resource_read_and_writes.ones());
} }
@ -537,10 +544,7 @@ impl<T: SparseSetIndex> Access<T> {
self.resource_read_and_writes self.resource_read_and_writes
.intersection(&other.resource_writes), .intersection(&other.resource_writes),
); );
conflicts AccessConflicts::Individual(conflicts)
.ones()
.map(SparseSetIndex::get_sparse_set_index)
.collect()
} }
/// Returns the indices of the components this has access to. /// Returns the indices of the components this has access to.
@ -635,6 +639,53 @@ impl<T: SparseSetIndex> From<FilteredAccess<T>> for FilteredAccessSet<T> {
} }
} }
/// Records how two accesses conflict with each other
#[derive(Debug, PartialEq)]
pub enum AccessConflicts {
/// Conflict is for all indices
All,
/// There is a conflict for a subset of indices
Individual(FixedBitSet),
}
impl AccessConflicts {
fn add(&mut self, other: &Self) {
match (self, other) {
(s, AccessConflicts::All) => {
*s = AccessConflicts::All;
}
(AccessConflicts::Individual(this), AccessConflicts::Individual(other)) => {
this.extend(other.ones());
}
_ => {}
}
}
pub(crate) fn is_empty(&self) -> bool {
match self {
Self::All => false,
Self::Individual(set) => set.is_empty(),
}
}
/// An [`AccessConflicts`] which represents the absence of any conflict
pub(crate) fn empty() -> Self {
Self::Individual(FixedBitSet::new())
}
}
impl From<FixedBitSet> for AccessConflicts {
fn from(value: FixedBitSet) -> Self {
Self::Individual(value)
}
}
impl<T: SparseSetIndex> From<Vec<T>> for AccessConflicts {
fn from(value: Vec<T>) -> Self {
Self::Individual(value.iter().map(T::sparse_set_index).collect())
}
}
impl<T: SparseSetIndex> FilteredAccess<T> { impl<T: SparseSetIndex> FilteredAccess<T> {
/// Returns a `FilteredAccess` which has no access and matches everything. /// Returns a `FilteredAccess` which has no access and matches everything.
/// This is the equivalent of a `TRUE` logic atom. /// This is the equivalent of a `TRUE` logic atom.
@ -752,12 +803,12 @@ impl<T: SparseSetIndex> FilteredAccess<T> {
} }
/// Returns a vector of elements that this and `other` cannot access at the same time. /// Returns a vector of elements that this and `other` cannot access at the same time.
pub fn get_conflicts(&self, other: &FilteredAccess<T>) -> Vec<T> { pub fn get_conflicts(&self, other: &FilteredAccess<T>) -> AccessConflicts {
if !self.is_compatible(other) { if !self.is_compatible(other) {
// filters are disjoint, so we can just look at the unfiltered intersection // filters are disjoint, so we can just look at the unfiltered intersection
return self.access.get_conflicts(&other.access); return self.access.get_conflicts(&other.access);
} }
Vec::new() AccessConflicts::empty()
} }
/// Adds all access and filters from `other`. /// Adds all access and filters from `other`.
@ -948,29 +999,29 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
} }
/// Returns a vector of elements that this set and `other` cannot access at the same time. /// Returns a vector of elements that this set and `other` cannot access at the same time.
pub fn get_conflicts(&self, other: &FilteredAccessSet<T>) -> Vec<T> { pub fn get_conflicts(&self, other: &FilteredAccessSet<T>) -> AccessConflicts {
// if the unfiltered access is incompatible, must check each pair // if the unfiltered access is incompatible, must check each pair
let mut conflicts = HashSet::new(); let mut conflicts = AccessConflicts::empty();
if !self.combined_access.is_compatible(other.combined_access()) { if !self.combined_access.is_compatible(other.combined_access()) {
for filtered in &self.filtered_accesses { for filtered in &self.filtered_accesses {
for other_filtered in &other.filtered_accesses { for other_filtered in &other.filtered_accesses {
conflicts.extend(filtered.get_conflicts(other_filtered).into_iter()); conflicts.add(&filtered.get_conflicts(other_filtered));
} }
} }
} }
conflicts.into_iter().collect() conflicts
} }
/// Returns a vector of elements that this set and `other` cannot access at the same time. /// Returns a vector of elements that this set and `other` cannot access at the same time.
pub fn get_conflicts_single(&self, filtered_access: &FilteredAccess<T>) -> Vec<T> { pub fn get_conflicts_single(&self, filtered_access: &FilteredAccess<T>) -> AccessConflicts {
// if the unfiltered access is incompatible, must check each pair // if the unfiltered access is incompatible, must check each pair
let mut conflicts = HashSet::new(); let mut conflicts = AccessConflicts::empty();
if !self.combined_access.is_compatible(filtered_access.access()) { if !self.combined_access.is_compatible(filtered_access.access()) {
for filtered in &self.filtered_accesses { for filtered in &self.filtered_accesses {
conflicts.extend(filtered.get_conflicts(filtered_access).into_iter()); conflicts.add(&filtered.get_conflicts(filtered_access));
} }
} }
conflicts.into_iter().collect() conflicts
} }
/// Adds the filtered access to the set. /// Adds the filtered access to the set.
@ -1030,7 +1081,7 @@ impl<T: SparseSetIndex> Default for FilteredAccessSet<T> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::query::access::AccessFilters; use crate::query::access::AccessFilters;
use crate::query::{Access, FilteredAccess, FilteredAccessSet}; use crate::query::{Access, AccessConflicts, FilteredAccess, FilteredAccessSet};
use fixedbitset::FixedBitSet; use fixedbitset::FixedBitSet;
use std::marker::PhantomData; use std::marker::PhantomData;
@ -1195,21 +1246,27 @@ mod tests {
access_b.add_component_read(0); access_b.add_component_read(0);
access_b.add_component_write(1); access_b.add_component_write(1);
assert_eq!(access_a.get_conflicts(&access_b), vec![1]); assert_eq!(access_a.get_conflicts(&access_b), vec![1_usize].into());
let mut access_c = Access::<usize>::default(); let mut access_c = Access::<usize>::default();
access_c.add_component_write(0); access_c.add_component_write(0);
access_c.add_component_write(1); access_c.add_component_write(1);
assert_eq!(access_a.get_conflicts(&access_c), vec![0, 1]); assert_eq!(
assert_eq!(access_b.get_conflicts(&access_c), vec![0, 1]); access_a.get_conflicts(&access_c),
vec![0_usize, 1_usize].into()
);
assert_eq!(
access_b.get_conflicts(&access_c),
vec![0_usize, 1_usize].into()
);
let mut access_d = Access::<usize>::default(); let mut access_d = Access::<usize>::default();
access_d.add_component_read(0); access_d.add_component_read(0);
assert_eq!(access_d.get_conflicts(&access_a), vec![]); assert_eq!(access_d.get_conflicts(&access_a), AccessConflicts::empty());
assert_eq!(access_d.get_conflicts(&access_b), vec![]); assert_eq!(access_d.get_conflicts(&access_b), AccessConflicts::empty());
assert_eq!(access_d.get_conflicts(&access_c), vec![0]); assert_eq!(access_d.get_conflicts(&access_c), vec![0_usize].into());
} }
#[test] #[test]
@ -1223,7 +1280,7 @@ mod tests {
let conflicts = access_a.get_conflicts_single(&filter_b); let conflicts = access_a.get_conflicts_single(&filter_b);
assert_eq!( assert_eq!(
&conflicts, &conflicts,
&[1_usize], &AccessConflicts::from(vec![1_usize]),
"access_a: {access_a:?}, filter_b: {filter_b:?}" "access_a: {access_a:?}, filter_b: {filter_b:?}"
); );
} }

View File

@ -23,6 +23,8 @@ use crate::{
world::World, world::World,
}; };
use crate::query::AccessConflicts;
use crate::storage::SparseSetIndex;
pub use stepping::Stepping; pub use stepping::Stepping;
/// Resource that stores [`Schedule`]s mapped to [`ScheduleLabel`]s excluding the current running [`Schedule`]. /// Resource that stores [`Schedule`]s mapped to [`ScheduleLabel`]s excluding the current running [`Schedule`].
@ -1363,14 +1365,22 @@ impl ScheduleGraph {
let access_a = system_a.component_access(); let access_a = system_a.component_access();
let access_b = system_b.component_access(); let access_b = system_b.component_access();
if !access_a.is_compatible(access_b) { if !access_a.is_compatible(access_b) {
let conflicts: Vec<_> = access_a match access_a.get_conflicts(access_b) {
.get_conflicts(access_b) AccessConflicts::Individual(conflicts) => {
.into_iter() let conflicts: Vec<_> = conflicts
.filter(|id| !ignored_ambiguities.contains(id)) .ones()
.collect(); .map(ComponentId::get_sparse_set_index)
.filter(|id| !ignored_ambiguities.contains(id))
if !conflicts.is_empty() { .collect();
conflicting_systems.push((a, b, conflicts)); if !conflicts.is_empty() {
conflicting_systems.push((a, b, conflicts));
}
}
AccessConflicts::All => {
// there is no specific component conflicting, but the systems are overall incompatible
// for example 2 systems with `Query<EntityMut>`
conflicting_systems.push((a, b, Vec::new()));
}
} }
} }
} }

View File

@ -312,9 +312,9 @@ where
assert_is_system(system); assert_is_system(system);
} }
/// Ensures that the provided system doesn't with itself. /// Ensures that the provided system doesn't conflict with itself.
/// ///
/// This function will panic if the provided system conflict with itself. /// This function will panic if the provided system conflict with itself.
/// ///
/// Note: this will run the system on an empty world. /// Note: this will run the system on an empty world.
pub fn assert_system_does_not_conflict<Out, Params, S: IntoSystem<(), Out, Params>>(sys: S) { pub fn assert_system_does_not_conflict<Out, Params, S: IntoSystem<(), Out, Params>>(sys: S) {
@ -340,10 +340,11 @@ impl<T> std::ops::DerefMut for In<T> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bevy_utils::default;
use std::any::TypeId; use std::any::TypeId;
use bevy_utils::default; use crate::prelude::EntityRef;
use crate::world::EntityMut;
use crate::{ use crate::{
self as bevy_ecs, self as bevy_ecs,
archetype::{ArchetypeComponentId, Archetypes}, archetype::{ArchetypeComponentId, Archetypes},
@ -1102,7 +1103,7 @@ mod tests {
.get_resource_id(TypeId::of::<B>()) .get_resource_id(TypeId::of::<B>())
.unwrap(); .unwrap();
let d_id = world.components().get_id(TypeId::of::<D>()).unwrap(); let d_id = world.components().get_id(TypeId::of::<D>()).unwrap();
assert_eq!(conflicts, vec![b_id, d_id]); assert_eq!(conflicts, vec![b_id, d_id].into());
} }
#[test] #[test]
@ -1608,6 +1609,33 @@ mod tests {
super::assert_system_does_not_conflict(system); super::assert_system_does_not_conflict(system);
} }
#[test]
#[should_panic(
expected = "error[B0001]: Query<bevy_ecs::world::entity_ref::EntityMut, ()> in system bevy_ecs::system::tests::assert_world_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without<T>` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001"
)]
fn assert_world_and_entity_mut_system_does_conflict() {
fn system(_query: &World, _q2: Query<EntityMut>) {}
super::assert_system_does_not_conflict(system);
}
#[test]
#[should_panic(
expected = "error[B0001]: Query<bevy_ecs::world::entity_ref::EntityMut, ()> in system bevy_ecs::system::tests::assert_entity_ref_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without<T>` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001"
)]
fn assert_entity_ref_and_entity_mut_system_does_conflict() {
fn system(_query: Query<EntityRef>, _q2: Query<EntityMut>) {}
super::assert_system_does_not_conflict(system);
}
#[test]
#[should_panic(
expected = "error[B0001]: Query<bevy_ecs::world::entity_ref::EntityMut, ()> in system bevy_ecs::system::tests::assert_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without<T>` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001"
)]
fn assert_entity_mut_system_does_conflict() {
fn system(_query: Query<EntityMut>, _q2: Query<EntityMut>) {}
super::assert_system_does_not_conflict(system);
}
#[test] #[test]
#[should_panic] #[should_panic]
fn panic_inside_system() { fn panic_inside_system() {

View File

@ -1,4 +1,6 @@
pub use crate::change_detection::{NonSendMut, Res, ResMut}; pub use crate::change_detection::{NonSendMut, Res, ResMut};
use crate::query::AccessConflicts;
use crate::storage::SparseSetIndex;
use crate::{ use crate::{
archetype::{Archetype, Archetypes}, archetype::{Archetype, Archetypes},
bundle::Bundles, bundle::Bundles,
@ -296,12 +298,22 @@ fn assert_component_access_compatibility(
if conflicts.is_empty() { if conflicts.is_empty() {
return; return;
} }
let conflicting_components = conflicts let accesses = match conflicts {
.into_iter() AccessConflicts::All => "",
.map(|component_id| world.components.get_info(component_id).unwrap().name()) AccessConflicts::Individual(indices) => &format!(
.collect::<Vec<&str>>(); " {}",
let accesses = conflicting_components.join(", "); indices
panic!("error[B0001]: Query<{query_type}, {filter_type}> in system {system_name} accesses component(s) {accesses} in a way that conflicts with a previous system parameter. Consider using `Without<T>` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001"); .ones()
.map(|index| world
.components
.get_info(ComponentId::get_sparse_set_index(index))
.unwrap()
.name())
.collect::<Vec<&str>>()
.join(", ")
),
};
panic!("error[B0001]: Query<{query_type}, {filter_type}> in system {system_name} accesses component(s){accesses} in a way that conflicts with a previous system parameter. Consider using `Without<T>` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001");
} }
/// A collection of potentially conflicting [`SystemParam`]s allowed by disjoint access. /// A collection of potentially conflicting [`SystemParam`]s allowed by disjoint access.