diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 3cd89b77e2b93..f5925292baef0 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -16,6 +16,7 @@ pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; pub mod removal_detection; +pub mod resource_bundle; pub mod schedule; pub mod storage; pub mod system; @@ -1403,6 +1404,28 @@ mod tests { query.iter(&world_b).for_each(|_| {}); } + #[derive(Resource)] + struct SmallNum(i8); + + #[derive(Resource)] + struct Num(isize); + + #[derive(Resource)] + struct BigNum(i128); + + #[test] + #[should_panic] + #[cfg(not(miri))] + fn access_conflict_in_resource_bundle() { + let mut world = World::new(); + world.insert_resource(SmallNum(1)); + world.insert_resource(Num(1_000)); + + let (_small, _num, _num_mut) = world + .get_resources_mut::<(SmallNum, Num, Num)>() // This is an access conflict! + .unwrap(); + } + #[test] fn resource_scope() { let mut world = World::default(); diff --git a/crates/bevy_ecs/src/resource_bundle.rs b/crates/bevy_ecs/src/resource_bundle.rs new file mode 100644 index 0000000000000..c568fbbe485f8 --- /dev/null +++ b/crates/bevy_ecs/src/resource_bundle.rs @@ -0,0 +1,148 @@ +//! This module contains the logic for bundling up resources together. +use bevy_utils::{all_tuples, TypeIdSet}; +use std::any::TypeId; + +use crate::{ + prelude::Mut, + system::{Res, Resource}, + world::unsafe_world_cell::UnsafeWorldCell, +}; + +/// Bundle of resources. With this trait we can fetch multiple resources at once from a world. +pub trait ResourceBundle { + /// Write access to the resources of this resource bundle. This type should provide write access, like `&mut R` or `ResMut` + type WriteAccess<'a>; + /// Read-only access to the resources of this resource bundle. This type should provide read-only access, like `&R` or `Res` + type ReadOnlyAccess<'a>; + /// Read-only, change-detection-enabled access to the resources of this resource bundle. + /// This type should provide read-only, change-detection-enabled access like `Res` or `Ref` + type ReadOnlySmartRefAccess<'a>; + /// Get write access to the resources in the bundle. + /// + /// # Safety + /// The caller must ensure that each resource in this bundle is safe to access mutably. + /// For example, if `R` is in the bundle, there should not be any other valid references to R. + unsafe fn fetch_write_access(world: UnsafeWorldCell<'_>) -> Option>; + /// Get read-only access to the resources in this bundle. + /// + /// # Safety + /// The caller must it is valid to get read-only access to each of the resources in this bundle. + /// For example, if `R` is in the bundle, there should not be any valid *mutable* references to R. + unsafe fn fetch_read_only(world: UnsafeWorldCell<'_>) -> Option>; + /// Get read-only change-detection-enabled access to the resources in this bundle. + /// + /// # Safety + /// The caller must it is valid to get read-only access to each of the resources in this bundle. + /// For example, if `R` is in the bundle, there should not be any valid *mutable* references to R. + unsafe fn fetch_read_only_ref( + world: UnsafeWorldCell<'_>, + ) -> Option>; + /// Return `true` if there are access conflicts within the bundle. In other words, this returns `true` + /// if and only a resource appears twice in the bundle. + fn contains_access_conflicts() -> bool { + false + } +} + +/// This isn't public and part of the [`ResourceBundle`] trait because [`BundleAccessTable`] shouldn't be public. +trait AccessConflictTracker { + /// Merge the internal [`access table`](BundleAccessTable) with some external one. + fn merge_with(other: &mut BundleAccessTable); + /// Return `true` if there is conflicting access within the bundle. For example, two mutable references + /// to the same resource. + fn contains_conflicting_access() -> bool { + false + } +} + +/// Type to keep track which resources the [`ResourceBundle`] accesses. +struct BundleAccessTable { + table: TypeIdSet, + conflicted: bool, +} + +impl BundleAccessTable { + /// Create a new empty access table. + fn new_empty_unconflicted() -> Self { + Self { + table: TypeIdSet::default(), + conflicted: false, + } + } + + /// Insert a key-value pair to the table. If the insert causes an access conflict, the internal conflict flag will be set to `true`. + fn insert_checked(&mut self, id: TypeId) { + self.conflicted |= !self.table.insert(id); + } + + /// Returns the internal access conflict flag. + /// If this is `true`, that means that either the internal table contains an access conflict, + /// or at one point there was an attempt to merge this table with a conflicted one. + fn is_conflicted(&self) -> bool { + self.conflicted + } +} + +impl ResourceBundle for R { + type WriteAccess<'a> = Mut<'a, R>; + type ReadOnlyAccess<'a> = &'a R; + type ReadOnlySmartRefAccess<'a> = Res<'a, R>; + unsafe fn fetch_write_access(world: UnsafeWorldCell<'_>) -> Option> { + world.get_resource_mut::() + } + unsafe fn fetch_read_only(world: UnsafeWorldCell<'_>) -> Option> { + world.get_resource::() + } + unsafe fn fetch_read_only_ref( + world: UnsafeWorldCell<'_>, + ) -> Option> { + world.get_resource_ref::() + } +} + +impl AccessConflictTracker for R { + fn merge_with(other: &mut BundleAccessTable) { + other.insert_checked(TypeId::of::()); + } +} + +macro_rules! impl_conflict_tracker { + ($($tracker:ident),*) => { + impl <$($tracker: AccessConflictTracker),*> AccessConflictTracker for ($($tracker,)*) { + fn contains_conflicting_access() -> bool { + let mut tmp_table = BundleAccessTable::new_empty_unconflicted(); + Self::merge_with(&mut tmp_table); + tmp_table.is_conflicted() + } + + fn merge_with(other: &mut BundleAccessTable) { + $($tracker::merge_with(other));* + } + } + }; +} + +macro_rules! impl_resource_bundle { + ($($bundle:ident),*) => { + impl<$($bundle: ResourceBundle + AccessConflictTracker),*> ResourceBundle for ($($bundle,)*) { + type WriteAccess<'a> = ($($bundle::WriteAccess<'a>,)*); + type ReadOnlyAccess<'a> = ($($bundle::ReadOnlyAccess<'a>,)*); + type ReadOnlySmartRefAccess<'a> = ($($bundle::ReadOnlySmartRefAccess<'a>,)*); + unsafe fn fetch_write_access(world: UnsafeWorldCell<'_>) -> Option> { + Some(($($bundle::fetch_write_access(world)?,)*)) + } + unsafe fn fetch_read_only(world: UnsafeWorldCell<'_>) -> Option> { + Some(($($bundle::fetch_read_only(world)?,)*)) + } + unsafe fn fetch_read_only_ref(world: UnsafeWorldCell<'_>) -> Option> { + Some(($($bundle::fetch_read_only_ref(world)?,)*)) + } + fn contains_access_conflicts() -> bool { + ::contains_conflicting_access() + } + } + }; +} + +all_tuples!(impl_resource_bundle, 1, 15, B); +all_tuples!(impl_conflict_tracker, 1, 15, T); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index f79d6abdeb121..33946d1a9ce0c 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -26,6 +26,7 @@ use crate::{ event::{Event, EventId, Events, SendBatchIds}, query::{DebugCheckedUnwrap, QueryData, QueryEntityError, QueryFilter, QueryState}, removal_detection::RemovedComponentEvents, + resource_bundle::ResourceBundle, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, system::{Res, Resource}, @@ -1377,6 +1378,121 @@ impl World { unsafe { self.as_unsafe_world_cell().get_resource_mut() } } + /// Gets mutable access to one or more resources at once. + /// + /// Return `None` if not all of the resources exist. + /// + /// # Panics + /// Panics if multiple instances of the same resource are requested. For example: + /// ` + /// world.get_resources_mut::<(R, R, T)>(); // This will panic! There cannot be two mutable references to the same resource! + /// world.get_resources_mut::<(R, T, F)>(); // This is ok! + /// ` + /// + /// # Examples + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Resource)] + /// struct Num(isize); + /// + /// #[derive(Resource)] + /// struct BigNum(i128); + /// + /// let mut world = World::new(); + /// world.insert_resource(Num(100)); + /// world.insert_resource(BigNum(100_000)); + /// + /// let (mut num, mut big_num) = world.get_resources_mut::<(Num, BigNum)>().unwrap(); + /// num.0 *= 2; + /// big_num.0 *= 2; + /// + /// assert_eq!(world.resource::().0, 200); + /// assert_eq!(world.resource::().0, 200_000); + /// ``` + #[inline] + pub fn get_resources_mut(&mut self) -> Option> { + // SAFETY: We have a mutable access to the world + `UnsafeWorldCell::get_resources_mut` checks for access conflicts + unsafe { self.as_unsafe_world_cell().get_resources_mut::() } + } + + /// Gets read-only access to one or more resources at once. + /// + /// Return `None` if not all of the resources exist. + /// + /// # Examples + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Resource)] + /// struct Num(isize); + /// + /// #[derive(Resource)] + /// struct BigNum(i128); + /// + /// let mut world = World::new(); + /// world.insert_resource(Num(100)); + /// world.insert_resource(BigNum(100_000)); + /// + /// let (Num(num), BigNum(big_num)) = world.get_resources::<(Num, BigNum)>().unwrap(); + /// + /// assert_eq!(*num, 100); + /// assert_eq!(*big_num, 100_000); + /// ``` + #[inline] + pub fn get_resources(&self) -> Option> { + // SAFETY: We have a shared reference to this `World`, so there aren't any other valid mutable references to the `World`'s resources. + unsafe { self.as_unsafe_world_cell_readonly().get_resources::() } + } + + /// Gets read-only change-detection-enabled access to one or more resources at once. + /// + /// Return `None` if not all of the resources exist. + /// + /// # Examples + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Resource)] + /// struct Num(isize); + /// + /// #[derive(Resource)] + /// struct BigNum(i128); + /// + /// let mut world = World::new(); + /// world.insert_resource(Num(100)); + /// world.insert_resource(BigNum(100_000)); + /// + /// let (num, big_num) = world.get_resources_ref::<(Num, BigNum)>().unwrap(); + /// + /// assert_eq!(num.0, 100); + /// assert_eq!(big_num.0, 100_000); + /// assert_eq!(num.is_added(), big_num.is_added()); + /// ``` + #[inline] + pub fn get_resources_ref(&self) -> Option> { + // SAFETY: We have a shared reference to this `World`, so there aren't any other valid mutable references to the `World`'s resources. + unsafe { + self.as_unsafe_world_cell_readonly() + .get_resources_ref::() + } + } + + /// Gets access to a bundle of resources at once, without checking for access conflicts within the bundle. + /// Similar to [`World::get_resources_mut`] but this will not check for access conflicts. + /// + /// # Safety + /// The caller must ensure that there are no access conflicts within the [`ResourceBundle`]. For example: (for any resources R, T, F) + /// (R, R) - *Not Allowed!* Two mutable references can't exist at the same time! + /// (R, T, F) - *Allowed!* No access conflicts + #[inline] + pub unsafe fn get_resources_mut_unchecked( + &mut self, + ) -> Option> { + self.as_unsafe_world_cell() + .get_resources_mut_unchecked::() + } + /// Gets a mutable reference to the resource of type `T` if it exists, /// otherwise inserts the resource using the result of calling `func`. #[inline] diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index a851024a701e4..88e163f8541e0 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -13,6 +13,7 @@ use crate::{ entity::{Entities, Entity, EntityLocation}, prelude::Component, removal_detection::RemovedComponentEvents, + resource_bundle::ResourceBundle, storage::{Column, ComponentSparseSet, Storages}, system::{Res, Resource}, }; @@ -590,6 +591,78 @@ impl<'w> UnsafeWorldCell<'w> { .get(component_id)? .get_with_ticks() } + + /// Gets mutable access to one or more resources at once. + /// + /// Return `None` if one of the resources couldn't be fetched from the [`World`]. + /// + /// # Panics + /// This method will panic if there are access conflicts within provided resource bundle. + /// For example, for any resources R, T, F: + /// ` + /// world.get_resources_mut::<(R, R, T)>(); // This will panic! There cannot be two mutable references to the same resource! + /// world.get_resources_mut::<(R, T, F)>(); // This is ok! + /// ` + /// See [`World::get_resources`] for examples. + /// + /// # Safety + /// It is the caller's responsibility to make sure that + /// - This [`UnsafeWorldCell`] has permission to access all of the resources. + /// - There are no other references to any of the resources. + #[inline] + pub unsafe fn get_resources_mut(self) -> Option> { + assert!( + !B::contains_access_conflicts(), + "Make sure that each resource appears at most once in the bundle." + ); + // SAFETY: The safety of this function as described in the "# Safety" section. + unsafe { B::fetch_write_access(self) } + } + + /// Gets read-only access to one or more resources at once. + /// + /// Return `None` if one of the resources couldn't be fetched from the [`World`]. + /// See [`World::get_resources`] for examples. + /// # Safety + /// It is the caller's responsibility to make sure that + /// - This [`UnsafeWorldCell`] has permission to access all of the resources. + /// - There are no other mutable references to any of the resources. + #[inline] + pub unsafe fn get_resources(self) -> Option> { + // SAFETY: The safety of this function as described in the "# Safety" section. + unsafe { B::fetch_read_only(self) } + } + + /// Gets read-only change-detection-enabled access to one or more resources at once. + /// + /// Return `None` if one of the resources couldn't be fetched from the [`World`]. + /// See [`World::get_resources`] for examples. + /// # Safety + /// It is the caller's responsibility to make sure that + /// - This [`UnsafeWorldCell`] has permission to access all of the resources. + /// - There are no other mutable references to any of the resources. + #[inline] + pub unsafe fn get_resources_ref( + self, + ) -> Option> { + // SAFETY: The safety of this function as described in the "# Safety" section. + unsafe { B::fetch_read_only_ref(self) } + } + + /// Gets mutable access to one or more resources at once, without checking for access conflicts within the bundle. + /// Similar to [`World::get_resources_mut`] but this will not check for access conflicts. + /// + /// # Safety + /// The caller must ensure that there are no access conflicts within the [`ResourceBundle`]. For example: (for any resources R, T, F) + /// (R, R) - *Not Allowed!* Two mutable references can't exist at the same time! + /// (R, T, F) - *Allowed!* No access conflicts + #[inline] + pub unsafe fn get_resources_mut_unchecked( + self, + ) -> Option> { + // SAFETY: The safety of this function as described in the "# Safety" section. + unsafe { B::fetch_write_access(self) } + } } impl Debug for UnsafeWorldCell<'_> { diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index c7381c25208ec..6883cd4fc3329 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -348,6 +348,10 @@ pub type EntityHashSet = hashbrown::HashSet; /// Iteration order only depends on the order of insertions and deletions. pub type TypeIdMap = hashbrown::HashMap; +/// A specialized hashset type with Key of [`TypeId`] +/// Iteration order only depends on the order of insertions and deletions. +pub type TypeIdSet = hashbrown::HashSet; + /// [`BuildHasher`] for [`TypeId`]s. #[derive(Default)] pub struct NoOpTypeIdHash;