Remove resources from Access (#22910)

# Objective

There's a lot of code duplication in `access.rs`. The same logic is
duplicated between components and resources. This also takes up
unnecessary memory in `Access`, as it relies on bitsets spanning the
entire `ComponentId` range.

## Solution

Since resources are now a special kind of component, this can be
removed.

## Limitations

Since `!Send` data queries used `Access` resources, `!Send` data queries
now conflict with broad queries.
```rust
// 0.18
fn system(q1_: Query<EntityMut>, q2_: NonSend<R>) {} // valid, does not conflict

// 0.19
fn system(q1_: Query<EntityMut>, q2_: NonSend<R>) {} // invalid, does conflict
```
Given how rarely non-send data is used, I recommend using
```
// 0.19
fn system(q1_: Query<EntityMut, Without<R>>, q2_: NonSend<R>) {} // works again
```

If this is also unacceptable, this PR is blocked on the `!Send` data
removal from the ECS (or some hacky workaround).

## Extra Attention

@chescock brought `AssetChanged` to my attention. It has a weird access
pattern. See the following example:
```rust
fn system(c: Query<&mut AssetChanges<Mesh>>, r: Query<(), AssetChanged<Mesh>>) {}
```
System `c` registers access with `add_write` for `AssetChanges<Mesh>`,
while `r` registers access with `add_read` for both `Mesh` and
`AssetChanges<Mesh>`. This system is invalid, and I've added a test to
reflect that. However, since this stuff is tricky, I would like some
extra eyes on it. Currently, it looks *fine*.
This commit is contained in:
Trashtalk217
2026-03-03 00:48:04 +01:00
committed by GitHub
parent 5b953998ce
commit c89541a1af
19 changed files with 612 additions and 834 deletions
+9 -20
View File
@@ -246,28 +246,16 @@ unsafe impl<A: AsAssetId> WorldQuery for AssetChanged<A> {
component_access_set: &mut FilteredAccessSet,
_world: UnsafeWorldCell,
) {
let combined_access = component_access_set.combined_access();
assert!(
!combined_access.has_resource_write(state.resource_id),
"error[B0002]: AssetChanged<{}> in system {:?} conflicts with a previous system param. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<A>(),
system_name,
);
let mut filter = FilteredAccess::default();
filter.add_component_read(state.resource_id);
filter.add_resource_read(state.resource_id);
filter.add_read(state.resource_id);
filter.and_with(IS_RESOURCE);
assert!(component_access_set
.get_conflicts_single(&filter)
.is_empty(),
"error[B0002]: AssetChanged<{}> in system {:?} conflicts with a previous system param. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<A>(),
system_name,
);
component_access_set.add(filter);
let conflicts = component_access_set.get_conflicts_single(&filter);
if conflicts.is_empty() {
component_access_set.add(filter);
return;
}
panic!("error[B0002]: AssetChanged<{}> in system {:?} conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", DebugName::type_name::<A>(), system_name);
}
fn init_state(world: &mut World) -> AssetChangedState<A> {
@@ -326,6 +314,7 @@ mod tests {
use crate::tests::create_app;
use crate::{AssetEventSystems, Handle};
use alloc::{vec, vec::Vec};
use bevy_ecs::system::assert_is_system;
use core::num::NonZero;
use std::println;
@@ -336,7 +325,7 @@ mod tests {
component::Component,
message::MessageWriter,
resource::Resource,
system::{assert_is_system, Commands, IntoSystem, Local, Query, Res, ResMut},
system::{Commands, IntoSystem, Local, Query, Res, ResMut},
};
use bevy_reflect::TypePath;
+1 -3
View File
@@ -257,9 +257,7 @@ mod tests {
// A component access with an unrelated component
let mut component_access = FilteredAccess::default();
component_access
.access_mut()
.add_component_read(ComponentId::new(2));
component_access.access_mut().add_read(ComponentId::new(2));
let mut applied_access = component_access.clone();
filters.modify_access(&mut applied_access);
+2 -2
View File
@@ -1542,8 +1542,8 @@ mod tests {
let mut expected = FilteredAccess::default();
let a_id = world.components.get_id(TypeId::of::<A>()).unwrap();
let b_id = world.components.get_id(TypeId::of::<B>()).unwrap();
expected.add_component_write(a_id);
expected.add_component_read(b_id);
expected.add_write(a_id);
expected.add_read(b_id);
assert!(
query.component_access.eq(&expected),
"ComponentId access from query fetch and query filter should be combined"
File diff suppressed because it is too large Load Diff
+8 -66
View File
@@ -72,14 +72,8 @@ fn has_conflicts_large<'a, Q: QueryData>(
}
EcsAccessType::Component(EcsAccessLevel::ReadAll)
| EcsAccessType::Component(EcsAccessLevel::WriteAll) => true,
EcsAccessType::Resource(ResourceAccessLevel::Read(resource_id))
| EcsAccessType::Resource(ResourceAccessLevel::Write(resource_id)) => {
filter.check_insert(&resource_id.index())
}
EcsAccessType::Access(access) => {
if access.has_read_all_resources() || access.has_write_all_resources() {
true
} else if let Ok(component_iter) = access.try_iter_component_access() {
if let Ok(component_iter) = access.try_iter_access() {
let mut needs_check = false;
for kind in component_iter {
let index = match kind {
@@ -91,11 +85,6 @@ fn has_conflicts_large<'a, Q: QueryData>(
needs_check = true;
}
}
for resource_id in access.resource_reads_and_writes() {
if filter.check_insert(&resource_id.index()) {
needs_check = true;
}
}
needs_check
} else {
true
@@ -123,8 +112,6 @@ fn has_conflicts_large<'a, Q: QueryData>(
pub enum EcsAccessType<'a> {
/// Accesses [`Component`](crate::prelude::Component) data
Component(EcsAccessLevel),
/// Accesses [`Resource`](crate::prelude::Resource) data
Resource(ResourceAccessLevel),
/// borrowed access from [`WorldQuery::State`](crate::query::WorldQuery)
Access(&'a Access),
/// Does not access any data that can conflict.
@@ -146,32 +133,19 @@ impl<'a> EcsAccessType<'a> {
(Empty, _)
| (_, Empty)
| (Component(_), Resource(_))
| (Resource(_), Component(_))
// read only access doesn't conflict
| (Component(Read(_)), Component(Read(_)))
| (Component(ReadAll), Component(Read(_)))
| (Component(Read(_)), Component(ReadAll))
| (Component(ReadAll), Component(ReadAll))
| (Resource(ResourceAccessLevel::Read(_)), Resource(ResourceAccessLevel::Read(_))) => {
=> {
Ok(())
}
(Component(Read(id)), Component(Write(id_other)))
| (Component(Write(id)), Component(Read(id_other)))
| (Component(Write(id)), Component(Write(id_other)))
| (
Resource(ResourceAccessLevel::Read(id)),
Resource(ResourceAccessLevel::Write(id_other)),
)
| (
Resource(ResourceAccessLevel::Write(id)),
Resource(ResourceAccessLevel::Read(id_other)),
)
| (
Resource(ResourceAccessLevel::Write(id)),
Resource(ResourceAccessLevel::Write(id_other)),
) => if id == id_other {
=> if id == id_other {
Err(AccessConflictError(*self, other))
} else {
Ok(())
@@ -179,41 +153,28 @@ impl<'a> EcsAccessType<'a> {
// Borrowed Access
(Component(Read(component_id)), Access(access))
| (Access(access), Component(Read(component_id))) => if access.has_component_write(component_id) {
| (Access(access), Component(Read(component_id))) => if access.has_write(component_id) {
Err(AccessConflictError(*self, other))
} else {
Ok(())
},
(Component(Write(component_id)), Access(access))
| (Access(access), Component(Write(component_id))) => if access.has_component_read(component_id) {
| (Access(access), Component(Write(component_id))) => if access.has_read(component_id) {
Err(AccessConflictError(*self, other))
} else {
Ok(())
},
(Component(ReadAll), Access(access))
| (Access(access), Component(ReadAll)) => if access.has_any_component_write() {
| (Access(access), Component(ReadAll)) => if access.has_any_write() {
Err(AccessConflictError(*self, other))
} else {
Ok(())
},
(Component(WriteAll), Access(access))
| (Access(access), Component(WriteAll))=> if access.has_any_component_read() {
Err(AccessConflictError(*self, other))
} else {
Ok(())
},
(Resource(ResourceAccessLevel::Read(component_id)), Access(access))
| (Access(access), Resource(ResourceAccessLevel::Read(component_id))) => if access.has_resource_write(component_id) {
Err(AccessConflictError(*self, other))
} else {
Ok(())
},
(Resource(ResourceAccessLevel::Write(component_id)), Access(access))
| (Access(access), Resource(ResourceAccessLevel::Write(component_id))) => if access.has_resource_read(component_id) {
| (Access(access), Component(WriteAll))=> if access.has_any_read() {
Err(AccessConflictError(*self, other))
} else {
Ok(())
@@ -242,15 +203,6 @@ pub enum EcsAccessLevel {
WriteAll,
}
/// Access level needed by [`QueryData`] fetch to the resource.
#[derive(Copy, Clone, Debug, PartialEq, Hash)]
pub enum ResourceAccessLevel {
/// Reads the resource with [`ComponentId`]
Read(ComponentId),
/// Writes the resource with [`ComponentId`]
Write(ComponentId),
}
/// Error returned from [`EcsAccessType::is_compatible`]
pub struct AccessConflictError<'a>(EcsAccessType<'a>, EcsAccessType<'a>);
@@ -319,16 +271,6 @@ impl Display for AccessConflictError<'_> {
f,
"Access has a read that conflicts with component write all"
),
(Access(_), Resource(ResourceAccessLevel::Read(id)))
| (Resource(ResourceAccessLevel::Read(id)), Access(_)) => write!(
f,
"Access has a write that conflicts with resource {id:?} read."
),
(Access(_), Resource(ResourceAccessLevel::Write(id)))
| (Resource(ResourceAccessLevel::Write(id)), Access(_)) => write!(
f,
"Access has a read that conflicts with resource {id:?} write."
),
(Access(_), Access(_)) => write!(f, "Access conflicts with other Access"),
_ => {
@@ -355,7 +297,7 @@ impl Display for QueryAccessError {
QueryAccessError::ComponentNotRegistered => {
write!(
f,
"At least one component in Q was not registered in world.
"At least one component in Q was not registered in world.
Consider calling `World::register_component`"
)
}
+2 -2
View File
@@ -170,14 +170,14 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
/// Adds `&T` to the [`FilteredAccess`] of self.
pub fn ref_id(&mut self, id: ComponentId) -> &mut Self {
self.with_id(id);
self.access.add_component_read(id);
self.access.add_read(id);
self
}
/// Adds `&mut T` to the [`FilteredAccess`] of self.
pub fn mut_id(&mut self, id: ComponentId) -> &mut Self {
self.with_id(id);
self.access.add_component_write(id);
self.access.add_write(id);
self
}
+20 -20
View File
@@ -955,10 +955,10 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> {
fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) {
assert!(
!access.access().has_any_component_write(),
!access.access().has_any_write(),
"EntityRef conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",
);
access.read_all_components();
access.read_all();
}
fn init_state(_world: &mut World) {}
@@ -1071,10 +1071,10 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> {
fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) {
assert!(
!access.access().has_any_component_read(),
!access.access().has_any_read(),
"EntityMut conflicts with a previous access in this query. Exclusive access cannot coincide with any other accesses.",
);
access.write_all_components();
access.write_all();
}
fn init_state(_world: &mut World) {}
@@ -1449,16 +1449,16 @@ where
fn init_state(world: &mut World) -> Self::State {
let mut access = Access::new();
access.read_all_components();
access.read_all();
for id in B::component_ids(&mut world.components_registrator()) {
access.remove_component_read(id);
access.remove_read(id);
}
access
}
fn get_state(components: &Components) -> Option<Self::State> {
let mut access = Access::new();
access.read_all_components();
access.read_all();
// If the component isn't registered, we don't have a `ComponentId`
// to use to exclude its access.
// Rather than fail, just try to take additional access.
@@ -1466,7 +1466,7 @@ where
// Since the component isn't registered, there are no entities with that
// component, and the extra access will usually have no effect.
for id in B::get_component_ids(components).flatten() {
access.remove_component_read(id);
access.remove_read(id);
}
Some(access)
}
@@ -1572,16 +1572,16 @@ where
fn init_state(world: &mut World) -> Self::State {
let mut access = Access::new();
access.write_all_components();
access.write_all();
for id in B::component_ids(&mut world.components_registrator()) {
access.remove_component_read(id);
access.remove_read(id);
}
access
}
fn get_state(components: &Components) -> Option<Self::State> {
let mut access = Access::new();
access.write_all_components();
access.write_all();
// If the component isn't registered, we don't have a `ComponentId`
// to use to exclude its access.
// Rather than fail, just try to take additional access.
@@ -1589,7 +1589,7 @@ where
// Since the component isn't registered, there are no entities with that
// component, and the extra access will usually have no effect.
for id in B::get_component_ids(components).flatten() {
access.remove_component_read(id);
access.remove_read(id);
}
Some(access)
}
@@ -1841,11 +1841,11 @@ unsafe impl<T: Component> WorldQuery for &T {
fn update_component_access(&component_id: &ComponentId, access: &mut FilteredAccess) {
assert!(
!access.access().has_component_write(component_id),
!access.access().has_write(component_id),
"&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",
DebugName::type_name::<T>(),
);
access.add_component_read(component_id);
access.add_read(component_id);
}
fn init_state(world: &mut World) -> ComponentId {
@@ -2065,11 +2065,11 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> {
fn update_component_access(&component_id: &ComponentId, access: &mut FilteredAccess) {
assert!(
!access.access().has_component_write(component_id),
!access.access().has_write(component_id),
"&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",
DebugName::type_name::<T>(),
);
access.add_component_read(component_id);
access.add_read(component_id);
}
fn init_state(world: &mut World) -> ComponentId {
@@ -2332,11 +2332,11 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T {
fn update_component_access(&component_id: &ComponentId, access: &mut FilteredAccess) {
assert!(
!access.access().has_component_read(component_id),
!access.access().has_read(component_id),
"&mut {} conflicts with a previous access in this query. Mutable component access must be unique.",
DebugName::type_name::<T>(),
);
access.add_component_write(component_id);
access.add_write(component_id);
}
fn init_state(world: &mut World) -> ComponentId {
@@ -2539,11 +2539,11 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> {
// Update component access here instead of in `<&mut T as WorldQuery>` to avoid erroneously referencing
// `&mut T` in error message.
assert!(
!access.access().has_component_read(component_id),
!access.access().has_read(component_id),
"Mut<{}> conflicts with a previous access in this query. Mutable component access mut be unique.",
DebugName::type_name::<T>(),
);
access.add_component_write(component_id);
access.add_write(component_id);
}
// Forwarded to `&mut T`
+4 -4
View File
@@ -826,10 +826,10 @@ unsafe impl<T: Component> WorldQuery for Added<T> {
#[inline]
fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) {
if access.access().has_component_write(id) {
if access.access().has_write(id) {
panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", DebugName::type_name::<T>());
}
access.add_component_read(id);
access.add_read(id);
}
fn init_state(world: &mut World) -> ComponentId {
@@ -1053,10 +1053,10 @@ unsafe impl<T: Component> WorldQuery for Changed<T> {
#[inline]
fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) {
if access.access().has_component_write(id) {
if access.access().has_write(id) {
panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", DebugName::type_name::<T>());
}
access.add_component_read(id);
access.add_read(id);
}
fn init_state(world: &mut World) -> ComponentId {
+6 -119
View File
@@ -113,18 +113,15 @@ impl<T> DebugCheckedUnwrap for Option<T> {
#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
mod tests {
use crate::{
archetype::Archetype,
change_detection::Tick,
component::{Component, ComponentId, Components},
prelude::{AnyOf, Changed, Entity, Or, QueryState, Resource, With, Without},
component::Component,
prelude::{AnyOf, Changed, Entity, Or, QueryState, With, Without},
query::{
ArchetypeFilter, ArchetypeQueryData, FilteredAccess, Has, IterQueryData,
QueryCombinationIter, QueryData, QueryFilter, ReadOnlyQueryData, WorldQuery,
ArchetypeFilter, ArchetypeQueryData, Has, QueryCombinationIter, QueryData, QueryFilter,
ReadOnlyQueryData,
},
schedule::{IntoScheduleConfigs, Schedule},
storage::{Table, TableRow},
system::{assert_is_system, IntoSystem, Query, System, SystemState},
world::{unsafe_world_cell::UnsafeWorldCell, World},
system::{IntoSystem, Query, System, SystemState},
world::World,
};
use alloc::{vec, vec::Vec};
use core::{any::type_name, fmt::Debug, hash::Hash};
@@ -817,114 +814,4 @@ mod tests {
let values = world.query::<&B>().iter(&world).collect::<Vec<&B>>();
assert_eq!(values, vec![&B(2)]);
}
#[derive(Resource)]
struct R;
/// `QueryData` that performs read access on R to test that resource access is tracked
struct ReadsRData;
// SAFETY:
// `update_component_access` adds resource read access for `R`.
unsafe impl WorldQuery for ReadsRData {
type Fetch<'w> = ();
type State = ComponentId;
fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {}
unsafe fn init_fetch<'w, 's>(
_world: UnsafeWorldCell<'w>,
_state: &'s Self::State,
_last_run: Tick,
_this_run: Tick,
) -> Self::Fetch<'w> {
}
const IS_DENSE: bool = true;
#[inline]
unsafe fn set_archetype<'w, 's>(
_fetch: &mut Self::Fetch<'w>,
_state: &'s Self::State,
_archetype: &'w Archetype,
_table: &Table,
) {
}
#[inline]
unsafe fn set_table<'w, 's>(
_fetch: &mut Self::Fetch<'w>,
_state: &'s Self::State,
_table: &'w Table,
) {
}
fn update_component_access(&component_id: &Self::State, access: &mut FilteredAccess) {
assert!(
!access.access().has_resource_write(component_id),
"ReadsRData conflicts with a previous access in this query. Shared access cannot coincide with exclusive access."
);
access.add_resource_read(component_id);
}
fn init_state(world: &mut World) -> Self::State {
world.components_registrator().register_component::<R>()
}
fn get_state(components: &Components) -> Option<Self::State> {
components.component_id::<R>()
}
fn matches_component_set(
_state: &Self::State,
_set_contains_id: &impl Fn(ComponentId) -> bool,
) -> bool {
true
}
}
// SAFETY: `Self` is the same as `Self::ReadOnly`
unsafe impl QueryData for ReadsRData {
const IS_READ_ONLY: bool = true;
const IS_ARCHETYPAL: bool = true;
type ReadOnly = Self;
type Item<'w, 's> = ();
fn shrink<'wlong: 'wshort, 'wshort, 's>(
_item: Self::Item<'wlong, 's>,
) -> Self::Item<'wshort, 's> {
}
#[inline(always)]
unsafe fn fetch<'w, 's>(
_state: &'s Self::State,
_fetch: &mut Self::Fetch<'w>,
_entity: Entity,
_table_row: TableRow,
) -> Option<Self::Item<'w, 's>> {
Some(())
}
fn iter_access(
state: &Self::State,
) -> impl Iterator<Item = super::access_iter::EcsAccessType<'_>> {
core::iter::once(super::access_iter::EcsAccessType::Resource(
super::access_iter::ResourceAccessLevel::Read(*state),
))
}
}
// SAFETY: access is read only
unsafe impl ReadOnlyQueryData for ReadsRData {}
/// SAFETY: access is read only
unsafe impl IterQueryData for ReadsRData {}
impl ArchetypeQueryData for ReadsRData {}
#[test]
fn read_res_read_res_no_conflict() {
fn system(_q1: Query<ReadsRData, With<A>>, _q2: Query<ReadsRData, Without<A>>) {}
assert_is_system(system);
}
}
+2 -2
View File
@@ -1012,8 +1012,8 @@ mod tests {
let _ = schedule.initialize(&mut world);
// this should fail, since resources are components
assert_eq!(schedule.graph().conflicting_systems().len(), 1);
// this should fail, since resources are components and non_sends also do access with components
assert_eq!(schedule.graph().conflicting_systems().len(), 2);
schedule = Schedule::default();
schedule.add_systems((
+1 -1
View File
@@ -252,7 +252,7 @@ where
// We might need to read the default error handler after the component
// systems have run to report failures.
let error_resource = world.register_resource::<crate::error::DefaultErrorHandler>();
a_access.add_unfiltered_resource_read(error_resource);
a_access.add_resource_read(error_resource);
a_access
}
+80 -68
View File
@@ -749,30 +749,24 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> {
&component_id: &Self::State,
system_meta: &mut SystemMeta,
component_access_set: &mut FilteredAccessSet,
_world: &mut World,
world: &mut World,
) {
let combined_access = component_access_set.combined_access();
assert!(
!combined_access.has_resource_write(component_id),
"error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<T>(),
system_meta.name,
);
let mut filter = FilteredAccess::default();
filter.add_component_read(component_id);
filter.add_resource_read(component_id);
filter.add_read(component_id);
filter.and_with(IS_RESOURCE);
assert!(component_access_set
.get_conflicts_single(&filter)
.is_empty(),
"error[B0002]: Res<{}> in system {} conflicts with a previous query. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<T>(),
system_meta.name
);
let conflicts = component_access_set.get_conflicts_single(&filter);
if conflicts.is_empty() {
component_access_set.add(filter);
return;
}
component_access_set.add(filter);
let mut accesses = conflicts.format_conflict_list(world.as_unsafe_world_cell());
// Access list may be empty (if access to all components requested)
if !accesses.is_empty() {
accesses.push(' ');
}
panic!("error[B0002]: Res<{}> in system {} conflicts with a previous system parameter. Consider removing the duplicate access using `Without<IsResource>` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0002", DebugName::type_name::<T>(), system_meta.name);
}
#[inline]
@@ -837,33 +831,24 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> {
&component_id: &Self::State,
system_meta: &mut SystemMeta,
component_access_set: &mut FilteredAccessSet,
_world: &mut World,
world: &mut World,
) {
let combined_access = component_access_set.combined_access();
if combined_access.has_resource_write(component_id) {
panic!(
"error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<T>(), system_meta.name);
} else if combined_access.has_resource_read(component_id) {
panic!(
"error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<T>(), system_meta.name);
}
let mut filter = FilteredAccess::default();
filter.add_component_write(component_id);
filter.add_resource_write(component_id);
filter.add_write(component_id);
filter.and_with(IS_RESOURCE);
assert!(component_access_set
.get_conflicts_single(&filter)
.is_empty(),
"error[B0002]: ResMut<{}> in system {} conflicts with a previous query. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<T>(),
system_meta.name
);
let conflicts = component_access_set.get_conflicts_single(&filter);
if conflicts.is_empty() {
component_access_set.add(filter);
return;
}
component_access_set.add(filter);
let mut accesses = conflicts.format_conflict_list(world.as_unsafe_world_cell());
// Access list may be empty (if access to all components requested)
if !accesses.is_empty() {
accesses.push(' ');
}
panic!("error[B0002]: ResMut<{}> in system {} conflicts with a previous system parameter. Consider removing the duplicate access or using `Without<IsResource>` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0002", DebugName::type_name::<T>(), system_meta.name);
}
#[inline]
@@ -1471,12 +1456,12 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> {
let combined_access = component_access_set.combined_access();
assert!(
!combined_access.has_resource_write(component_id),
!combined_access.has_write(component_id),
"error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<T>(),
system_meta.name,
);
component_access_set.add_unfiltered_resource_read(component_id);
component_access_set.add_unfiltered_component_read(component_id);
}
#[inline]
@@ -1541,16 +1526,16 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> {
system_meta.set_non_send();
let combined_access = component_access_set.combined_access();
if combined_access.has_resource_write(component_id) {
if combined_access.has_write(component_id) {
panic!(
"error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<T>(), system_meta.name);
} else if combined_access.has_resource_read(component_id) {
} else if combined_access.has_read(component_id) {
panic!(
"error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002",
DebugName::type_name::<T>(), system_meta.name);
}
component_access_set.add_unfiltered_resource_write(component_id);
component_access_set.add_unfiltered_component_write(component_id);
}
#[inline]
@@ -2773,13 +2758,10 @@ unsafe impl SystemParam for FilteredResources<'_, '_> {
panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002");
}
if access.has_read_all_resources() {
component_access_set.add_unfiltered_read_all_resources();
} else {
for component_id in access.resource_reads_and_writes() {
component_access_set.add_unfiltered_resource_read(component_id);
}
}
let mut filter = FilteredAccess::matches_everything();
filter.access_mut().extend(access);
filter.and_with(IS_RESOURCE);
component_access_set.add(filter);
}
unsafe fn get_param<'world, 'state>(
@@ -2822,21 +2804,10 @@ unsafe impl SystemParam for FilteredResourcesMut<'_, '_> {
panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002");
}
if access.has_read_all_resources() {
component_access_set.add_unfiltered_read_all_resources();
} else {
for component_id in access.resource_reads() {
component_access_set.add_unfiltered_resource_read(component_id);
}
}
if access.has_write_all_resources() {
component_access_set.add_unfiltered_write_all_resources();
} else {
for component_id in access.resource_writes() {
component_access_set.add_unfiltered_resource_write(component_id);
}
}
let mut filter = FilteredAccess::matches_everything();
filter.access_mut().extend(access);
filter.and_with(IS_RESOURCE);
component_access_set.add(filter);
}
unsafe fn get_param<'world, 'state>(
@@ -2941,7 +2912,10 @@ impl Display for SystemParamValidationError {
#[cfg(test)]
mod tests {
use super::*;
use crate::query::Without;
use crate::resource::IsResource;
use crate::system::assert_is_system;
use crate::world::EntityMut;
use core::cell::RefCell;
#[test]
@@ -2960,6 +2934,44 @@ mod tests {
schedule.run(&mut world);
}
#[test]
#[should_panic]
fn non_send_and_entities() {
#[derive(Resource)]
struct A(usize);
fn my_system(mut ns: NonSendMut<A>, _: Query<EntityMut>) {
ns.0 += 1;
}
assert_is_system(my_system);
}
#[test]
#[should_panic]
fn res_and_entities() {
#[derive(Resource)]
struct A(usize);
fn my_system(mut res: ResMut<A>, _: Query<EntityMut>) {
res.0 += 1;
}
assert_is_system(my_system);
}
#[test]
fn res_and_entities_filtered() {
#[derive(Resource)]
struct A(usize);
fn res_system(mut res: ResMut<A>, _: Query<EntityMut, Without<IsResource>>) {
res.0 += 1;
}
assert_is_system(res_system);
fn non_send_system(mut ns: NonSendMut<A>, _: Query<EntityMut, Without<A>>) {
ns.0 += 1;
}
assert_is_system(non_send_system);
}
// Compile test for https://github.com/bevyengine/bevy/pull/2838.
#[test]
fn system_param_generic_bounds() {
@@ -144,7 +144,7 @@ impl<'w, 's> FilteredEntityRef<'w, 's> {
.components()
.get_valid_id(TypeId::of::<T>())?;
self.access
.has_component_read(id)
.has_read(id)
// SAFETY: We have read access
.then(|| unsafe { self.entity.get() })
.flatten()
@@ -162,7 +162,7 @@ impl<'w, 's> FilteredEntityRef<'w, 's> {
.components()
.get_valid_id(TypeId::of::<T>())?;
self.access
.has_component_read(id)
.has_read(id)
// SAFETY: We have read access
.then(|| unsafe { self.entity.get_ref() })
.flatten()
@@ -178,7 +178,7 @@ impl<'w, 's> FilteredEntityRef<'w, 's> {
.components()
.get_valid_id(TypeId::of::<T>())?;
self.access
.has_component_read(id)
.has_read(id)
// SAFETY: We have read access
.then(|| unsafe { self.entity.get_change_ticks::<T>() })
.flatten()
@@ -193,7 +193,7 @@ impl<'w, 's> FilteredEntityRef<'w, 's> {
#[inline]
pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option<ComponentTicks> {
self.access
.has_component_read(component_id)
.has_read(component_id)
// SAFETY: We have read access
.then(|| unsafe { self.entity.get_change_ticks_by_id(component_id) })
.flatten()
@@ -210,7 +210,7 @@ impl<'w, 's> FilteredEntityRef<'w, 's> {
#[inline]
pub fn get_by_id(&self, component_id: ComponentId) -> Option<Ptr<'w>> {
self.access
.has_component_read(component_id)
.has_read(component_id)
// SAFETY: We have read access
.then(|| unsafe { self.entity.get_by_id(component_id) })
.flatten()
@@ -575,7 +575,7 @@ impl<'w, 's> FilteredEntityMut<'w, 's> {
.components()
.get_valid_id(TypeId::of::<T>())?;
self.access
.has_component_write(id)
.has_write(id)
// SAFETY: We have permission to access the component mutable
// and we promise to not create other references to the same component
.then(|| unsafe { self.entity.get_mut() })
@@ -608,7 +608,7 @@ impl<'w, 's> FilteredEntityMut<'w, 's> {
.components()
.get_valid_id(TypeId::of::<T>())?;
self.access
.has_component_write(id)
.has_write(id)
// SAFETY:
// - We have write access
// - Caller ensures `T` is a mutable component
@@ -686,7 +686,7 @@ impl<'w, 's> FilteredEntityMut<'w, 's> {
component_id: ComponentId,
) -> Option<MutUntyped<'_>> {
self.access
.has_component_write(component_id)
.has_write(component_id)
// SAFETY: We have permission to access the component mutable
// and we promise to not create other references to the same component
.then(|| unsafe { self.entity.get_mut_by_id(component_id).ok() })
+14 -40
View File
@@ -149,7 +149,7 @@ impl<'w, 's> FilteredResources<'w, 's> {
/// Note that [`Self::get()`] may still return `Err` if the resource does not exist.
pub fn has_read<R: Resource>(&self) -> bool {
let component_id = self.world.components().component_id::<R>();
component_id.is_some_and(|component_id| self.access.has_resource_read(component_id))
component_id.is_some_and(|component_id| self.access.has_read(component_id))
}
/// Gets a reference to the resource of the given type if it exists and the `FilteredResources` has access to it.
@@ -159,7 +159,7 @@ impl<'w, 's> FilteredResources<'w, 's> {
.components()
.valid_component_id::<R>()
.ok_or(ResourceFetchError::NotRegistered)?;
if !self.access.has_resource_read(component_id) {
if !self.access.has_read(component_id) {
return Err(ResourceFetchError::NoResourceAccess(component_id));
}
@@ -179,7 +179,7 @@ impl<'w, 's> FilteredResources<'w, 's> {
/// Gets a pointer to the resource with the given [`ComponentId`] if it exists and the `FilteredResources` has access to it.
pub fn get_by_id(&self, component_id: ComponentId) -> Result<Ptr<'w>, ResourceFetchError> {
if !self.access.has_resource_read(component_id) {
if !self.access.has_read(component_id) {
return Err(ResourceFetchError::NoResourceAccess(component_id));
}
// SAFETY: We have read access to this resource
@@ -220,14 +220,7 @@ impl<'w, 's> From<&'w FilteredResourcesMut<'_, 's>> for FilteredResources<'w, 's
impl<'w> From<&'w World> for FilteredResources<'w, 'static> {
fn from(value: &'w World) -> Self {
const READ_ALL_RESOURCES: &Access = {
const ACCESS: Access = {
let mut access = Access::new();
access.read_all_resources();
access
};
&ACCESS
};
const READ_ALL_RESOURCES: &Access = const { &Access::new_read_all() };
let last_run = value.last_change_tick();
let this_run = value.read_change_tick();
@@ -417,14 +410,14 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> {
/// Note that [`Self::get()`] may still return `Err` if the resource does not exist.
pub fn has_read<R: Resource>(&self) -> bool {
let component_id = self.world.components().component_id::<R>();
component_id.is_some_and(|component_id| self.access.has_resource_read(component_id))
component_id.is_some_and(|component_id| self.access.has_read(component_id))
}
/// Returns `true` if the `FilteredResources` has write access to the given resource.
/// Note that [`Self::get_mut()`] may still return `Err` if the resource does not exist.
pub fn has_write<R: Resource>(&self) -> bool {
let component_id = self.world.components().component_id::<R>();
component_id.is_some_and(|component_id| self.access.has_resource_write(component_id))
component_id.is_some_and(|component_id| self.access.has_write(component_id))
}
/// Gets a reference to the resource of the given type if it exists and the `FilteredResources` has access to it.
@@ -489,7 +482,7 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> {
&mut self,
component_id: ComponentId,
) -> Result<MutUntyped<'w>, ResourceFetchError> {
if !self.access.has_resource_write(component_id) {
if !self.access.has_write(component_id) {
return Err(ResourceFetchError::NoResourceAccess(component_id));
}
@@ -510,14 +503,7 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> {
impl<'w> From<&'w mut World> for FilteredResourcesMut<'w, 'static> {
fn from(value: &'w mut World) -> Self {
const WRITE_ALL_RESOURCES: &Access = {
const ACCESS: Access = {
let mut access = Access::new();
access.write_all_resources();
access
};
&ACCESS
};
const WRITE_ALL_RESOURCES: &Access = const { &Access::new_write_all() };
let last_run = value.last_change_tick();
let this_run = value.change_tick();
@@ -557,10 +543,7 @@ impl<'w> FilteredResourcesBuilder<'w> {
/// Add accesses required to read all resources.
pub fn add_read_all(&mut self) -> &mut Self {
self.access.read_all_resources();
for &component_id in self.world.resource_entities().indices() {
self.access.add_component_read(component_id);
}
self.access.read_all();
self
}
@@ -575,8 +558,7 @@ impl<'w> FilteredResourcesBuilder<'w> {
/// Add accesses required to read the resource with the given [`ComponentId`].
pub fn add_read_by_id(&mut self, component_id: ComponentId) -> &mut Self {
self.access.add_resource_read(component_id);
self.access.add_component_read(component_id);
self.access.add_read(component_id);
self
}
@@ -610,10 +592,7 @@ impl<'w> FilteredResourcesMutBuilder<'w> {
/// Add accesses required to read all resources.
pub fn add_read_all(&mut self) -> &mut Self {
self.access.read_all_resources();
for &component_id in self.world.resource_entities().indices() {
self.access.add_component_read(component_id);
}
self.access.read_all();
self
}
@@ -628,17 +607,13 @@ impl<'w> FilteredResourcesMutBuilder<'w> {
/// Add accesses required to read the resource with the given [`ComponentId`].
pub fn add_read_by_id(&mut self, component_id: ComponentId) -> &mut Self {
self.access.add_resource_read(component_id);
self.access.add_component_read(component_id);
self.access.add_read(component_id);
self
}
/// Add accesses required to get mutable access to all resources.
pub fn add_write_all(&mut self) -> &mut Self {
self.access.write_all_resources();
for &component_id in self.world.resource_entities().indices() {
self.access.add_component_write(component_id);
}
self.access.write_all();
self
}
@@ -653,8 +628,7 @@ impl<'w> FilteredResourcesMutBuilder<'w> {
/// Add accesses required to get mutable access to the resource with the given [`ComponentId`].
pub fn add_write_by_id(&mut self, component_id: ComponentId) -> &mut Self {
self.access.add_resource_write(component_id);
self.access.add_component_write(component_id);
self.access.add_write(component_id);
self
}
@@ -249,7 +249,7 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam
component_access_set: &mut FilteredAccessSet,
world: &mut World,
) {
component_access_set.add_unfiltered_resource_read(state.resource_id);
component_access_set.add_resource_read(state.resource_id);
<Query<'_, '_, D, F> as SystemParam>::init_access(
&state.query_state,
+4
View File
@@ -89,3 +89,7 @@ fn main() {
.run();
}
```
## Note
If you run this error while using resources, check out [B0001](https://bevy.org/learn/errors/b0002) for additional information.
+36
View File
@@ -42,3 +42,39 @@ fn main() {
.run();
}
```
## Resources as Components
Under the hood, resources *are* components. The consequences of this are usually nothing more than an implementation detail, however, you may run into an issue where the following system throws an error:
```rust,no_run
# use bevy::prelude::*;
# #[derive(Resource)]
# struct MyResource;
fn system(all_entities: Query<EntityMut>, res: Res<MyResource>) {}
```
It's not possible to both query all entities and a resource when resources are stored on an entity.
To fix this, you should either constrain the `all_entities` query (you usually don't need to query *every* single entity), or exclude `MyResource` with `Without<MyResource>` or `Without<IsResource>`.
Here, `IsResource`, is a marker component on every single entity that holds a resource, so it's very convenient if you want to exclude resources.
Queries that might conflict with `Res` or `ResMut` include:
- `Query<()>`
- `Query<Entity>`
- `Query<EntityMut>`
- `Query<EntityRef>`
- `Query<EntityMutExcept>`
- `Query<EntityRefExcept>`
- `Query<Option<&T>>`
You can run into the same problem with non-send data:
```rust,no_run
# use bevy::prelude::*;
# #[derive(Resource)]
# struct MyNonSend;
fn system(all_entities: Query<EntityMut>, some_non_send: NonSend<MyNonSend>) {}
```
This can be fixed by adding a `Without<MyNonSend>` filter to the query.
+1 -1
View File
@@ -158,7 +158,7 @@ fn main() {
query.iter_mut(&mut world).for_each(|filtered_entity| {
let terms = filtered_entity
.access()
.try_iter_component_access()
.try_iter_access()
.unwrap()
.map(|component_access| {
let id = *component_access.index();
@@ -1,6 +1,6 @@
---
title: Resources as Components
pull_requests: [20934]
pull_requests: [20934, 22910, 22911, 22919, 22930]
---
## `#[derive(Resource)]` implements the `Component` trait
@@ -34,6 +34,35 @@ The `ReflectResource` is a ZST (zero-sized type) in 0.19 and only functions to s
Instead, `#[reflect(Resource)]` also reflects the `Component` trait, so use `ReflectComponent` instead.
This is likely to show up in code that uses reflection, like BRP (Bevy Reflect Protocol) and `bevy_scene`.
## Broad Queries and System Conflicts
Now that resources are components, they can be queried using 'broad' queries. These are queries that query all entities. Examples include:
- `Query<()>`
- `Query<Entity>`
- `Query<EntityMut>`
- `Query<EntityRef>`
- `Query<EntityMutExcept>`
- `Query<EntityRefExcept>`
- `Query<Option<&T>>`
These should rarely come up in real games, but if they do, they might conflict with resource access, i.e.
```rust
fn system(entity_query: Query<EntityMut>, some_resource: Res<MyResource>) {} // err! entity_query conflicts with some_resource
```
To fix this, you can narrow down the query by using either the `Without<MyResource>` or `Without<IsResource>` filter.
The `IsResource` marker is attached to all resource entities, so it always filters them out.
The same is true for non-send data:
```rust
fn system(entity_query: Query<EntityMut>, some_non_send: NonSend<MyNonSend>) {} // err! entity_query conflicts with some_resource
```
This can be fixed by adding a `Without<MyNonSend>` filter to the query.
## Renaming Non-Send Resources to Non-Send Data
Previously there were two types of resources: `Send` resources and `!Send` resources.
@@ -75,7 +104,43 @@ The registration process for components and resources is very similar and now th
- `ComponentsQueuedRegistrator::queue_register_resource` was deprecated in favor of `ComponentsQueuedRegistrator::queue_register_component`.
- `ComponentDescriptor::new_resource` was deprecated in favor of `ComponentDescriptor::new`
- `ComponentDescriptor::new_resource` was deprecated in favor of `ComponentDescriptor::new`
- `World::register_resource_with_descriptor was renamed to World::register_non_send_with_descriptor`.
- `World::register_resource_with_descriptor` was renamed to `World::register_non_send_with_descriptor`.
## Access
Resources were also removed from `Access`, which keeps track what data any given query / system has access to.
- `Access::add_component_read` and `Access::add_resource_read` were deprecated in favor of `Access::add_read`.
- `Access::add_component_write` and `Access::add_resource_write` were deprecated in favor of `Access::add_write`.
- `Access::remove_component_read` was deprecated in favor of `Access::remove_read`.
- `Access::remove_component_write` was deprecated in favor of `Access::remove_write`.
- `Access::has_component_read` and `Access::has_resource_read` were deprecated in favor of `Access::has_read`.
- `Access::has_any_component_read` and `Access::has_any_resource_read` were deprecated in favor of `Access::has_any_read`.
- `Access::has_component_write` and `Access::has_resource_write` were deprecated in favor of `Access::has_write`.
- `Access::has_any_component_write` and `Access::has_any_resource_write` were deprecated in favor of `Access::has_any_write`.
- `Access::read_all_components` was deprecated in favor of `Access::read_all`.
- `Access::write_all_components` was deprecated in favor of `Access::write_all`.
- `Access::read_all_resources` and `Access::write_all_resources` were removed.
- `Access::has_read_all_components` was deprecated in favor of `Access::has_read_all`.
- `Access::has_write_all_components` was deprecated in favor of `Access::has_write_all`.
- `Access::has_read_all_resources` and `Access::has_write_all_resources` were removed.
- `Access::is_components_compatible` was deprecated in favor of `Access::is_compatible`.
- `Access::is_resources_compatible` was removed.
- `Access::is_subset_components` was deprecated in favor of `Access::is_subset`.
- `Access::is_subset_resources` was removed.
- `Access::resource_reads_and_writes`, `Access::resource_reads`, `Access::resource_writes` were removed.
- `Access::try_iter_component_access` was deprecated in favor of `Access::try_iter_access`.
- `FilteredAccess::add_component_read` was deprecated in favor of `FilteredAccess::add_read`.
- `FilteredAccess::add_component_write` was deprecated in favor of `FilteredAccess::add_write`.
- `FilteredAccess::add_resource_read` and `FilteredAccess::add_resource_write` were removed.
- `FilteredAccess::read_all_components` was deprecated in favor of `FilteredAccess::read_all`.
- `FilteredAccess::write_all_components` was deprecated in favor of `FilteredAccess::write_all`.
- `FilteredAccessSet::add_unfiltered_resource_read` was deprecated in favor of `FilteredAccessSet::add_resource_read`.
- `FilteredAccessSet::add_unfiltered_resource_write` was deprecated in favor of `FilteredAccessSet::add_resource_write`.
Due to the split storage it used to be possible to both access an entity and a resource in a `WorldQuery` implementor.
This is no longer valid. In order to access multiple different entities for a `WorldQuery` implementation, use `WorldQuery::init_nested_access`.
See the implementation of `WorldQuery` for `AssetChanged` for an example of how this can be done correctly.
## Miscellaneous