From 22e24fbdc660cbb160cb54315a0553d5b821a07e Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 23 Jun 2026 14:29:06 +0200 Subject: [PATCH 01/44] make ComponentId use Entity --- .../components-as-entities.md | 15 + crates/bevy_ecs/src/archetype.rs | 5 +- crates/bevy_ecs/src/component/constants.rs | 24 +- crates/bevy_ecs/src/component/info.rs | 107 +-- crates/bevy_ecs/src/component/register.rs | 92 +-- crates/bevy_ecs/src/entity_disabling.rs | 22 +- crates/bevy_ecs/src/lifecycle.rs | 10 +- crates/bevy_ecs/src/name.rs | 2 +- .../src/observer/centralized_storage.rs | 57 +- crates/bevy_ecs/src/query/access.rs | 652 +++++++----------- crates/bevy_ecs/src/query/access_iter.rs | 6 +- crates/bevy_ecs/src/query/state.rs | 8 +- crates/bevy_ecs/src/resource.rs | 2 +- crates/bevy_ecs/src/schedule/node.rs | 1 + crates/bevy_ecs/src/storage/sparse_set.rs | 6 +- crates/bevy_ecs/src/storage/table/mod.rs | 8 +- .../bevy_ecs/src/world/entity_access/mod.rs | 4 +- crates/bevy_ecs/src/world/mod.rs | 17 +- 18 files changed, 423 insertions(+), 615 deletions(-) create mode 100644 _release-content/migration-guides/components-as-entities.md diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md new file mode 100644 index 0000000000000..829f075d113ac --- /dev/null +++ b/_release-content/migration-guides/components-as-entities.md @@ -0,0 +1,15 @@ +--- +title: "Components as Entities" +pull_requests: [ ... ] +--- + +- `ComponentId::new` now takes `Entity` as an argument instead of `usize`. +- `ComponentId::index` was removed. +- `ComponentId::from_u32` was added. +- `ComponentId` now implements `ContainsEntity` so the entity can be gotten through `ComponentId::entity`. +- `ComponentIdSet` is now an `EntityEquivalentHashSet` instead of a `FixedBitSet`. This means that methods like `union_with` no longer work, use `bitor_assign` instead. +- `ComponentIds` has been removed. +- `ComponentsRegistrator::new` now takes `EntityAllocator` instead of `ComponentIds`. +- `ComponentsQueuedRegistrator::new` not takes `RemoteAllocator` instead of `ComponentIds`. +- `Access` no longer derives `Hash`. +- `EcsAccessType` no longer derives `Hash`. diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index e4b75653aa57b..3aec602c131ee 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -22,7 +22,7 @@ use crate::{ bundle::BundleId, component::{ComponentId, Components, RequiredComponentConstructor, StorageType}, - entity::{Entity, EntityLocation}, + entity::{Entity, EntityEquivalentHashMap, EntityLocation}, event::Event, observer::Observers, query::DebugCheckedUnwrap, @@ -762,7 +762,8 @@ struct ArchetypeComponents { /// Maps a [`ComponentId`] to the list of [`Archetypes`]([`Archetype`]) that contain the [`Component`](crate::component::Component), /// along with an [`ArchetypeRecord`] which contains some metadata about how the component is stored in the archetype. -pub type ComponentIndex = HashMap>; +pub type ComponentIndex = + EntityEquivalentHashMap>; /// The backing store of all [`Archetype`]s within a [`World`]. /// diff --git a/crates/bevy_ecs/src/component/constants.rs b/crates/bevy_ecs/src/component/constants.rs index 3f582545f85b9..69f37f6db9020 100644 --- a/crates/bevy_ecs/src/component/constants.rs +++ b/crates/bevy_ecs/src/component/constants.rs @@ -1,14 +1,14 @@ //! Constant components included in every world. -/// `usize` for the [`Add`](crate::lifecycle::Add) component used in lifecycle observers. -pub const ADD: usize = 0; -/// `usize` for the [`Insert`](crate::lifecycle::Insert) component used in lifecycle observers. -pub const INSERT: usize = 1; -/// `usize` for the [`Discard`](crate::lifecycle::Discard) component used in lifecycle observers. -pub const DISCARD: usize = 2; -/// `usize` for the [`Remove`](crate::lifecycle::Remove) component used in lifecycle observers. -pub const REMOVE: usize = 3; -/// `usize` for [`Despawn`](crate::lifecycle::Despawn) component used in lifecycle observers. -pub const DESPAWN: usize = 4; -/// `usize` of the [`IsResource`](crate::resource::IsResource) component used to mark entities with resources. -pub const IS_RESOURCE: usize = 5; +/// `u32` for the [`Add`](crate::lifecycle::Add) component used in lifecycle observers. +pub const ADD: u32 = 0; +/// `u32` for the [`Insert`](crate::lifecycle::Insert) component used in lifecycle observers. +pub const INSERT: u32 = 1; +/// `u32` for the [`Discard`](crate::lifecycle::Discard) component used in lifecycle observers. +pub const DISCARD: u32 = 2; +/// `u32` for the [`Remove`](crate::lifecycle::Remove) component used in lifecycle observers. +pub const REMOVE: u32 = 3; +/// `u32` for [`Despawn`](crate::lifecycle::Despawn) component used in lifecycle observers. +pub const DESPAWN: u32 = 4; +/// `u32` of the [`IsResource`](crate::resource::IsResource) component used to mark entities with resources. +pub const IS_RESOURCE: u32 = 5; diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 76b39eccd52a2..9d3d2952e0634 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -1,4 +1,4 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::borrow::Cow; use bevy_platform::{hash::FixedHasher, sync::PoisonError}; use bevy_ptr::OwningPtr; #[cfg(feature = "bevy_reflect")] @@ -18,6 +18,9 @@ use crate::{ Component, ComponentCloneBehavior, ComponentMutability, QueuedComponents, RequiredComponents, StorageType, }, + entity::{ + ContainsEntity, Entity, EntityEquivalent, EntityEquivalentHashMap, EntityEquivalentHashSet, + }, lifecycle::ComponentHooks, query::DebugCheckedUnwrap as _, relationship::{ @@ -177,37 +180,53 @@ impl ComponentInfo { derive(Reflect), reflect(Debug, Hash, PartialEq, Clone) )] -pub struct ComponentId(pub(super) usize); +pub struct ComponentId(pub(super) Entity); + +impl ContainsEntity for ComponentId { + fn entity(&self) -> Entity { + self.0 + } +} + +// SAFETY: EntityWrapper is a newtype around Entity that derives its comparison traits. +unsafe impl EntityEquivalent for ComponentId {} impl ComponentId { - /// Creates a new [`ComponentId`]. - /// - /// The `index` is a unique value associated with each type of component in a given world. - /// Usually, this value is taken from a counter incremented for each type of component registered with the world. + /// Creates a new [`ComponentId`] from an entity. + /// Usually this entity is created by an [`EntityAllocator`](crate::entity::EntityAllocator). + /// `Entity::PLACEHOLDER` is not valid for `ComponentId`. #[inline] - pub const fn new(index: usize) -> ComponentId { - ComponentId(index) + pub const fn new(entity: Entity) -> ComponentId { + ComponentId(entity) } - /// Returns the index of the current component. + /// Creates a [`ComponentId`] from a u32. + /// This is for debugging purposes, because in a real application you have to ensure the id doesn't conflict with any other entity. + /// Moreover, this function panics when `index` is `u32::MAX`. #[inline] - pub fn index(self) -> usize { - self.0 + pub const fn from_u32(index: u32) -> ComponentId { + ComponentId(Entity::from_raw_u32(index).unwrap()) } } impl SparseSetIndex for ComponentId { #[inline] fn sparse_set_index(&self) -> usize { - self.index() + self.entity().sparse_set_index() } #[inline] fn get_sparse_set_index(value: usize) -> Self { - Self(value) + Self(Entity::get_sparse_set_index(value)) } } +/// A map with [`ComponentId`]s as keys. +pub type ComponentIdMap = EntityEquivalentHashMap; + +/// A set of [`ComponentId`]s. +pub type ComponentIdSet = EntityEquivalentHashSet; + /// A value describing a component or resource, which may or may not correspond to a Rust type. #[derive(Clone)] pub struct ComponentDescriptor { @@ -359,7 +378,7 @@ impl ComponentDescriptor { /// Stores metadata associated with each kind of [`Component`] in a given [`World`](crate::world::World). #[derive(Debug, Default)] pub struct Components { - pub(super) components: Vec>, + pub(super) components: ComponentIdMap, pub(super) indices: TypeIdMap, // This is kept internal and local to verify that no deadlocks can occur. pub(super) queued: bevy_platform::sync::RwLock, @@ -379,15 +398,10 @@ impl Components { ) { descriptor.initialize(id, self); let info = ComponentInfo::new(id, descriptor); - let least_len = id.0 + 1; - if self.components.len() < least_len { - self.components.resize_with(least_len, || None); + // SAFETY: The id has never been registered before. + unsafe { + self.components.insert_unique_unchecked(id, info); } - // SAFETY: We just extended the vec to make this index valid. - let slot = unsafe { self.components.get_mut(id.0).debug_checked_unwrap() }; - // Caller ensures id is unique - debug_assert!(slot.is_none()); - *slot = Some(info); } /// Returns the number of components registered or queued with this instance. @@ -449,7 +463,7 @@ impl Components { /// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value. #[inline] pub fn get_info(&self, id: ComponentId) -> Option<&ComponentInfo> { - self.components.get(id.0).and_then(|info| info.as_ref()) + self.components.get(&id) } /// Gets the [`ComponentDescriptor`] of the component with this [`ComponentId`] if it is present. @@ -461,8 +475,8 @@ impl Components { #[inline] pub fn get_descriptor<'a>(&'a self, id: ComponentId) -> Option> { self.components - .get(id.0) - .and_then(|info| info.as_ref().map(|info| Cow::Borrowed(&info.descriptor))) + .get(&id) + .map(|info| Cow::Borrowed(&info.descriptor)) .or_else(|| { let queued = self.queued.read().unwrap_or_else(PoisonError::into_inner); // first check components, then resources, then dynamic @@ -482,8 +496,8 @@ impl Components { #[inline] pub fn get_name<'a>(&'a self, id: ComponentId) -> Option { self.components - .get(id.0) - .and_then(|info| info.as_ref().map(|info| info.descriptor.name())) + .get(&id) + .map(|info| info.descriptor.name()) .or_else(|| { let queued = self.queued.read().unwrap_or_else(PoisonError::into_inner); // first check components, then resources, then dynamic @@ -503,27 +517,19 @@ impl Components { #[inline] pub unsafe fn get_info_unchecked(&self, id: ComponentId) -> &ComponentInfo { // SAFETY: The caller ensures `id` is valid. - unsafe { - self.components - .get(id.0) - .debug_checked_unwrap() - .as_ref() - .debug_checked_unwrap() - } + unsafe { self.components.get(&id).debug_checked_unwrap() } } #[inline] pub(crate) fn get_hooks_mut(&mut self, id: ComponentId) -> Option<&mut ComponentHooks> { - self.components - .get_mut(id.0) - .and_then(|info| info.as_mut().map(|info| &mut info.hooks)) + self.components.get_mut(&id).map(|info| &mut info.hooks) } #[inline] pub(crate) fn get_required_components(&self, id: ComponentId) -> Option<&RequiredComponents> { self.components - .get(id.0) - .and_then(|info| info.as_ref().map(|info| &info.required_components)) + .get(&id) + .map(|info| &info.required_components) } #[inline] @@ -532,8 +538,8 @@ impl Components { id: ComponentId, ) -> Option<&mut RequiredComponents> { self.components - .get_mut(id.0) - .and_then(|info| info.as_mut().map(|info| &mut info.required_components)) + .get_mut(&id) + .map(|info| &mut info.required_components) } #[inline] @@ -541,9 +547,7 @@ impl Components { &self, id: ComponentId, ) -> Option<&IndexSet> { - self.components - .get(id.0) - .and_then(|info| info.as_ref().map(|info| &info.required_by)) + self.components.get(&id).map(|info| &info.required_by) } #[inline] @@ -552,8 +556,8 @@ impl Components { id: ComponentId, ) -> Option<&mut IndexSet> { self.components - .get_mut(id.0) - .and_then(|info| info.as_mut().map(|info| &mut info.required_by)) + .get_mut(&id) + .map(|info| &mut info.required_by) } /// Returns true if the [`ComponentId`] is fully registered and valid. @@ -561,7 +565,7 @@ impl Components { /// Those ids are still correct, but they are not usable in every context yet. #[inline] pub fn is_id_valid(&self, id: ComponentId) -> bool { - self.components.get(id.0).is_some_and(Option::is_some) + self.components.get(&id).is_some() } /// Type-erased equivalent of [`Components::valid_component_id()`]. @@ -663,7 +667,7 @@ impl Components { /// Gets an iterator over all components fully registered with this instance. pub fn iter_registered(&self) -> impl Iterator + '_ { - self.components.iter().filter_map(Option::as_ref) + self.components.values() } pub(crate) fn get_relationship_accessor_mut( @@ -671,10 +675,7 @@ impl Components { component_id: ComponentId, ) -> Option<&mut MaybeRelationshipAccessor> { self.components - .get_mut(component_id.index()) - .and_then(|info| { - info.as_mut() - .map(|info| &mut info.descriptor.relationship_accessor) - }) + .get_mut(&component_id) + .map(|info| &mut info.descriptor.relationship_accessor) } } diff --git a/crates/bevy_ecs/src/component/register.rs b/crates/bevy_ecs/src/component/register.rs index e06eddf021e0a..74fa6213aedb0 100644 --- a/crates/bevy_ecs/src/component/register.rs +++ b/crates/bevy_ecs/src/component/register.rs @@ -5,6 +5,7 @@ use core::any::Any; use core::{any::TypeId, fmt::Debug, ops::Deref}; use crate::component::{enforce_no_required_components_recursion, RequiredComponentsRegistrator}; +use crate::entity::{EntityAllocator, RemoteAllocator}; use crate::lifecycle::ComponentHooks; use crate::{ component::{ @@ -13,57 +14,10 @@ use crate::{ query::DebugCheckedUnwrap as _, }; -/// Generates [`ComponentId`]s. -#[derive(Debug, Default)] -pub struct ComponentIds { - next: bevy_platform::sync::atomic::AtomicUsize, -} - -impl ComponentIds { - /// Peeks the next [`ComponentId`] to be generated without generating it. - pub fn peek(&self) -> ComponentId { - ComponentId( - self.next - .load(bevy_platform::sync::atomic::Ordering::Relaxed), - ) - } - - /// Generates and returns the next [`ComponentId`]. - pub fn next(&self) -> ComponentId { - ComponentId( - self.next - .fetch_add(1, bevy_platform::sync::atomic::Ordering::Relaxed), - ) - } - - /// Peeks the next [`ComponentId`] to be generated without generating it. - pub fn peek_mut(&mut self) -> ComponentId { - ComponentId(*self.next.get_mut()) - } - - /// Generates and returns the next [`ComponentId`]. - pub fn next_mut(&mut self) -> ComponentId { - let id = self.next.get_mut(); - let result = ComponentId(*id); - *id += 1; - result - } - - /// Returns the number of [`ComponentId`]s generated. - pub fn len(&self) -> usize { - self.peek().0 - } - - /// Returns true if and only if no ids have been generated. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - /// A [`Components`] wrapper that enables additional features, like registration. pub struct ComponentsRegistrator<'w> { pub(super) components: &'w mut Components, - pub(super) ids: &'w mut ComponentIds, + pub(super) allocator: &'w mut EntityAllocator, pub(super) recursion_check_stack: Vec, } @@ -80,12 +34,11 @@ impl<'w> ComponentsRegistrator<'w> { /// /// # Safety /// - /// The [`Components`] and [`ComponentIds`] must match. - /// For example, they must be from the same world. - pub unsafe fn new(components: &'w mut Components, ids: &'w mut ComponentIds) -> Self { + /// The [`Components`] and [`EntityAllocator`] must come from the same world. + pub unsafe fn new(components: &'w mut Components, allocator: &'w mut EntityAllocator) -> Self { Self { components, - ids, + allocator, recursion_check_stack: Vec::new(), } } @@ -95,7 +48,12 @@ impl<'w> ComponentsRegistrator<'w> { /// It is generally not a good idea to queue a registration when you can instead register directly on this type. pub fn as_queued(&self) -> ComponentsQueuedRegistrator<'_> { // SAFETY: ensured by the caller that created self. - unsafe { ComponentsQueuedRegistrator::new(self.components, self.ids) } + unsafe { + ComponentsQueuedRegistrator::new( + self.components, + self.allocator.build_remote_allocator(), + ) + } } /// Applies every queued registration. @@ -200,7 +158,7 @@ impl<'w> ComponentsRegistrator<'w> { return registrator.register(self); } - let id = self.ids.next_mut(); + let id = ComponentId::new(self.allocator.alloc()); // SAFETY: The component is not currently registered, and the id is fresh. unsafe { self.register_component_unchecked( @@ -254,9 +212,7 @@ impl<'w> ComponentsRegistrator<'w> { &mut self .components .components - .get_mut(id.0) - .debug_checked_unwrap() - .as_mut() + .get_mut(&id) .debug_checked_unwrap() }; @@ -287,7 +243,7 @@ impl<'w> ComponentsRegistrator<'w> { &mut self, descriptor: ComponentDescriptor, ) -> ComponentId { - let id = self.ids.next_mut(); + let id = ComponentId::new(self.allocator.alloc()); // SAFETY: The id is fresh. unsafe { self.components.register_component_inner(id, descriptor); @@ -336,7 +292,7 @@ impl<'w> ComponentsRegistrator<'w> { return registrator.register(self); } - let id = self.ids.next_mut(); + let id = ComponentId::new(self.allocator.alloc()); // SAFETY: The resource is not currently registered, the id is fresh, and the [`ComponentDescriptor`] matches the [`TypeId`] unsafe { self.components @@ -430,10 +386,9 @@ impl Debug for QueuedComponents { /// /// As a rule of thumb, if you have mutable access to [`ComponentsRegistrator`], prefer to use that instead. /// Use this only if you need to know the id of a component but do not need to modify the contents of the world based on that id. -#[derive(Clone, Copy)] pub struct ComponentsQueuedRegistrator<'w> { components: &'w Components, - ids: &'w ComponentIds, + allocator: RemoteAllocator, } impl Deref for ComponentsQueuedRegistrator<'_> { @@ -449,10 +404,12 @@ impl<'w> ComponentsQueuedRegistrator<'w> { /// /// # Safety /// - /// The [`Components`] and [`ComponentIds`] must match. - /// For example, they must be from the same world. - pub unsafe fn new(components: &'w Components, ids: &'w ComponentIds) -> Self { - Self { components, ids } + /// The [`Components`] and [`RemoteAllocator`] must come from the same world. + pub unsafe fn new(components: &'w Components, allocator: RemoteAllocator) -> Self { + Self { + components, + allocator, + } } /// Queues this function to run as a component registrator if the given @@ -474,8 +431,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> { .components .entry(type_id) .or_insert_with(|| { + let id = ComponentId::new(self.allocator.alloc()); // SAFETY: The id was just generated. - unsafe { QueuedRegistration::new(self.ids.next(), descriptor, func) } + unsafe { QueuedRegistration::new(id, descriptor, func) } }) .id } @@ -486,7 +444,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> { descriptor: ComponentDescriptor, func: fn(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor), ) -> ComponentId { - let id = self.ids.next(); + let id = ComponentId::new(self.allocator.alloc()); self.components .queued .write() diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 1e751070ac2df..46ff6237ee2ab 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -253,42 +253,44 @@ mod tests { #[test] fn filters_modify_access() { let mut filters = DefaultQueryFilters::empty(); - filters.register_disabling_component(ComponentId::new(1)); + filters.register_disabling_component(ComponentId::from_u32(1)); // A component access with an unrelated component let mut component_access = FilteredAccess::default(); - component_access.access_mut().add_read(ComponentId::new(2)); + component_access + .access_mut() + .add_read(ComponentId::from_u32(2)); let mut applied_access = component_access.clone(); filters.modify_access(&mut applied_access); assert_eq!(0, applied_access.with_filters().count()); assert_eq!( - vec![ComponentId::new(1)], + vec![ComponentId::from_u32(1)], applied_access.without_filters().collect::>() ); // We add a with filter, now we expect to see both filters - component_access.and_with(ComponentId::new(4)); + component_access.and_with(ComponentId::from_u32(4)); let mut applied_access = component_access.clone(); filters.modify_access(&mut applied_access); assert_eq!( - vec![ComponentId::new(4)], + vec![ComponentId::from_u32(4)], applied_access.with_filters().collect::>() ); assert_eq!( - vec![ComponentId::new(1)], + vec![ComponentId::from_u32(1)], applied_access.without_filters().collect::>() ); let copy = component_access.clone(); // We add a rule targeting a default component, that filter should no longer be added - component_access.and_with(ComponentId::new(1)); + component_access.and_with(ComponentId::from_u32(1)); let mut applied_access = component_access.clone(); filters.modify_access(&mut applied_access); assert_eq!( - vec![ComponentId::new(1), ComponentId::new(4)], + vec![ComponentId::from_u32(1), ComponentId::from_u32(4)], applied_access.with_filters().collect::>() ); assert_eq!(0, applied_access.without_filters().count()); @@ -297,12 +299,12 @@ mod tests { component_access = copy.clone(); component_access .access_mut() - .add_archetypal(ComponentId::new(1)); + .add_archetypal(ComponentId::from_u32(1)); let mut applied_access = component_access.clone(); filters.modify_access(&mut applied_access); assert_eq!( - vec![ComponentId::new(4)], + vec![ComponentId::from_u32(4)], applied_access.with_filters().collect::>() ); assert_eq!(0, applied_access.without_filters().count()); diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs index 82e73d326c215..cb3638bb4c8d1 100644 --- a/crates/bevy_ecs/src/lifecycle.rs +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -315,15 +315,15 @@ impl ComponentHooks { } /// [`EventKey`] for [`Add`] -pub const ADD: EventKey = EventKey(ComponentId::new(crate::component::ADD)); +pub const ADD: EventKey = EventKey(ComponentId::from_u32(crate::component::ADD)); /// [`EventKey`] for [`Insert`] -pub const INSERT: EventKey = EventKey(ComponentId::new(crate::component::INSERT)); +pub const INSERT: EventKey = EventKey(ComponentId::from_u32(crate::component::INSERT)); /// [`EventKey`] for [`Discard`] -pub const DISCARD: EventKey = EventKey(ComponentId::new(crate::component::DISCARD)); +pub const DISCARD: EventKey = EventKey(ComponentId::from_u32(crate::component::DISCARD)); /// [`EventKey`] for [`Remove`] -pub const REMOVE: EventKey = EventKey(ComponentId::new(crate::component::REMOVE)); +pub const REMOVE: EventKey = EventKey(ComponentId::from_u32(crate::component::REMOVE)); /// [`EventKey`] for [`Despawn`] -pub const DESPAWN: EventKey = EventKey(ComponentId::new(crate::component::DESPAWN)); +pub const DESPAWN: EventKey = EventKey(ComponentId::from_u32(crate::component::DESPAWN)); /// Trigger emitted when a component is inserted onto an entity that does not already have that /// component. Runs before `Insert`. diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index bb2b98d85581a..14898c1ab76ee 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -299,7 +299,7 @@ mod tests { let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} - assert_eq!(d1.to_string(), "1v0"); + assert_eq!(d1.to_string(), "10v0"); let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); diff --git a/crates/bevy_ecs/src/observer/centralized_storage.rs b/crates/bevy_ecs/src/observer/centralized_storage.rs index 8b09e95a0ec90..23c4e79eb7afe 100644 --- a/crates/bevy_ecs/src/observer/centralized_storage.rs +++ b/crates/bevy_ecs/src/observer/centralized_storage.rs @@ -39,13 +39,18 @@ impl Observers { pub(crate) fn get_observers_mut(&mut self, event_key: EventKey) -> &mut CachedObservers { use crate::lifecycle::*; - match event_key { - ADD => &mut self.add, - INSERT => &mut self.insert, - DISCARD => &mut self.discard, - REMOVE => &mut self.remove, - DESPAWN => &mut self.despawn, - _ => self.cache.entry(event_key).or_default(), + if event_key == ADD { + &mut self.add + } else if event_key == INSERT { + &mut self.insert + } else if event_key == DISCARD { + &mut self.discard + } else if event_key == REMOVE { + &mut self.remove + } else if event_key == DESPAWN { + &mut self.despawn + } else { + self.cache.entry(event_key).or_default() } } @@ -62,26 +67,36 @@ impl Observers { pub fn try_get_observers(&self, event_key: EventKey) -> Option<&CachedObservers> { use crate::lifecycle::*; - match event_key { - ADD => Some(&self.add), - INSERT => Some(&self.insert), - DISCARD => Some(&self.discard), - REMOVE => Some(&self.remove), - DESPAWN => Some(&self.despawn), - _ => self.cache.get(&event_key), + if event_key == ADD { + Some(&self.add) + } else if event_key == INSERT { + Some(&self.insert) + } else if event_key == DISCARD { + Some(&self.discard) + } else if event_key == REMOVE { + Some(&self.remove) + } else if event_key == DESPAWN { + Some(&self.despawn) + } else { + self.cache.get(&event_key) } } pub(crate) fn is_archetype_cached(event_key: EventKey) -> Option { use crate::lifecycle::*; - match event_key { - ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), - INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), - DISCARD => Some(ArchetypeFlags::ON_DISCARD_OBSERVER), - REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), - DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), - _ => None, + if event_key == ADD { + Some(ArchetypeFlags::ON_ADD_OBSERVER) + } else if event_key == INSERT { + Some(ArchetypeFlags::ON_INSERT_OBSERVER) + } else if event_key == DISCARD { + Some(ArchetypeFlags::ON_DISCARD_OBSERVER) + } else if event_key == REMOVE { + Some(ArchetypeFlags::ON_REMOVE_OBSERVER) + } else if event_key == DESPAWN { + Some(ArchetypeFlags::ON_DESPAWN_OBSERVER) + } else { + None } } diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 3e00230bd5e56..787d2e7483316 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -1,17 +1,19 @@ use crate::world::unsafe_world_cell::UnsafeWorldCell; -use crate::{component::ComponentId, resource::IS_RESOURCE}; +use crate::{ + component::{ComponentId, ComponentIdSet}, + resource::IS_RESOURCE, +}; use alloc::{format, string::String, vec, vec::Vec}; -use core::iter::FusedIterator; -use core::{fmt, fmt::Debug}; +use core::fmt::Debug; +use core::ops::{BitAnd, BitAndAssign, BitOrAssign, Sub, SubAssign}; use derive_more::From; -use fixedbitset::{Difference, FixedBitSet, Intersection, IntoOnes, Ones, Union}; use thiserror::Error; /// Tracks read and write access to specific elements in a collection. /// /// Used internally to ensure soundness during system initialization and execution. /// See the [`is_compatible`](Access::is_compatible) and [`get_conflicts`](Access::get_conflicts) functions. -#[derive(Eq, PartialEq, Default, Hash, Debug)] +#[derive(Eq, PartialEq, Default, Debug)] pub struct Access { /// All accessed components, or forbidden components if /// `Self::component_read_and_writes_inverted` is set. @@ -90,7 +92,7 @@ impl Access { if !self.read_and_writes_inverted { self.read_and_writes.insert(index); } else { - self.read_and_writes.remove(index); + self.read_and_writes.remove(&index); } } @@ -100,7 +102,7 @@ impl Access { if !self.writes_inverted { self.writes.insert(index); } else { - self.writes.remove(index); + self.writes.remove(&index); } } @@ -117,7 +119,7 @@ impl Access { if self.read_and_writes_inverted { self.read_and_writes.insert(index); } else { - self.read_and_writes.remove(index); + self.read_and_writes.remove(&index); } } @@ -133,7 +135,7 @@ impl Access { if self.writes_inverted { self.writes.insert(index); } else { - self.writes.remove(index); + self.writes.remove(&index); } } @@ -152,22 +154,22 @@ impl Access { /// Returns `true` if this can access the component given by `index`. pub fn has_read(&self, index: ComponentId) -> bool { - self.read_and_writes_inverted ^ self.read_and_writes.contains(index) + self.read_and_writes_inverted ^ self.read_and_writes.contains(&index) } /// Returns `true` if this can access any component. pub fn has_any_read(&self) -> bool { - self.read_and_writes_inverted || !self.read_and_writes.is_clear() + self.read_and_writes_inverted || !self.read_and_writes.is_empty() } /// Returns `true` if this can exclusively access the component given by `index`. pub fn has_write(&self, index: ComponentId) -> bool { - self.writes_inverted ^ self.writes.contains(index) + self.writes_inverted ^ self.writes.contains(&index) } /// Returns `true` if this accesses any component mutably. pub fn has_any_write(&self) -> bool { - self.writes_inverted || !self.writes.is_clear() + self.writes_inverted || !self.writes.is_empty() } /// Returns true if this has an archetypal (indirect) access to the component given by `index`. @@ -179,7 +181,7 @@ impl Access { /// /// [`Has`]: crate::query::Has pub fn has_archetypal(&self, index: ComponentId) -> bool { - self.archetypal.contains(index) + self.archetypal.contains(&index) } /// Sets this as having access to all components (i.e. `EntityRef` and `&World`). @@ -200,13 +202,13 @@ impl Access { /// Returns `true` if this has access to all components (i.e. `EntityRef` and `&World`). #[inline] pub fn has_read_all(&self) -> bool { - self.read_and_writes_inverted && self.read_and_writes.is_clear() + self.read_and_writes_inverted && self.read_and_writes.is_empty() } /// Returns `true` if this has write access to all components (i.e. `EntityMut` and `&mut World`). #[inline] pub fn has_write_all(&self) -> bool { - self.writes_inverted && self.writes.is_clear() + self.writes_inverted && self.writes.is_empty() } /// Removes all writes. @@ -237,7 +239,7 @@ impl Access { &other.writes, other.writes_inverted, ); - self.archetypal.union_with(&other.archetypal); + self.archetypal.bitor_assign(&other.archetypal); } /// Removes any access from `self` that would conflict with `other`. @@ -385,11 +387,11 @@ impl Access { let temp_conflicts: ComponentIdSet = match (lhs_writes_inverted, rhs_reads_and_writes_inverted) { (true, true) => return AccessConflicts::All, - (false, true) => lhs_writes.difference(rhs_reads_and_writes).collect(), - (true, false) => rhs_reads_and_writes.difference(lhs_writes).collect(), - (false, false) => lhs_writes.intersection(rhs_reads_and_writes).collect(), + (false, true) => lhs_writes.sub(rhs_reads_and_writes), + (true, false) => rhs_reads_and_writes.sub(lhs_writes), + (false, false) => lhs_writes.bitand(rhs_reads_and_writes), }; - conflicts.union_with(&temp_conflicts); + conflicts.bitor_assign(&temp_conflicts); } AccessConflicts::Individual(conflicts) @@ -446,9 +448,9 @@ impl Access { /// # use bevy_ecs::component::ComponentId; /// let mut access = Access::default(); /// - /// access.add_read(ComponentId::new(1)); - /// access.add_write(ComponentId::new(2)); - /// access.add_archetypal(ComponentId::new(3)); + /// access.add_read(ComponentId::from_u32(1)); + /// access.add_write(ComponentId::from_u32(2)); + /// access.add_archetypal(ComponentId::from_u32(3)); /// /// let result = access /// .try_iter_access() @@ -457,9 +459,9 @@ impl Access { /// assert_eq!( /// result, /// Ok(vec![ - /// ComponentAccessKind::Shared(ComponentId::new(1)), - /// ComponentAccessKind::Exclusive(ComponentId::new(2)), - /// ComponentAccessKind::Archetypal(ComponentId::new(3)), + /// ComponentAccessKind::Shared(ComponentId::from_u32(1)), + /// ComponentAccessKind::Exclusive(ComponentId::from_u32(2)), + /// ComponentAccessKind::Archetypal(ComponentId::from_u32(3)), /// ]), /// ); /// ``` @@ -468,16 +470,16 @@ impl Access { ) -> Result + '_, UnboundedAccessError> { let reads_and_writes = self.try_reads_and_writes()?.iter().map(|index| { if self.writes.contains(index) { - ComponentAccessKind::Exclusive(index) + ComponentAccessKind::Exclusive(*index) } else { - ComponentAccessKind::Shared(index) + ComponentAccessKind::Shared(*index) } }); let archetypal = self .archetypal .difference(&self.read_and_writes) - .map(ComponentAccessKind::Archetypal); + .map(|id| ComponentAccessKind::Archetypal(*id)); Ok(reads_and_writes.chain(archetypal)) } @@ -498,13 +500,13 @@ fn invertible_union_with( other_inverted: bool, ) { match (*self_inverted, other_inverted) { - (true, true) => self_set.intersect_with(other_set), - (true, false) => self_set.difference_with(other_set), + (true, true) => self_set.bitand_assign(other_set), + (true, false) => self_set.sub_assign(other_set), (false, true) => { *self_inverted = true; - self_set.difference_from(other_set); + *self_set = other_set.clone().sub(self_set); } - (false, false) => self_set.union_with(other_set), + (false, false) => self_set.bitor_assign(other_set), } } @@ -647,7 +649,7 @@ impl AccessConflicts { pub fn is_empty(&self) -> bool { match self { Self::All => false, - Self::Individual(set) => set.is_clear(), + Self::Individual(set) => set.is_empty(), } } @@ -659,7 +661,7 @@ impl AccessConflicts { .map(|index| { format!( "{}", - world.components().get_name(index).unwrap().shortname() + world.components().get_name(*index).unwrap().shortname() ) }) .collect::>() @@ -802,14 +804,14 @@ impl FilteredAccess { /// `Or<((With, With), (With, Without), (Without, With), (Without, Without))>`. pub fn extend(&mut self, other: &FilteredAccess) { self.access.extend(&other.access); - self.required.union_with(&other.required); + self.required.bitor_assign(&other.required); // We can avoid allocating a new array of bitsets if `other` contains just a single set of filters: // in this case we can short-circuit by performing an in-place union for each bitset. if other.filter_sets.len() == 1 { for filter in &mut self.filter_sets { - filter.with.union_with(&other.filter_sets[0].with); - filter.without.union_with(&other.filter_sets[0].without); + filter.with.bitor_assign(&other.filter_sets[0].with); + filter.without.bitor_assign(&other.filter_sets[0].without); } return; } @@ -818,8 +820,8 @@ impl FilteredAccess { for filter in &self.filter_sets { for other_filter in &other.filter_sets { let mut new_filter = filter.clone(); - new_filter.with.union_with(&other_filter.with); - new_filter.without.union_with(&other_filter.without); + new_filter.with.bitor_assign(&other_filter.with); + new_filter.without.bitor_assign(&other_filter.without); new_filters.push(new_filter); } } @@ -865,12 +867,14 @@ impl FilteredAccess { /// Returns the indices of the elements that this access filters for. pub fn with_filters(&self) -> impl Iterator + '_ { - self.filter_sets.iter().flat_map(|f| f.with.iter()) + self.filter_sets.iter().flat_map(|f| f.with.iter().copied()) } /// Returns the indices of the elements that this access filters out. pub fn without_filters(&self) -> impl Iterator + '_ { - self.filter_sets.iter().flat_map(|f| f.without.iter()) + self.filter_sets + .iter() + .flat_map(|f| f.without.iter().copied()) } /// Returns true if the index is used by this `FilteredAccess` in filters or archetypal access. @@ -881,7 +885,7 @@ impl FilteredAccess { || self .filter_sets .iter() - .any(|f| f.with.contains(index) || f.without.contains(index)) + .any(|f| f.with.contains(&index) || f.without.contains(&index)) } } @@ -1110,238 +1114,27 @@ impl FilteredAccessSet { } } -/// A set of [`ComponentId`]s. -#[derive(Default, Eq, PartialEq, Hash)] -#[repr(transparent)] -pub struct ComponentIdSet(FixedBitSet); - -impl ComponentIdSet { - /// Create a new empty `ComponentIdSet`. - #[inline] - pub const fn new() -> Self { - Self(FixedBitSet::new()) - } - - #[cfg(test)] - pub(crate) fn from_bits(bits: FixedBitSet) -> Self { - Self(bits) - } - - /// Adds a [`ComponentId`] to the set. - #[inline] - pub fn insert(&mut self, index: ComponentId) { - self.0.grow_and_insert(index.index()); - } - - /// Removes a [`ComponentId`] from the set. - #[inline] - pub fn remove(&mut self, index: ComponentId) { - if index.index() < self.0.len() { - self.0.remove(index.index()); - } - } - - /// Removes all [`ComponentId`]s from the set. - #[inline] - pub fn clear(&mut self) { - self.0.clear(); - } - - /// Returns `true` if the [`ComponentId`] is in the set. - #[inline] - pub fn contains(&self, index: ComponentId) -> bool { - self.0.contains(index.index()) - } - - /// Returns `true` if `self` has no elements in common with `other`. This - /// is equivalent to checking for an empty intersection. - #[inline] - pub fn is_disjoint(&self, other: &ComponentIdSet) -> bool { - self.0.is_disjoint(&other.0) - } - - /// Returns `true` if the set is a subset of another, i.e. `other` contains - /// at least all the values in `self`. - #[inline] - pub fn is_subset(&self, other: &ComponentIdSet) -> bool { - self.0.is_subset(&other.0) - } - - /// Returns `true` if the set is empty. - #[inline] - pub fn is_clear(&self) -> bool { - self.0.is_clear() - } - - /// Iterates the [`ComponentId`]s in the set. - #[inline] - pub fn iter(&self) -> ComponentIdIter> { - ComponentIdIter(self.0.ones()) - } - - /// Returns a lazy iterator over the union of two [`ComponentIdSet`]s. - #[inline] - pub fn union<'a>(&'a self, other: &'a ComponentIdSet) -> ComponentIdIter> { - ComponentIdIter(self.0.union(&other.0)) - } - - /// Returns a lazy iterator over the intersection of two [`ComponentIdSet`]s. - #[inline] - pub fn intersection<'a>( - &'a self, - other: &'a ComponentIdSet, - ) -> ComponentIdIter> { - ComponentIdIter(self.0.intersection(&other.0)) - } - - /// Returns a lazy iterator over the difference of two [`ComponentIdSet`]s. - #[inline] - pub fn difference<'a>(&'a self, other: &'a ComponentIdSet) -> ComponentIdIter> { - ComponentIdIter(self.0.difference(&other.0)) - } - - /// In-place union of two [`ComponentIdSet`]s. - #[inline] - pub fn union_with(&mut self, other: &ComponentIdSet) { - self.0.union_with(&other.0); - } - - /// In-place intersection of two [`ComponentIdSet`]s. - #[inline] - pub fn intersect_with(&mut self, other: &ComponentIdSet) { - self.0.intersect_with(&other.0); - } - - /// In-place difference of two [`ComponentIdSet`]s. - #[inline] - pub fn difference_with(&mut self, other: &ComponentIdSet) { - self.0.difference_with(&other.0); - } - - /// In-place reversed difference of two [`ComponentIdSet`]s. - /// This sets `self` to be `other.difference(self)`. - #[inline] - pub fn difference_from(&mut self, other: &ComponentIdSet) { - // Calculate `other - self` as `!self & other` - // We have to grow here because the new bits are going to get flipped to 1. - self.0.grow(other.0.len()); - self.0.toggle_range(..); - self.0.intersect_with(&other.0); - } -} - -impl Debug for ComponentIdSet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // `FixedBitSet` normally has a `Debug` output like: - // FixedBitSet { data: [ 160 ], length: 8 } - // Instead, print the list of set values, like: - // [ 5, 7 ] - // Don't wrap in `ComponentId`, since that would just output: - // [ ComponentId(5), ComponentId(7) ] - f.debug_list().entries(self.0.ones()).finish() - } -} - -impl Clone for ComponentIdSet { - #[inline] - fn clone(&self) -> Self { - Self(self.0.clone()) - } - - #[inline] - fn clone_from(&mut self, source: &Self) { - self.0.clone_from(&source.0); - } -} - -impl IntoIterator for ComponentIdSet { - type Item = ComponentId; - - type IntoIter = ComponentIdIter; - - #[inline] - fn into_iter(self) -> Self::IntoIter { - ComponentIdIter(self.0.into_ones()) - } -} - -impl<'a> IntoIterator for &'a ComponentIdSet { - type Item = ComponentId; - - type IntoIter = ComponentIdIter>; - - #[inline] - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl FromIterator for ComponentIdSet { - #[inline] - fn from_iter>(iter: T) -> Self { - Self(FixedBitSet::from_iter( - iter.into_iter().map(ComponentId::index), - )) - } -} - -impl Extend for ComponentIdSet { - #[inline] - fn extend>(&mut self, iter: T) { - self.0.extend(iter.into_iter().map(ComponentId::index)); - } -} - -/// An iterator of [`ComponentId`]s. -/// -/// This is equivalent to `map(ComponentId::new)`, -/// but is a named type to allow it to be used in associated types. -#[repr(transparent)] -pub struct ComponentIdIter(I); - -impl> Iterator for ComponentIdIter { - type Item = ComponentId; - - #[inline] - fn next(&mut self) -> Option { - self.0.next().map(ComponentId::new) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -impl> DoubleEndedIterator for ComponentIdIter { - #[inline] - fn next_back(&mut self) -> Option { - self.0.next_back().map(ComponentId::new) - } -} - -impl> FusedIterator for ComponentIdIter {} - #[cfg(test)] mod tests { + use core::ops::{BitAndAssign, BitOrAssign, SubAssign}; + use super::{invertible_difference_with, invertible_union_with}; use crate::{ - component::ComponentId, + component::{ComponentId, ComponentIdSet}, query::{ - access::AccessFilters, Access, AccessConflicts, ComponentAccessKind, ComponentIdSet, - FilteredAccess, FilteredAccessSet, UnboundedAccessError, + access::AccessFilters, Access, AccessConflicts, ComponentAccessKind, FilteredAccess, + FilteredAccessSet, UnboundedAccessError, }, }; use alloc::{vec, vec::Vec}; - use fixedbitset::FixedBitSet; fn create_sample_access() -> Access { let mut access = Access::default(); - access.add_read(ComponentId::new(1)); - access.add_read(ComponentId::new(2)); - access.add_write(ComponentId::new(3)); - access.add_archetypal(ComponentId::new(5)); + access.add_read(ComponentId::from_u32(1)); + access.add_read(ComponentId::from_u32(2)); + access.add_write(ComponentId::from_u32(3)); + access.add_archetypal(ComponentId::from_u32(5)); access.read_all(); access @@ -1350,10 +1143,10 @@ mod tests { fn create_sample_filtered_access() -> FilteredAccess { let mut filtered_access = FilteredAccess::default(); - filtered_access.add_write(ComponentId::new(1)); - filtered_access.add_read(ComponentId::new(2)); - filtered_access.add_required(ComponentId::new(3)); - filtered_access.and_with(ComponentId::new(4)); + filtered_access.add_write(ComponentId::from_u32(1)); + filtered_access.add_read(ComponentId::from_u32(2)); + filtered_access.add_required(ComponentId::from_u32(3)); + filtered_access.and_with(ComponentId::from_u32(4)); filtered_access } @@ -1361,8 +1154,8 @@ mod tests { fn create_sample_access_filters() -> AccessFilters { let mut access_filters = AccessFilters::default(); - access_filters.with.insert(ComponentId::new(3)); - access_filters.without.insert(ComponentId::new(5)); + access_filters.with.insert(ComponentId::from_u32(3)); + access_filters.without.insert(ComponentId::from_u32(5)); access_filters } @@ -1370,8 +1163,8 @@ mod tests { fn create_sample_filtered_access_set() -> FilteredAccessSet { let mut filtered_access_set = FilteredAccessSet::default(); - filtered_access_set.add_unfiltered_component_read(ComponentId::new(2)); - filtered_access_set.add_unfiltered_component_write(ComponentId::new(4)); + filtered_access_set.add_unfiltered_component_read(ComponentId::from_u32(2)); + filtered_access_set.add_unfiltered_component_write(ComponentId::from_u32(4)); filtered_access_set.read_all(); filtered_access_set @@ -1390,9 +1183,9 @@ mod tests { let original = create_sample_access(); let mut cloned = Access::default(); - cloned.add_write(ComponentId::new(7)); - cloned.add_read(ComponentId::new(4)); - cloned.add_archetypal(ComponentId::new(8)); + cloned.add_write(ComponentId::from_u32(7)); + cloned.add_read(ComponentId::from_u32(4)); + cloned.add_archetypal(ComponentId::from_u32(8)); cloned.write_all(); cloned.clone_from(&original); @@ -1413,8 +1206,8 @@ mod tests { let original = create_sample_filtered_access(); let mut cloned = FilteredAccess::default(); - cloned.add_write(ComponentId::new(7)); - cloned.add_read(ComponentId::new(4)); + cloned.add_write(ComponentId::from_u32(7)); + cloned.add_read(ComponentId::from_u32(4)); cloned.append_or(&FilteredAccess::default()); cloned.clone_from(&original); @@ -1435,8 +1228,8 @@ mod tests { let original = create_sample_access_filters(); let mut cloned = AccessFilters::default(); - cloned.with.insert(ComponentId::new(1)); - cloned.without.insert(ComponentId::new(2)); + cloned.with.insert(ComponentId::from_u32(1)); + cloned.without.insert(ComponentId::from_u32(2)); cloned.clone_from(&original); @@ -1456,8 +1249,8 @@ mod tests { let original = create_sample_filtered_access_set(); let mut cloned = FilteredAccessSet::default(); - cloned.add_unfiltered_component_read(ComponentId::new(7)); - cloned.add_unfiltered_component_write(ComponentId::new(9)); + cloned.add_unfiltered_component_read(ComponentId::from_u32(7)); + cloned.add_unfiltered_component_write(ComponentId::from_u32(9)); cloned.write_all(); cloned.clone_from(&original); @@ -1469,7 +1262,7 @@ mod tests { fn read_all_access_conflicts() { // read_all / single write let mut access_a = Access::default(); - access_a.add_write(ComponentId::new(0)); + access_a.add_write(ComponentId::from_u32(0)); let mut access_b = Access::default(); access_b.read_all(); @@ -1489,54 +1282,54 @@ mod tests { #[test] fn access_get_conflicts() { let mut access_a = Access::default(); - access_a.add_read(ComponentId::new(0)); - access_a.add_read(ComponentId::new(1)); + access_a.add_read(ComponentId::from_u32(0)); + access_a.add_read(ComponentId::from_u32(1)); let mut access_b = Access::default(); - access_b.add_read(ComponentId::new(0)); - access_b.add_write(ComponentId::new(1)); + access_b.add_read(ComponentId::from_u32(0)); + access_b.add_write(ComponentId::from_u32(1)); assert_eq!( access_a.get_conflicts(&access_b), - vec![ComponentId::new(1)].into() + vec![ComponentId::from_u32(1)].into() ); let mut access_c = Access::default(); - access_c.add_write(ComponentId::new(0)); - access_c.add_write(ComponentId::new(1)); + access_c.add_write(ComponentId::from_u32(0)); + access_c.add_write(ComponentId::from_u32(1)); assert_eq!( access_a.get_conflicts(&access_c), - vec![ComponentId::new(0), ComponentId::new(1)].into() + vec![ComponentId::from_u32(0), ComponentId::from_u32(1)].into() ); assert_eq!( access_b.get_conflicts(&access_c), - vec![ComponentId::new(0), ComponentId::new(1)].into() + vec![ComponentId::from_u32(0), ComponentId::from_u32(1)].into() ); let mut access_d = Access::default(); - access_d.add_read(ComponentId::new(0)); + access_d.add_read(ComponentId::from_u32(0)); assert_eq!(access_d.get_conflicts(&access_a), AccessConflicts::empty()); assert_eq!(access_d.get_conflicts(&access_b), AccessConflicts::empty()); assert_eq!( access_d.get_conflicts(&access_c), - vec![ComponentId::new(0)].into() + vec![ComponentId::from_u32(0)].into() ); } #[test] fn filtered_combined_access() { let mut access_a = FilteredAccessSet::default(); - access_a.add_unfiltered_component_read(ComponentId::new(1)); + access_a.add_unfiltered_component_read(ComponentId::from_u32(1)); let mut filter_b = FilteredAccess::default(); - filter_b.add_write(ComponentId::new(1)); + filter_b.add_write(ComponentId::from_u32(1)); let conflicts = access_a.get_conflicts_single(&filter_b); assert_eq!( &conflicts, - &AccessConflicts::from(vec![ComponentId::new(1)]), + &AccessConflicts::from(vec![ComponentId::from_u32(1)]), "access_a: {access_a:?}, filter_b: {filter_b:?}" ); } @@ -1544,42 +1337,46 @@ mod tests { #[test] fn filtered_access_extend() { let mut access_a = FilteredAccess::default(); - access_a.add_read(ComponentId::new(0)); - access_a.add_read(ComponentId::new(1)); - access_a.and_with(ComponentId::new(2)); + access_a.add_read(ComponentId::from_u32(0)); + access_a.add_read(ComponentId::from_u32(1)); + access_a.and_with(ComponentId::from_u32(2)); let mut access_b = FilteredAccess::default(); - access_b.add_read(ComponentId::new(0)); - access_b.add_write(ComponentId::new(3)); - access_b.and_without(ComponentId::new(4)); + access_b.add_read(ComponentId::from_u32(0)); + access_b.add_write(ComponentId::from_u32(3)); + access_b.and_without(ComponentId::from_u32(4)); access_a.extend(&access_b); let mut expected = FilteredAccess::default(); - expected.add_read(ComponentId::new(0)); - expected.add_read(ComponentId::new(1)); - expected.and_with(ComponentId::new(2)); - expected.add_write(ComponentId::new(3)); - expected.and_without(ComponentId::new(4)); + expected.add_read(ComponentId::from_u32(0)); + expected.add_read(ComponentId::from_u32(1)); + expected.and_with(ComponentId::from_u32(2)); + expected.add_write(ComponentId::from_u32(3)); + expected.and_without(ComponentId::from_u32(4)); assert!(access_a.eq(&expected)); } + fn set_from_u32s(iter: Vec) -> ComponentIdSet { + ComponentIdSet::from_iter(iter.into_iter().map(ComponentId::from_u32)) + } + #[test] fn filtered_access_extend_or() { let mut access_a = FilteredAccess::default(); // Exclusive access to `(&mut A, &mut B)`. - access_a.add_write(ComponentId::new(0)); - access_a.add_write(ComponentId::new(1)); + access_a.add_write(ComponentId::from_u32(0)); + access_a.add_write(ComponentId::from_u32(1)); // Filter by `With`. let mut access_b = FilteredAccess::default(); - access_b.and_with(ComponentId::new(2)); + access_b.and_with(ComponentId::from_u32(2)); // Filter by `(With, Without)`. let mut access_c = FilteredAccess::default(); - access_c.and_with(ComponentId::new(3)); - access_c.and_without(ComponentId::new(4)); + access_c.and_with(ComponentId::from_u32(3)); + access_c.and_without(ComponentId::from_u32(4)); // Turns `access_b` into `Or<(With, (With, Without))>`. access_b.append_or(&access_c); @@ -1591,20 +1388,17 @@ mod tests { // The intention here is to test that exclusive access implied by `add_write` // forms correct normalized access structs when extended with `Or` filters. let mut expected = FilteredAccess::default(); - expected.add_write(ComponentId::new(0)); - expected.add_write(ComponentId::new(1)); + expected.add_write(ComponentId::from_u32(0)); + expected.add_write(ComponentId::from_u32(1)); // The resulted access is expected to represent `Or<((With, With, With), (With, With, With, Without))>`. expected.filter_sets = vec![ AccessFilters { - with: ComponentIdSet::from_bits(FixedBitSet::with_capacity_and_blocks(3, [0b111])), + with: set_from_u32s(vec![0, 1, 2]), without: ComponentIdSet::default(), }, AccessFilters { - with: ComponentIdSet::from_bits(FixedBitSet::with_capacity_and_blocks(4, [0b1011])), - without: ComponentIdSet::from_bits(FixedBitSet::with_capacity_and_blocks( - 5, - [0b10000], - )), + with: set_from_u32s(vec![0, 1, 3]), + without: set_from_u32s(vec![4]), }, ]; @@ -1613,23 +1407,27 @@ mod tests { #[test] fn try_iter_component_access_simple() { + use bevy_platform::collections::HashSet; + let mut access = Access::default(); - access.add_read(ComponentId::new(1)); - access.add_read(ComponentId::new(2)); - access.add_write(ComponentId::new(3)); - access.add_archetypal(ComponentId::new(5)); + access.add_read(ComponentId::from_u32(1)); + access.add_read(ComponentId::from_u32(2)); + access.add_write(ComponentId::from_u32(3)); + access.add_archetypal(ComponentId::from_u32(5)); - let result = access.try_iter_access().map(Iterator::collect::>); + let result = access + .try_iter_access() + .map(Iterator::collect::>); assert_eq!( result, - Ok(vec![ - ComponentAccessKind::Shared(ComponentId::new(1)), - ComponentAccessKind::Shared(ComponentId::new(2)), - ComponentAccessKind::Exclusive(ComponentId::new(3)), - ComponentAccessKind::Archetypal(ComponentId::new(5)), - ]), + Ok(HashSet::from_iter([ + ComponentAccessKind::Shared(ComponentId::from_u32(1)), + ComponentAccessKind::Shared(ComponentId::from_u32(2)), + ComponentAccessKind::Exclusive(ComponentId::from_u32(3)), + ComponentAccessKind::Archetypal(ComponentId::from_u32(5)), + ])), ); } @@ -1637,8 +1435,8 @@ mod tests { fn try_iter_component_access_unbounded_write_all() { let mut access = Access::default(); - access.add_read(ComponentId::new(1)); - access.add_read(ComponentId::new(2)); + access.add_read(ComponentId::from_u32(1)); + access.add_read(ComponentId::from_u32(2)); access.write_all(); let result = access.try_iter_access().map(Iterator::collect::>); @@ -1656,8 +1454,8 @@ mod tests { fn try_iter_component_access_unbounded_read_all() { let mut access = Access::default(); - access.add_read(ComponentId::new(1)); - access.add_read(ComponentId::new(2)); + access.add_read(ComponentId::from_u32(1)); + access.add_read(ComponentId::from_u32(2)); access.read_all(); let result = access.try_iter_access().map(Iterator::collect::>); @@ -1671,20 +1469,12 @@ mod tests { ); } - /// Create a `ComponentIdSet` with a given number of total bits and a given list of bits to set. - /// Setting the number of bits is important in tests since the `PartialEq` impl checks that the length matches. - fn bit_set(bits: usize, iter: impl IntoIterator) -> ComponentIdSet { - let mut result = FixedBitSet::with_capacity(bits); - result.extend(iter); - ComponentIdSet::from_bits(result) - } - #[test] fn invertible_union_with_tests() { let invertible_union = |mut self_inverted: bool, other_inverted: bool| { // Check all four possible bit states: In both sets, the first, the second, or neither - let mut self_set = bit_set(4, [0, 1]); - let other_set = bit_set(4, [0, 2]); + let mut self_set = set_from_u32s(vec![0, 1]); + let other_set = set_from_u32s(vec![0, 2]); invertible_union_with( &mut self_set, &mut self_inverted, @@ -1697,19 +1487,19 @@ mod tests { // Check each combination of `inverted` flags let (s, i) = invertible_union(false, false); // [0, 1] | [0, 2] = [0, 1, 2] - assert_eq!((s, i), (bit_set(4, [0, 1, 2]), false)); + assert_eq!((s, i), (set_from_u32s(vec![0, 1, 2]), false)); let (s, i) = invertible_union(false, true); // [0, 1] | [1, 3, ...] = [0, 1, 3, ...] - assert_eq!((s, i), (bit_set(4, [2]), true)); + assert_eq!((s, i), (set_from_u32s(vec![2]), true)); let (s, i) = invertible_union(true, false); // [2, 3, ...] | [0, 2] = [0, 2, 3, ...] - assert_eq!((s, i), (bit_set(4, [1]), true)); + assert_eq!((s, i), (set_from_u32s(vec![1]), true)); let (s, i) = invertible_union(true, true); // [2, 3, ...] | [1, 3, ...] = [1, 2, 3, ...] - assert_eq!((s, i), (bit_set(4, [0]), true)); + assert_eq!((s, i), (set_from_u32s(vec![0]), true)); } #[test] @@ -1718,9 +1508,9 @@ mod tests { // make sure we invert the bits beyond the original length. // Failing to call `grow` before `toggle_range` would cause bit 1 to be zero, // which would incorrectly treat it as included in the output set. - let mut self_set = bit_set(1, [0]); + let mut self_set = set_from_u32s(vec![0]); let mut self_inverted = false; - let other_set = bit_set(3, [0, 1]); + let other_set = set_from_u32s(vec![0, 1]); let other_inverted = true; invertible_union_with( &mut self_set, @@ -1730,15 +1520,15 @@ mod tests { ); // [0] | [2, ...] = [0, 2, ...] - assert_eq!((self_set, self_inverted), (bit_set(3, [1]), true)); + assert_eq!((self_set, self_inverted), (set_from_u32s(vec![1]), true)); } #[test] fn invertible_difference_with_tests() { let invertible_difference = |mut self_inverted: bool, other_inverted: bool| { // Check all four possible bit states: In both sets, the first, the second, or neither - let mut self_set = bit_set(4, [0, 1]); - let other_set = bit_set(4, [0, 2]); + let mut self_set = set_from_u32s(vec![0, 1]); + let other_set = set_from_u32s(vec![0, 2]); invertible_difference_with( &mut self_set, &mut self_inverted, @@ -1751,66 +1541,66 @@ mod tests { // Check each combination of `inverted` flags let (s, i) = invertible_difference(false, false); // [0, 1] - [0, 2] = [1] - assert_eq!((s, i), (bit_set(4, [1]), false)); + assert_eq!((s, i), (set_from_u32s(vec![1]), false)); let (s, i) = invertible_difference(false, true); // [0, 1] - [1, 3, ...] = [0] - assert_eq!((s, i), (bit_set(4, [0]), false)); + assert_eq!((s, i), (set_from_u32s(vec![0]), false)); let (s, i) = invertible_difference(true, false); // [2, 3, ...] - [0, 2] = [3, ...] - assert_eq!((s, i), (bit_set(4, [0, 1, 2]), true)); + assert_eq!((s, i), (set_from_u32s(vec![0, 1, 2]), true)); let (s, i) = invertible_difference(true, true); // [2, 3, ...] - [1, 3, ...] = [2] - assert_eq!((s, i), (bit_set(4, [2]), false)); + assert_eq!((s, i), (set_from_u32s(vec![2]), false)); } #[test] fn component_id_set_insert_remove_clear() { let mut set = ComponentIdSet::new(); - assert!(!set.contains(ComponentId::new(0))); - assert!(!set.contains(ComponentId::new(1))); - assert!(!set.contains(ComponentId::new(2))); - assert!(set.is_clear()); - set.insert(ComponentId::new(2)); - set.insert(ComponentId::new(1)); - assert!(!set.contains(ComponentId::new(0))); - assert!(set.contains(ComponentId::new(1))); - assert!(set.contains(ComponentId::new(2))); - assert!(!set.is_clear()); - set.remove(ComponentId::new(1)); - assert!(!set.contains(ComponentId::new(0))); - assert!(!set.contains(ComponentId::new(1))); - assert!(set.contains(ComponentId::new(2))); - assert!(!set.is_clear()); - set.insert(ComponentId::new(2)); - set.insert(ComponentId::new(1)); - assert!(!set.contains(ComponentId::new(0))); - assert!(set.contains(ComponentId::new(1))); - assert!(set.contains(ComponentId::new(2))); - assert!(!set.is_clear()); + assert!(!set.contains(&ComponentId::from_u32(0))); + assert!(!set.contains(&ComponentId::from_u32(1))); + assert!(!set.contains(&ComponentId::from_u32(2))); + assert!(set.is_empty()); + set.insert(ComponentId::from_u32(2)); + set.insert(ComponentId::from_u32(1)); + assert!(!set.contains(&ComponentId::from_u32(0))); + assert!(set.contains(&ComponentId::from_u32(1))); + assert!(set.contains(&ComponentId::from_u32(2))); + assert!(!set.is_empty()); + set.remove(&ComponentId::from_u32(1)); + assert!(!set.contains(&ComponentId::from_u32(0))); + assert!(!set.contains(&ComponentId::from_u32(1))); + assert!(set.contains(&ComponentId::from_u32(2))); + assert!(!set.is_empty()); + set.insert(ComponentId::from_u32(2)); + set.insert(ComponentId::from_u32(1)); + assert!(!set.contains(&ComponentId::from_u32(0))); + assert!(set.contains(&ComponentId::from_u32(1))); + assert!(set.contains(&ComponentId::from_u32(2))); + assert!(!set.is_empty()); set.clear(); - assert!(!set.contains(ComponentId::new(0))); - assert!(!set.contains(ComponentId::new(1))); - assert!(!set.contains(ComponentId::new(2))); - assert!(set.is_clear()); + assert!(!set.contains(&ComponentId::from_u32(0))); + assert!(!set.contains(&ComponentId::from_u32(1))); + assert!(!set.contains(&ComponentId::from_u32(2))); + assert!(set.is_empty()); } #[test] fn component_id_set_remove_out_of_range() { let mut set = ComponentIdSet::new(); - set.remove(ComponentId::new(3)); - set.insert(ComponentId::new(1)); - set.remove(ComponentId::new(4)); - assert!(set.iter().eq([1].map(ComponentId::new))); + set.remove(&ComponentId::from_u32(3)); + set.insert(ComponentId::from_u32(1)); + set.remove(&ComponentId::from_u32(4)); + assert!(set.iter().eq([&ComponentId::from_u32(1)])); } #[test] fn component_id_set_is_subset_is_disjoint() { - let set_1234 = ComponentIdSet::from_iter([1, 2, 3, 4].map(ComponentId::new)); - let set_23 = ComponentIdSet::from_iter([2, 3].map(ComponentId::new)); - let set_45 = ComponentIdSet::from_iter([4, 5].map(ComponentId::new)); + let set_1234 = set_from_u32s(vec![1, 2, 3, 4]); + let set_23 = set_from_u32s(vec![2, 3]); + let set_45 = set_from_u32s(vec![4, 5]); assert!(set_23.is_subset(&set_1234)); assert!(!set_1234.is_subset(&set_23)); assert!(set_23.is_disjoint(&set_45)); @@ -1821,52 +1611,74 @@ mod tests { #[test] fn component_id_set_union_intersection_difference() { - let set_13 = ComponentIdSet::from_iter([1, 3].map(ComponentId::new)); - let set_23 = ComponentIdSet::from_iter([2, 3].map(ComponentId::new)); + let set_13 = set_from_u32s(vec![1, 3]); + let set_23 = set_from_u32s(vec![2, 3]); - assert!(set_13.union(&set_23).eq([1, 3, 2].map(ComponentId::new))); - assert!(set_23.union(&set_13).eq([2, 3, 1].map(ComponentId::new))); - assert!(set_13.intersection(&set_23).eq([3].map(ComponentId::new))); - assert!(set_23.intersection(&set_13).eq([3].map(ComponentId::new))); - assert!(set_13.difference(&set_23).eq([1].map(ComponentId::new))); - assert!(set_23.difference(&set_13).eq([2].map(ComponentId::new))); + assert_eq!( + set_13.union(&set_23).copied().collect::(), + set_from_u32s(vec![1, 2, 3]) + ); + assert_eq!( + set_23.union(&set_13).copied().collect::(), + set_from_u32s(vec![1, 2, 3]) + ); + assert_eq!( + set_13 + .intersection(&set_23) + .copied() + .collect::(), + set_from_u32s(vec![3]) + ); + assert_eq!( + set_23 + .intersection(&set_13) + .copied() + .collect::(), + set_from_u32s(vec![3]) + ); + assert_eq!( + set_13 + .difference(&set_23) + .copied() + .collect::(), + set_from_u32s(vec![1]) + ); + assert_eq!( + set_23 + .difference(&set_13) + .copied() + .collect::(), + set_from_u32s(vec![2]) + ); } #[test] fn component_id_set_union_intersection_difference_with() { - let set_13 = ComponentIdSet::from_iter([1, 3].map(ComponentId::new)); - let set_23 = ComponentIdSet::from_iter([2, 3].map(ComponentId::new)); - - let mut s = set_13.clone(); - s.union_with(&set_23); - assert!(s.iter().eq([1, 2, 3].map(ComponentId::new))); - - let mut s = set_23.clone(); - s.union_with(&set_13); - assert!(s.iter().eq([1, 2, 3].map(ComponentId::new))); + let set_13 = set_from_u32s(vec![1, 3]); + let set_23 = set_from_u32s(vec![2, 3]); let mut s = set_13.clone(); - s.intersect_with(&set_23); - assert!(s.iter().eq([3].map(ComponentId::new))); + s.bitor_assign(&set_23); + assert_eq!(s, set_from_u32s(vec![1, 2, 3])); let mut s = set_23.clone(); - s.intersect_with(&set_13); - assert!(s.iter().eq([3].map(ComponentId::new))); + s.bitor_assign(&set_13); + assert_eq!(s, set_from_u32s(vec![1, 2, 3])); let mut s = set_13.clone(); - s.difference_with(&set_23); - assert!(s.iter().eq([1].map(ComponentId::new))); + s.bitand_assign(&set_23); + assert_eq!(s, set_from_u32s(vec![3])); let mut s = set_23.clone(); - s.difference_with(&set_13); - assert!(s.iter().eq([2].map(ComponentId::new))); + s.bitand_assign(&set_13); + assert_eq!(s, set_from_u32s(vec![3])); let mut s = set_13.clone(); - s.difference_from(&set_23); - assert!(s.iter().eq([2].map(ComponentId::new))); + s.sub_assign(&set_23); + assert_eq!(s, set_from_u32s(vec![1])); let mut s = set_23.clone(); - s.difference_from(&set_13); - assert!(s.iter().eq([1].map(ComponentId::new))); + s.sub_assign(&set_13); + assert_eq!(s, set_from_u32s(vec![2])); } } diff --git a/crates/bevy_ecs/src/query/access_iter.rs b/crates/bevy_ecs/src/query/access_iter.rs index a33d7d77ae2c9..4a3ab8056ad34 100644 --- a/crates/bevy_ecs/src/query/access_iter.rs +++ b/crates/bevy_ecs/src/query/access_iter.rs @@ -70,7 +70,7 @@ fn has_conflicts_large<'a, Q: QueryData>( let needs_check = match access { EcsAccessType::Component(EcsAccessLevel::Read(component_id)) | EcsAccessType::Component(EcsAccessLevel::Write(component_id)) => { - filter.check_insert(&component_id.index()) + filter.check_insert(&component_id) } EcsAccessType::Component(EcsAccessLevel::ReadAll) | EcsAccessType::Component(EcsAccessLevel::WriteAll) => true, @@ -81,7 +81,7 @@ fn has_conflicts_large<'a, Q: QueryData>( let index = match kind { crate::query::ComponentAccessKind::Shared(id) | crate::query::ComponentAccessKind::Exclusive(id) - | crate::query::ComponentAccessKind::Archetypal(id) => id.index(), + | crate::query::ComponentAccessKind::Archetypal(id) => id, }; if filter.check_insert(&index) { needs_check = true; @@ -110,7 +110,7 @@ fn has_conflicts_large<'a, Q: QueryData>( } /// The data storage type that is being accessed. -#[derive(Copy, Clone, Debug, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum EcsAccessType<'a> { /// Accesses [`Component`](crate::prelude::Component) data Component(EcsAccessLevel), diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 6b97387cd980c..31ae363399a98 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -560,7 +560,7 @@ impl QueryState { self.validate_world(world.id()); D::update_archetypes(&mut self.fetch_state, world); F::update_archetypes(&mut self.filter_state, world); - if self.component_access.required.is_clear() { + if self.component_access.required.is_empty() { let archetypes = world.archetypes(); let old_generation = core::mem::replace(&mut self.archetype_generation, archetypes.generation()); @@ -587,7 +587,7 @@ impl QueryState { world .archetypes() .component_index() - .get(&component_id) + .get(component_id) .map(|index| index.keys()) }) // select the component with the fewest archetypes @@ -666,8 +666,8 @@ impl QueryState { /// Returns `true` if this query matches a set of components. Otherwise, returns `false`. pub fn matches_component_set(&self, set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { self.component_access.filter_sets.iter().any(|set| { - set.with.iter().all(set_contains_id) - && set.without.iter().all(|index| !set_contains_id(index)) + set.with.iter().copied().all(set_contains_id) + && set.without.iter().all(|index| !set_contains_id(*index)) }) } diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 5189f5e3465f8..fbce6963f116c 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -211,7 +211,7 @@ impl IsResource { } /// [`ComponentId`] of the [`IsResource`] component. -pub const IS_RESOURCE: ComponentId = ComponentId::new(crate::component::IS_RESOURCE); +pub const IS_RESOURCE: ComponentId = ComponentId::from_u32(crate::component::IS_RESOURCE); #[cfg(test)] mod tests { diff --git a/crates/bevy_ecs/src/schedule/node.rs b/crates/bevy_ecs/src/schedule/node.rs index b98815dc0b86d..302aca38943f6 100644 --- a/crates/bevy_ecs/src/schedule/node.rs +++ b/crates/bevy_ecs/src/schedule/node.rs @@ -615,6 +615,7 @@ impl Systems { AccessConflicts::Individual(conflicts) => { let conflicts: Box<[_]> = conflicts .iter() + .copied() .filter(|id| !ignored_ambiguities.contains(id)) .collect(); if !conflicts.is_empty() { diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 13460e2defd1e..7409f7a162d32 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -937,12 +937,12 @@ mod tests { collected_sets.sort(); assert_eq!( collected_sets, - vec![(ComponentId::new(1), 0), (ComponentId::new(2), 0),] + vec![(ComponentId::from_u32(2), 0), (ComponentId::from_u32(1), 0),] ); - fn register_component(sets: &mut SparseSets, id: usize) { + fn register_component(sets: &mut SparseSets, id: u32) { let descriptor = ComponentDescriptor::new::(); - let id = ComponentId::new(id); + let id = ComponentId::from_u32(id); let info = ComponentInfo::new(id, descriptor); sets.get_or_insert(&info); } diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 9b3a0c86c097d..1debdd3926d08 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -857,8 +857,8 @@ impl Drop for Table { mod tests { use crate::{ change_detection::{MaybeLocation, Tick}, - component::{Component, ComponentIds, Components, ComponentsRegistrator}, - entity::{Entity, EntityIndex}, + component::{Component, Components, ComponentsRegistrator}, + entity::{Entity, EntityAllocator, EntityIndex}, ptr::OwningPtr, storage::{TableBuilder, TableId, TableRow, Tables}, }; @@ -882,10 +882,10 @@ mod tests { #[test] fn table() { let mut components = Components::default(); - let mut componentids = ComponentIds::default(); + let mut allocator = EntityAllocator::default(); // SAFETY: They are both new. let mut registrator = - unsafe { ComponentsRegistrator::new(&mut components, &mut componentids) }; + unsafe { ComponentsRegistrator::new(&mut components, &mut allocator) }; let component_id = registrator.register_component::>(); let columns = &[component_id]; let mut table = TableBuilder::with_capacity(0, columns.len()) diff --git a/crates/bevy_ecs/src/world/entity_access/mod.rs b/crates/bevy_ecs/src/world/entity_access/mod.rs index a4a63e76d2cf3..f11516bca908b 100644 --- a/crates/bevy_ecs/src/world/entity_access/mod.rs +++ b/crates/bevy_ecs/src/world/entity_access/mod.rs @@ -99,7 +99,7 @@ mod tests { #[test] fn entity_ref_get_by_id_invalid_component_id() { - let invalid_component_id = ComponentId::new(usize::MAX); + let invalid_component_id = ComponentId::new(Entity::PLACEHOLDER); let mut world = World::new(); let entity = world.spawn_empty().id(); @@ -109,7 +109,7 @@ mod tests { #[test] fn entity_mut_get_by_id_invalid_component_id() { - let invalid_component_id = ComponentId::new(usize::MAX); + let invalid_component_id = ComponentId::new(Entity::PLACEHOLDER); let mut world = World::new(); let mut entity = world.spawn_empty(); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index bd7c9e33133b0..f80c4217b30ce 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -39,7 +39,7 @@ use crate::{ CheckChangeTicks, ComponentTicks, ComponentTicksMut, MaybeLocation, MutUntyped, Tick, }, component::{ - Component, ComponentDescriptor, ComponentId, ComponentIds, ComponentInfo, Components, + Component, ComponentDescriptor, ComponentId, ComponentInfo, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, }, @@ -95,7 +95,6 @@ pub struct World { pub(crate) entities: Entities, pub(crate) entity_allocator: EntityAllocator, pub(crate) components: Components, - pub(crate) component_ids: ComponentIds, pub(crate) resource_entities: ResourceEntities, pub(crate) archetypes: Archetypes, pub(crate) storages: Storages, @@ -129,7 +128,6 @@ impl Default for World { last_check_tick: Tick::new(0), last_trigger_id: 0, command_queue: RawCommandQueue::new(), - component_ids: ComponentIds::default(), }; world.bootstrap(); world @@ -266,14 +264,19 @@ impl World { #[inline] pub fn components_queue(&self) -> ComponentsQueuedRegistrator<'_> { // SAFETY: These are from the same world. - unsafe { ComponentsQueuedRegistrator::new(&self.components, &self.component_ids) } + unsafe { + ComponentsQueuedRegistrator::new( + &self.components, + self.entity_allocator().build_remote_allocator(), + ) + } } /// Prepares a [`ComponentsRegistrator`] for the world. #[inline] pub fn components_registrator(&mut self) -> ComponentsRegistrator<'_> { // SAFETY: These are from the same world. - unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) } + unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.entity_allocator) } } /// Retrieves this world's [`Storages`] collection. @@ -3347,7 +3350,7 @@ impl World { pub(crate) fn register_bundle_info(&mut self) -> BundleId { // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. let mut registrator = - unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; + unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.entity_allocator) }; // SAFETY: `registrator`, `self.storages` and `self.bundles` all come from this world. unsafe { @@ -3359,7 +3362,7 @@ impl World { pub(crate) fn register_contributed_bundle_info(&mut self) -> BundleId { // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. let mut registrator = - unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; + unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.entity_allocator) }; // SAFETY: `registrator`, `self.bundles` and `self.storages` are all from this world. unsafe { From 96d54c97966133ce8a4681face10b4b88a14f4b1 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 23 Jun 2026 18:17:19 +0200 Subject: [PATCH 02/44] removed ResourceEntities --- .../components-as-entities.md | 5 + crates/bevy_ecs/src/name.rs | 2 +- crates/bevy_ecs/src/observer/mod.rs | 2 +- crates/bevy_ecs/src/resource.rs | 125 +++++---------- crates/bevy_ecs/src/storage/sparse_set.rs | 13 -- .../src/world/entity_access/world_mut.rs | 15 +- crates/bevy_ecs/src/world/mod.rs | 144 +++++++++--------- crates/bevy_ecs/src/world/reflect.rs | 7 +- .../bevy_ecs/src/world/unsafe_world_cell.rs | 22 +-- crates/bevy_settings/src/lib.rs | 7 +- .../src/dynamic_world.rs | 13 +- .../src/dynamic_world_builder.rs | 19 ++- .../src/world_asset.rs | 56 ++++--- 13 files changed, 200 insertions(+), 230 deletions(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index 829f075d113ac..ffad9b1a6dd33 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -13,3 +13,8 @@ pull_requests: [ ... ] - `ComponentsQueuedRegistrator::new` not takes `RemoteAllocator` instead of `ComponentIds`. - `Access` no longer derives `Hash`. - `EcsAccessType` no longer derives `Hash`. +- Removed `SparseArray::iter`. Not user-facing but worth mentioning. +- Removed `EntityWorldMut::resource_entities`. +- Removed `World::resource_entities`. +- Removed `UnsafeWorldCell::resource_entities`. +- Removed `ResourceEntities`. diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 14898c1ab76ee..926c5c7a22211 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -299,7 +299,7 @@ mod tests { let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} - assert_eq!(d1.to_string(), "10v0"); + assert_eq!(d1.to_string(), "9v0"); let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 2a2189810d074..caf5c38026cf1 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -646,7 +646,7 @@ mod tests { world.add_observer(|_: On, mut res: ResMut| res.observed("add_2")); world.spawn(A).flush(); - assert_eq!(vec!["add_2", "add_1"], world.resource::().0); + assert_eq!(vec!["add_1", "add_2"], world.resource::().0); // we have one A entity and two observers assert_eq!(world.query::<&A>().query(&world).count(), 1); assert_eq!(world.query::<&Observer>().query(&world).count(), 2); diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index fbce6963f116c..5a32063d00b04 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -4,16 +4,14 @@ use log::warn; use crate::{ component::{Component, ComponentId}, - entity::Entity, + entity::ContainsEntity, lifecycle::HookContext, - storage::SparseArray, world::DeferredWorld, }; #[cfg(feature = "bevy_reflect")] use {crate::reflect::ReflectComponent, bevy_reflect::Reflect}; // The derive macro for the `Resource` trait pub use bevy_ecs_macros::Resource; -use bevy_platform::cell::SyncUnsafeCell; /// A type that can be inserted into a [`World`] as a singleton. /// @@ -86,37 +84,6 @@ use bevy_platform::cell::SyncUnsafeCell; )] pub trait Resource: Component {} -/// A cache that links each `ComponentId` from a resource to the corresponding entity. -#[derive(Default)] -pub struct ResourceEntities(SyncUnsafeCell>); - -impl ResourceEntities { - /// Returns an iterator over all registered resource components and their corresponding entity. - /// - /// This must scan the entire array of components to find non-empty values, - /// which may be slow even if there are few resources. - #[inline] - pub fn iter(&self) -> impl Iterator { - self.deref().iter().map(|(id, entity)| (id, *entity)) - } - - /// Returns the entity for the given resource component, or `None` if there is no entity. - #[inline] - pub fn get(&self, id: ComponentId) -> Option { - self.deref().get(id).copied() - } - - #[inline] - fn deref(&self) -> &SparseArray { - // SAFETY: There are no other mutable references to the map. - // The underlying `SyncUnsafeCell` is never exposed outside this module, - // so mutable references are only created by the resource hooks. - // We only expose `&ResourceCache` to code with access to a resource (such as `&World`), - // and that would conflict with the `DeferredWorld` passed to the resource hook. - unsafe { &*self.0.get() } - } -} - /// A marker component for entities that have a Resource component. #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Debug))] #[derive(Component, Debug)] @@ -141,43 +108,36 @@ impl IsResource { .unwrap() .resource_component_id(); - if let Some(original_entity) = world.resource_entities.get(resource_component_id) { - if !world.entities().contains(original_entity) { - let name = world - .components() - .get_name(resource_component_id) - .expect("resource is registered"); - panic!( - "Resource entity {} of {} has been despawned, when it's not supposed to be.", - original_entity, name - ); - } + let original_entity = resource_component_id.entity(); + + if !world.entities().contains(original_entity) { + let name = world + .components() + .get_name(resource_component_id) + .expect("resource is registered"); + panic!( + "Resource entity {} of {} has been despawned, when it's not supposed to be.", + original_entity, name + ); + } - if original_entity != context.entity { - // the resource already exists and the new one should be removed - world - .commands() - .entity(context.entity) - .remove_by_id(resource_component_id); - world - .commands() - .entity(context.entity) - .remove_by_id(context.component_id); - let name = world - .components() - .get_name(resource_component_id) - .expect("resource is registered"); - warn!("Tried inserting the resource {} while one already exists. \ - Resources are unique components stored on a single entity. \ - Inserting on a different entity, when one already exists, causes the new value to be removed.", name); - } - } else { - // SAFETY: We have exclusive world access (as long as we don't make structural changes). - let cache = unsafe { world.as_unsafe_world_cell().resource_entities() }; - // SAFETY: There are no shared references to the map. - // We only expose `&ResourceCache` to code with access to a resource (such as `&World`), - // and that would conflict with the `DeferredWorld` passed to the resource hook. - unsafe { &mut *cache.0.get() }.insert(resource_component_id, context.entity); + if original_entity != context.entity { + // the resource already exists and the new one should be removed + world + .commands() + .entity(context.entity) + .remove_by_id(resource_component_id); + world + .commands() + .entity(context.entity) + .remove_by_id(context.component_id); + let name = world + .components() + .get_name(resource_component_id) + .expect("resource is registered"); + warn!("Tried inserting the resource {} while one already exists. \ + Resources are unique components stored on a single entity. \ + Inserting on a different entity, when one already exists, causes the new value to be removed.", name); } } @@ -188,16 +148,9 @@ impl IsResource { .unwrap() .resource_component_id(); - if let Some(resource_entity) = world.resource_entities.get(resource_component_id) - && resource_entity == context.entity - { - // SAFETY: We have exclusive world access (as long as we don't make structural changes). - let cache = unsafe { world.as_unsafe_world_cell().resource_entities() }; - // SAFETY: There are no shared references to the map. - // We only expose `&ResourceCache` to code with access to a resource (such as `&World`), - // and that would conflict with the `DeferredWorld` passed to the resource hook. - unsafe { &mut *cache.0.get() }.remove(resource_component_id); + let original_entity = resource_component_id.entity(); + if original_entity == context.entity { world .commands() .entity(context.entity) @@ -219,7 +172,7 @@ mod tests { use crate::{ change_detection::MaybeLocation, - entity::Entity, + entity::{ContainsEntity, Entity}, lifecycle::HookContext, ptr::OwningPtr, resource::{IsResource, Resource}, @@ -259,7 +212,7 @@ mod tests { } }); assert_eq!(world.entities().count_spawned(), start + 3); - let e3 = world.resource_entities().get(id3).unwrap(); + let e3 = id3.entity(); assert!(world.remove_resource_by_id(id3)); // the entity is stable: removing the resource should only remove the component from the entity, not despawn the entity assert_eq!(world.entities().count_spawned(), start + 3); @@ -269,13 +222,13 @@ mod tests { world.insert_resource_by_id(id3, ptr, MaybeLocation::caller()); } }); - assert_eq!(e3, world.resource_entities().get(id3).unwrap()); + assert_eq!(e3, id3.entity()); // again, the entity is stable: see previous explanation - let e1 = world.resource_entities().get(id1).unwrap(); + let e1 = id1.entity(); world.remove_resource::(); assert_eq!(world.entities().count_spawned(), start + 3); world.init_resource::(); - assert_eq!(e1, world.resource_entities().get(id1).unwrap()); + assert_eq!(e1, id1.entity()); // make sure that trying to add a resource twice results, doesn't change the entity count world.insert_resource(TestResource2(String::from("Bar"))); assert_eq!(world.entities().count_spawned(), start + 3); @@ -319,9 +272,9 @@ mod tests { entity }; - assert_ne!( + assert_eq!( first_entity, second_entity, - "The first resource entity was invalidated, so the second initialization should be new" + "The resource should always be inserted on the component entity" ); let id = world.spawn(TestResource).id(); diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 7409f7a162d32..e0e2a99e62245 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -135,19 +135,6 @@ impl SparseArray { marker: PhantomData, } } - - /// Returns an iterator over the non-empty values in the array. - /// - /// This must scan the entire array to find non-empty values, - /// which may be slow even if the array is sparsely populated. - #[inline] - pub(crate) fn iter(&self) -> impl Iterator { - self.values.iter().enumerate().filter_map(|(index, value)| { - value - .as_ref() - .map(|value| (SparseSetIndex::get_sparse_set_index(index), value)) - }) - } } /// A sparse data structure of [`Component`](crate::component::Component)s. diff --git a/crates/bevy_ecs/src/world/entity_access/world_mut.rs b/crates/bevy_ecs/src/world/entity_access/world_mut.rs index 6ae9d32e255eb..dfc57cf6732a4 100644 --- a/crates/bevy_ecs/src/world/entity_access/world_mut.rs +++ b/crates/bevy_ecs/src/world/entity_access/world_mut.rs @@ -5,7 +5,9 @@ use crate::{ }, change_detection::{ComponentTicks, MaybeLocation, MutUntyped, Tick}, component::{Component, ComponentId, Components, Mutable, StorageType}, - entity::{Entity, EntityCloner, EntityClonerBuilder, EntityLocation, OptIn, OptOut}, + entity::{ + ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityLocation, OptIn, OptOut, + }, event::{EntityComponentsTrigger, EntityEvent}, lifecycle::{Despawn, Discard, Remove, DESPAWN, DISCARD, REMOVE}, observer::IntoEntityObserver, @@ -14,7 +16,7 @@ use crate::{ ReleaseStateQueryData, SingleEntityQueryData, }, relationship::RelationshipHookMode, - resource::{Resource, ResourceEntities}, + resource::Resource, storage::{SparseSets, Table}, system::EntityCommands, template::{SceneEntityReferences, Template, TemplateContext}, @@ -735,19 +737,12 @@ impl<'w> EntityWorldMut<'w> { }) } - /// Retrieves this world's [`ResourceEntities`]. - #[inline] - #[track_caller] - pub fn resource_entities(&self) -> &ResourceEntities { - self.world.resource_entities() - } - /// Retrieves the [`Entity`] associated with the resource of type `R`, if it exists. #[inline] #[track_caller] pub fn resource_entity(&self) -> Option { let component_id = self.world.component_id::()?; - self.world.resource_entities().get(component_id) + Some(component_id.entity()) } /// Retrieves the change ticks for the given component. This can be useful for implementing change diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index f80c4217b30ce..d157457ce58e3 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -43,7 +43,9 @@ use crate::{ ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, }, - entity::{Entities, Entity, EntityAllocator, EntityNotSpawnedError, SpawnError}, + entity::{ + ContainsEntity, Entities, Entity, EntityAllocator, EntityNotSpawnedError, SpawnError, + }, entity_disabling::DefaultQueryFilters, error::{ErrorHandler, FallbackErrorHandler}, lifecycle::{ComponentHooks, RemovedComponentMessages, ADD, DESPAWN, DISCARD, INSERT, REMOVE}, @@ -52,7 +54,7 @@ use crate::{ prelude::{Add, Despawn, Discard, Insert, Remove}, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, relationship::RelationshipHookMode, - resource::{IsResource, Resource, ResourceEntities, IS_RESOURCE}, + resource::{IsResource, Resource, IS_RESOURCE}, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{NonSendData, Storages}, system::Commands, @@ -95,7 +97,6 @@ pub struct World { pub(crate) entities: Entities, pub(crate) entity_allocator: EntityAllocator, pub(crate) components: Components, - pub(crate) resource_entities: ResourceEntities, pub(crate) archetypes: Archetypes, pub(crate) storages: Storages, pub(crate) bundles: Bundles, @@ -115,7 +116,6 @@ impl Default for World { entities: Entities::new(), entity_allocator: EntityAllocator::default(), components: Default::default(), - resource_entities: Default::default(), archetypes: Archetypes::new(), storages: Default::default(), bundles: Default::default(), @@ -252,12 +252,6 @@ impl World { &self.components } - /// Retrieves this world's [`ResourceEntities`]. - #[inline] - pub fn resource_entities(&self) -> &ResourceEntities { - &self.resource_entities - } - /// Prepares a [`ComponentsQueuedRegistrator`] for the world. /// **NOTE:** [`ComponentsQueuedRegistrator`] is easily misused. /// See its docs for important notes on when and how it should be used. @@ -1488,7 +1482,8 @@ impl World { f: impl FnOnce(&mut R) -> S, ) -> Result, EntityMutableFetchError> { let component_id = self.register_component::(); - if let Some(entity) = self.resource_entities.get(component_id) { + let entity = component_id.entity(); + if self.entities().contains_spawned(entity) { let mut world = DeferredWorld::from(&mut *self); let result = world.modify_component_with_relationship_hook_mode( entity, @@ -1524,7 +1519,8 @@ impl World { component_id: ComponentId, f: impl for<'a> FnOnce(MutUntyped<'a>) -> S, ) -> Result, EntityMutableFetchError> { - if let Some(entity) = self.resource_entities.get(component_id) { + let entity = component_id.entity(); + if self.entities().contains_spawned(entity) { let mut world = DeferredWorld::from(&mut *self); let result = world.modify_component_by_id_with_relationship_hook_mode( @@ -1926,9 +1922,9 @@ impl World { caller: MaybeLocation, ) -> (ComponentId, EntityWorldMut<'_>) { let resource_id = self.register_component::(); + let entity = resource_id.entity(); - if let Some(entity) = self.resource_entities.get(resource_id) { - let entity_ref = self.get_entity(entity).expect("ResourceCache is in sync"); + if let Ok(entity_ref) = self.get_entity(entity) { if !entity_ref.contains_id(resource_id) { let resource = func(self); move_as_ptr!(resource); @@ -1944,7 +1940,7 @@ impl World { let resource = func(self); move_as_ptr!(resource); - let entity_mut = self.spawn_with_caller(resource, caller); // ResourceCache is updated automatically + let entity_mut = self.spawn_at_with_caller(entity, resource, caller).unwrap(); (resource_id, entity_mut) } @@ -2050,7 +2046,7 @@ impl World { #[inline] pub fn remove_resource(&mut self) -> Option { let resource_id = self.component_id::()?; - let entity = self.resource_entities.get(resource_id)?; + let entity = resource_id.entity(); let value = self .get_entity_mut(entity) .expect("ResourceCache is in sync") @@ -2088,9 +2084,7 @@ impl World { /// Returns `true` if a resource with provided `component_id` exists. Otherwise returns `false`. #[inline] pub fn contains_resource_by_id(&self, component_id: ComponentId) -> bool { - if let Some(entity) = self.resource_entities.get(component_id) - && let Ok(entity_ref) = self.get_entity(entity) - { + if let Ok(entity_ref) = self.get_entity(component_id.entity()) { return entity_ref.contains_id(component_id); } false @@ -2178,7 +2172,7 @@ impl World { &self, component_id: ComponentId, ) -> Option { - let entity = self.resource_entities.get(component_id)?; + let entity = component_id.entity(); let entity_ref = self.get_entity(entity).ok()?; entity_ref.get_change_ticks_by_id(component_id) } @@ -2806,7 +2800,7 @@ impl World { let change_tick = self.change_tick(); let component_id = self.components.valid_component_id::()?; - let entity = self.resource_entities.get(component_id)?; + let entity = component_id.entity(); let mut entity_mut = self.get_entity_mut(entity).ok()?; let mut ticks = entity_mut.get_change_ticks::()?; @@ -2989,12 +2983,12 @@ impl World { value: OwningPtr<'_>, caller: MaybeLocation, ) { + let entity = component_id.entity(); // if the resource already exists, we replace it on the same entity - let mut entity_mut = if let Some(entity) = self.resource_entities.get(component_id) { - self.get_entity_mut(entity) - .expect("ResourceCache is in sync") + let mut entity_mut = if self.entities().contains_spawned(entity) { + self.entity_mut(entity) } else { - self.spawn_empty() + self.spawn_empty_at(entity).unwrap() }; // SAFETY: pointer valid for this component id per precondition unsafe { @@ -3323,9 +3317,17 @@ impl World { /// This can easily cause systems expecting certain resources to immediately start panicking. /// Use with caution. pub fn clear_resources(&mut self) { - let pairs: Vec<(ComponentId, Entity)> = self.resource_entities().iter().collect(); - for (component_id, entity) in pairs { - self.entity_mut(entity).remove_by_id(component_id); + let ids: Vec = self + .components() + .iter_registered() + .map(ComponentInfo::id) + .collect(); + for component_id in ids { + let entity = component_id.entity(); + if self.entities().contains_spawned(entity) { + // only resource entities should have a component where the component_id matches the entity. + self.entity_mut(entity).remove_by_id(component_id); + } } } @@ -3524,14 +3526,18 @@ impl World { /// ``` #[inline] pub fn iter_resources(&self) -> impl Iterator)> { - self.resource_entities - .iter() - .filter_map(|(component_id, entity)| { - let component_info = self.components().get_info(component_id)?; - let entity_cell = self.get_entity(entity).ok()?; - let resource = entity_cell.get_by_id(component_id).ok()?; - Some((component_info, resource)) - }) + let ids: Vec = self + .components() + .iter_registered() + .map(ComponentInfo::id) + .collect(); + ids.into_iter().filter_map(|component_id| { + let entity = component_id.entity(); + let component_info = self.components().get_info(component_id)?; + let entity_cell = self.get_entity(entity).ok()?; + let resource = entity_cell.get_by_id(component_id).ok()?; + Some((component_info, resource)) + }) } /// Mutably iterates over all resources in the world. @@ -3600,30 +3606,32 @@ impl World { /// # assert_eq!(world.resource::().0, 3); /// ``` pub fn iter_resources_mut(&mut self) -> impl Iterator)> { + let ids: Vec = self + .components() + .iter_registered() + .map(ComponentInfo::id) + .collect(); + let unsafe_world = self.as_unsafe_world_cell(); - // SAFETY: exclusive world access to all resources - let resource_entities = unsafe { unsafe_world.resource_entities() }; let components = unsafe_world.components(); - resource_entities - .iter() - .filter_map(move |(component_id, entity)| { - // SAFETY: If a resource has been initialized, a corresponding ComponentInfo must exist with its ID. - let component_info = - unsafe { components.get_info(component_id).debug_checked_unwrap() }; - - let entity_cell = unsafe_world.get_entity(entity).ok()?; + ids.into_iter().filter_map(move |component_id| { + // SAFETY: If a resource has been initialized, a corresponding ComponentInfo must exist with its ID. + let component_info = + unsafe { components.get_info(component_id).debug_checked_unwrap() }; - // SAFETY: - // - We have exclusive world access - // - `UnsafeEntityCell::get_mut_by_id` doesn't access components - // or resource_entities mutably - // - `resource_entities` doesn't contain duplicate entities, so - // no duplicate references are created - let mut_untyped = unsafe { entity_cell.get_mut_by_id(component_id).ok()? }; + let entity_cell = unsafe_world.get_entity(component_id.entity()).ok()?; - Some((component_info, mut_untyped)) - }) + // SAFETY: + // - We have exclusive world access + // - `UnsafeEntityCell::get_mut_by_id` doesn't access components + // or resource_entities mutably + // - `resource_entities` doesn't contain duplicate entities, so + // no duplicate references are created + let mut_untyped = unsafe { entity_cell.get_mut_by_id(component_id).ok()? }; + + Some((component_info, mut_untyped)) + }) } /// Gets a pointer to `!Send` data with the id [`ComponentId`] if it exists. @@ -3673,8 +3681,8 @@ impl World { /// **You should prefer to use the typed API [`World::remove_resource`] where possible and only /// use this in cases where the actual types are not known at compile time.** pub fn remove_resource_by_id(&mut self, component_id: ComponentId) -> bool { - if let Some(entity) = self.resource_entities.get(component_id) - && let Ok(mut entity_mut) = self.get_entity_mut(entity) + let entity = component_id.entity(); + if let Ok(mut entity_mut) = self.get_entity_mut(entity) && entity_mut.contains_id(component_id) { entity_mut.remove_by_id(component_id); @@ -4146,11 +4154,6 @@ mod tests { let mut iter = world.iter_resources(); - let (info, ptr) = iter.next().unwrap(); - assert_eq!(info.name(), DebugName::type_name::()); - // SAFETY: We know that the resource is of type `TestResource` - assert_eq!(unsafe { ptr.deref::().0 }, 42); - let (info, ptr) = iter.next().unwrap(); assert_eq!(info.name(), DebugName::type_name::()); assert_eq!( @@ -4159,6 +4162,11 @@ mod tests { &"Hello, world!".to_string() ); + let (info, ptr) = iter.next().unwrap(); + assert_eq!(info.name(), DebugName::type_name::()); + // SAFETY: We know that the resource is of type `TestResource` + assert_eq!(unsafe { ptr.deref::().0 }, 42); + assert!(iter.next().is_none()); } @@ -4175,17 +4183,17 @@ mod tests { let mut iter = world.iter_resources_mut(); let (info, mut mut_untyped) = iter.next().unwrap(); - assert_eq!(info.name(), DebugName::type_name::()); - // SAFETY: We know that the resource is of type `TestResource` + assert_eq!(info.name(), DebugName::type_name::()); + // SAFETY: We know that the resource is of type `TestResource2` unsafe { - mut_untyped.as_mut().deref_mut::().0 = 43; + mut_untyped.as_mut().deref_mut::().0 = "Hello, world?".to_string(); }; let (info, mut mut_untyped) = iter.next().unwrap(); - assert_eq!(info.name(), DebugName::type_name::()); - // SAFETY: We know that the resource is of type `TestResource2` + assert_eq!(info.name(), DebugName::type_name::()); + // SAFETY: We know that the resource is of type `TestResource` unsafe { - mut_untyped.as_mut().deref_mut::().0 = "Hello, world?".to_string(); + mut_untyped.as_mut().deref_mut::().0 = 43; }; assert!(iter.next().is_none()); diff --git a/crates/bevy_ecs/src/world/reflect.rs b/crates/bevy_ecs/src/world/reflect.rs index 0c7909d5bc4aa..2dd24a3c68e6b 100644 --- a/crates/bevy_ecs/src/world/reflect.rs +++ b/crates/bevy_ecs/src/world/reflect.rs @@ -199,10 +199,13 @@ impl World { resource_id: ComponentId, reflected_resource: Box, ) { - if let Some(entity) = self.resource_entities().get(resource_id) { + let entity = resource_id.entity(); + if self.entities().contains_spawned(entity) { self.entity_mut(entity).insert_reflect(reflected_resource); } else { - self.spawn_empty().insert_reflect(reflected_resource); + self.spawn_empty_at(entity) + .unwrap() + .insert_reflect(reflected_resource); } } } diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 2ada7b14a831b..6782d26f0e704 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -17,7 +17,7 @@ use crate::{ observer::Observers, prelude::Component, query::{DebugCheckedUnwrap, QueryAccessError, ReleaseStateQueryData, SingleEntityQueryData}, - resource::{Resource, ResourceEntities}, + resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, }; @@ -292,17 +292,6 @@ impl<'w> UnsafeWorldCell<'w> { &unsafe { self.world_metadata() }.components } - /// Retrieves this world's resource-entity map. - /// - /// # Safety - /// The caller must have exclusive read or write access to the resources that are updated in the cache. - #[inline] - pub unsafe fn resource_entities(self) -> &'w ResourceEntities { - // SAFETY: - // - we only access world metadata - &unsafe { self.world_metadata() }.resource_entities - } - /// Retrieves this world's collection of [removed components](RemovedComponentMessages). pub fn removed_components(self) -> &'w RemovedComponentMessages { // SAFETY: @@ -468,8 +457,7 @@ impl<'w> UnsafeWorldCell<'w> { /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_resource_by_id(self, component_id: ComponentId) -> Option> { - // SAFETY: We have permission to access the resource of `component_id`. - let entity = unsafe { self.resource_entities() }.get(component_id)?; + let entity = component_id.entity(); let entity_cell = self.get_entity(entity).ok()?; // SAFETY: Exclusive access per preconditions unsafe { entity_cell.get_by_id(component_id) } @@ -554,8 +542,7 @@ impl<'w> UnsafeWorldCell<'w> { component_id: ComponentId, ) -> Option> { self.assert_allows_mutable_access(); - // SAFETY: We have permission to access the resource of `component_id`. - let entity = unsafe { self.resource_entities() }.get(component_id)?; + let entity = component_id.entity(); let entity_cell = self.get_entity(entity).ok()?; // SAFETY: Access permissions and uniqueness per preconditions unsafe { entity_cell.get_mut_by_id(component_id).ok() } @@ -651,8 +638,7 @@ impl<'w> UnsafeWorldCell<'w> { self, component_id: ComponentId, ) -> Option<(Ptr<'w>, ComponentTickCells<'w>)> { - // SAFETY: We have permission to access the resource of `component_id`. - let entity = unsafe { self.resource_entities() }.get(component_id)?; + let entity = component_id.entity(); let storage_type = self.components().get_info(component_id)?.storage_type(); let location = self.get_entity(entity).ok()?.location(); // SAFETY: diff --git a/crates/bevy_settings/src/lib.rs b/crates/bevy_settings/src/lib.rs index 40c9cbecadafe..1c33df725efde 100644 --- a/crates/bevy_settings/src/lib.rs +++ b/crates/bevy_settings/src/lib.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::{ change_detection::Tick, + entity::ContainsEntity, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, resource::Resource, system::{Command, Commands, Res, ResMut}, @@ -331,9 +332,7 @@ fn resources_to_toml( continue; }; - let Some(res_entity) = world.resource_entities().get(component_id) else { - continue; - }; + let res_entity = component_id.entity(); let res_entity_ref = world.entity(res_entity); let Some(reflect) = cmp.reflect(res_entity_ref) else { continue; @@ -460,7 +459,7 @@ fn apply_settings_to_world( let reflect_component = ty.data::().unwrap(); let component_id = world.components().get_id(*tid); - let res_entity = component_id.and_then(|cid| world.resource_entities().get(cid)); + let res_entity = component_id.map(|cid| cid.entity()); if let Some(res_entity) = res_entity { // Resource already exists, so apply toml properties to it. diff --git a/crates/bevy_world_serialization/src/dynamic_world.rs b/crates/bevy_world_serialization/src/dynamic_world.rs index 526357c64e1df..44de62d00d19c 100644 --- a/crates/bevy_world_serialization/src/dynamic_world.rs +++ b/crates/bevy_world_serialization/src/dynamic_world.rs @@ -2,7 +2,7 @@ use crate::{DynamicWorldBuilder, WorldAsset, WorldInstanceSpawnError}; use bevy_asset::Asset; use bevy_ecs::reflect::ReflectResource; use bevy_ecs::{ - entity::{Entity, EntityHashMap, SceneEntityMapper}, + entity::{ContainsEntity, Entity, EntityHashMap, SceneEntityMapper}, reflect::{AppTypeRegistry, ReflectComponent}, world::World, }; @@ -181,13 +181,12 @@ impl DynamicWorld { .expect("ReflectComponent is depended on ReflectResource"); let resource_id = reflect_component.register_component(world); + let entity = resource_id.entity(); - // check if the resource already exists, if not spawn it, otherwise override the value - let entity = if let Some(entity) = world.resource_entities().get(resource_id) { - entity - } else { - world.spawn_empty().id() - }; + // check if the resource already exists, if not spawn it + if !world.entities().contains_spawned(entity) { + let _ = world.spawn_empty_at(entity); + } SceneEntityMapper::world_scope(entity_map, world, |world, mapper| { reflect_component.apply_or_insert_mapped( diff --git a/crates/bevy_world_serialization/src/dynamic_world_builder.rs b/crates/bevy_world_serialization/src/dynamic_world_builder.rs index 727d7adc9fecc..608d9abe77c3d 100644 --- a/crates/bevy_world_serialization/src/dynamic_world_builder.rs +++ b/crates/bevy_world_serialization/src/dynamic_world_builder.rs @@ -5,7 +5,8 @@ use crate::{DynamicEntity, DynamicWorld, WorldFilter}; use alloc::collections::BTreeMap; use bevy_ecs::resource::IS_RESOURCE; use bevy_ecs::{ - component::{Component, ComponentId}, + component::{Component, ComponentId, ComponentInfo}, + entity::ContainsEntity, entity_disabling::DefaultQueryFilters, prelude::Entity, reflect::{ReflectComponent, ReflectResource}, @@ -377,10 +378,24 @@ impl<'w> DynamicWorldBuilder<'w> { .components() .get_valid_id(TypeId::of::()); - for (component_id, entity) in self.original_world.resource_entities().iter() { + let ids: Vec = self + .original_world + .components() + .iter_registered() + .map(ComponentInfo::id) + .collect(); + + for component_id in ids { + let entity = component_id.entity(); + + if !self.original_world.entities().contains_spawned(entity) { + continue; + } + if Some(component_id) == original_world_dqf_id { continue; } + let mut extract_and_push = || { let type_id = self .original_world diff --git a/crates/bevy_world_serialization/src/world_asset.rs b/crates/bevy_world_serialization/src/world_asset.rs index 458974e898533..fcecf98548509 100644 --- a/crates/bevy_world_serialization/src/world_asset.rs +++ b/crates/bevy_world_serialization/src/world_asset.rs @@ -5,8 +5,8 @@ use crate::{DynamicWorld, WorldInstanceSpawnError}; use bevy_asset::Asset; use bevy_ecs::resource::IS_RESOURCE; use bevy_ecs::{ - component::ComponentCloneBehavior, - entity::{Entity, EntityHashMap, SceneEntityMapper}, + component::{ComponentCloneBehavior, ComponentId, ComponentInfo}, + entity::{ContainsEntity, Entity, EntityHashMap, SceneEntityMapper}, entity_disabling::DefaultQueryFilters, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, relationship::RelationshipHookMode, @@ -74,8 +74,20 @@ impl WorldAsset { .components() .get_id(TypeId::of::()); + let ids: Vec = self + .world + .components() + .iter_registered() + .map(ComponentInfo::id) + .collect(); + // Resources archetype - for (component_id, source_entity) in self.world.resource_entities().iter() { + for component_id in ids { + let source_entity = component_id.entity(); + + if !self.world.entities().contains_spawned(source_entity) { + continue; + } if Some(component_id) == self_dqf_id { continue; } @@ -112,21 +124,29 @@ impl WorldAsset { .data::() .expect("ReflectComponent is depended on ReflectResource"); - // check if the resource already exists in the other world, if not spawn it - let destination_entity = - if let Some(entity) = world.resource_entities().get(component_id) { - entity - } else { - world.spawn_empty().id() - }; - - reflect_component.copy( - &self.world, - world, - source_entity, - destination_entity, - &type_registry, - ); + let Some(resource) = reflect_component + .reflect(self.world.entity(source_entity)) + .map(|component| clone_reflect_value(component.as_partial_reflect(), registration)) + else { + continue; + }; + + let destination_component_id = reflect_component.register_component(world); + let destination_entity = destination_component_id.entity(); + + if !world.entities().contains_spawned(destination_entity) { + let _ = world.spawn_empty_at(destination_entity); + } + + SceneEntityMapper::world_scope(entity_map, world, |world, mapper| { + reflect_component.apply_or_insert_mapped( + &mut world.entity_mut(source_entity), + resource.as_partial_reflect(), + &type_registry, + mapper, + RelationshipHookMode::Skip, + ); + }); } // Ensure that all source world entities have been allocated in the destination From 45ff0416be591a32399c79cf0453239299db2789 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 23 Jun 2026 19:20:33 +0200 Subject: [PATCH 03/44] fix world_serialization tests --- .../src/dynamic_world_builder.rs | 4 ++-- crates/bevy_world_serialization/src/serde.rs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bevy_world_serialization/src/dynamic_world_builder.rs b/crates/bevy_world_serialization/src/dynamic_world_builder.rs index 608d9abe77c3d..3fd74964085a3 100644 --- a/crates/bevy_world_serialization/src/dynamic_world_builder.rs +++ b/crates/bevy_world_serialization/src/dynamic_world_builder.rs @@ -516,8 +516,8 @@ mod tests { assert_eq!(dynamic_world.entities.len(), 1); assert_eq!(dynamic_world.entities[0].entity, entity); assert_eq!(dynamic_world.entities[0].components.len(), 2); - assert!(dynamic_world.entities[0].components[0].represents::()); - assert!(dynamic_world.entities[0].components[1].represents::()); + assert!(dynamic_world.entities[0].components[0].represents::()); + assert!(dynamic_world.entities[0].components[1].represents::()); } #[test] diff --git a/crates/bevy_world_serialization/src/serde.rs b/crates/bevy_world_serialization/src/serde.rs index 64505eda4b8ee..ab8cdde410dfb 100644 --- a/crates/bevy_world_serialization/src/serde.rs +++ b/crates/bevy_world_serialization/src/serde.rs @@ -694,25 +694,25 @@ mod tests { ), }, entities: { - 4294967290: ( + 4294967279: ( components: { "bevy_world_serialization::serde::tests::FakeMesh3d": (Uuid("00000000-0000-0000-0000-000000000001")), }, ), - 4294967291: ( + 4294967281: ( components: { "bevy_world_serialization::serde::tests::Bar": (345), "bevy_world_serialization::serde::tests::Baz": (789), "bevy_world_serialization::serde::tests::Foo": (123), }, ), - 4294967292: ( + 4294967283: ( components: { "bevy_world_serialization::serde::tests::Bar": (345), "bevy_world_serialization::serde::tests::Foo": (123), }, ), - 4294967293: ( + 4294967285: ( components: { "bevy_world_serialization::serde::tests::Foo": (123), }, @@ -920,7 +920,7 @@ mod tests { assert_eq!( vec![ - 0, 1, 253, 255, 255, 255, 15, 1, 51, 98, 101, 118, 121, 95, 119, 111, 114, 108, + 0, 1, 245, 255, 255, 255, 15, 1, 51, 98, 101, 118, 121, 95, 119, 111, 114, 108, 100, 95, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 2, 3, 102, 102, 166, 63, 205, 204, 108, 64, @@ -963,7 +963,7 @@ mod tests { assert_eq!( vec![ - 146, 128, 129, 206, 255, 255, 255, 253, 145, 129, 217, 51, 98, 101, 118, 121, 95, + 146, 128, 129, 206, 255, 255, 255, 245, 145, 129, 217, 51, 98, 101, 118, 121, 95, 119, 111, 114, 108, 100, 95, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 147, 147, 1, 2, 3, 146, 202, From 5e9d8fa1ce3cbe97cfee7829bb7df2267ef7c2f1 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 23 Jun 2026 19:34:25 +0200 Subject: [PATCH 04/44] add PR number --- _release-content/migration-guides/components-as-entities.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index ffad9b1a6dd33..e242bed56d3b5 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -1,6 +1,6 @@ --- title: "Components as Entities" -pull_requests: [ ... ] +pull_requests: [24728] --- - `ComponentId::new` now takes `Entity` as an argument instead of `usize`. From 16f2b9e8b7094d6539e0993593b1e5720846eedb Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 23 Jun 2026 23:11:50 +0200 Subject: [PATCH 05/44] fix bevy_remote --- crates/bevy_remote/src/builtin_methods.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 2f10c3397d6a0..6108ac2f4ec74 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Result as AnyhowResult}; use bevy_dev_tools::schedule_data::serde::ScheduleData; use bevy_ecs::{ component::ComponentId, - entity::Entity, + entity::{ContainsEntity, Entity}, hierarchy::ChildOf, lifecycle::RemovedComponentEntity, message::MessageCursor, @@ -2014,10 +2014,7 @@ fn get_resource_entity_pair( .components() .get_id(type_id) .ok_or(anyhow!("Resource not registered: `{}`", resource_path))?; - let entity = world - .resource_entities() - .get(component_id) - .ok_or(anyhow!("Resource entity does not exist."))?; + let entity = component_id.entity(); Ok((entity, component_id)) } From c6d167027fd5a4726206123fb8fef643ba8fc1b2 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 23 Jun 2026 23:27:01 +0200 Subject: [PATCH 06/44] fix dynamic example --- examples/ecs/dynamic.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ecs/dynamic.rs b/examples/ecs/dynamic.rs index 73c11ce7b6aaf..018d7d7d5d298 100644 --- a/examples/ecs/dynamic.rs +++ b/examples/ecs/dynamic.rs @@ -126,7 +126,7 @@ fn main() { }; component_names.insert(name.to_string(), id); component_info.insert(id, info.clone()); - println!("Component {} created with id: {}", name, id.index()); + println!("Component {} created with id: {}", name, id.entity()); }); } "s" => { @@ -251,7 +251,7 @@ fn main() { println!( "Event '{name}' registered (key: {}) with a dynamic observer", - event_component_id.index() + event_component_id.entity() ); }); From a7d5879ad01b7946c300fe5e4037c691b914d289 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 23 Jun 2026 23:37:04 +0200 Subject: [PATCH 07/44] fix doc test --- crates/bevy_ecs/src/query/access.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 787d2e7483316..c140033f17612 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -459,8 +459,8 @@ impl Access { /// assert_eq!( /// result, /// Ok(vec![ - /// ComponentAccessKind::Shared(ComponentId::from_u32(1)), /// ComponentAccessKind::Exclusive(ComponentId::from_u32(2)), + /// ComponentAccessKind::Shared(ComponentId::from_u32(1)), /// ComponentAccessKind::Archetypal(ComponentId::from_u32(3)), /// ]), /// ); From eed38d9db71b2f8a067528149a794d2bea0310c1 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Wed, 24 Jun 2026 00:20:30 +0200 Subject: [PATCH 08/44] fix bevy_settings test --- crates/bevy_settings/src/lib.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/bevy_settings/src/lib.rs b/crates/bevy_settings/src/lib.rs index 1c33df725efde..439aa1bdeeca1 100644 --- a/crates/bevy_settings/src/lib.rs +++ b/crates/bevy_settings/src/lib.rs @@ -333,11 +333,14 @@ fn resources_to_toml( }; let res_entity = component_id.entity(); + if !world.entities().contains_spawned(res_entity) { + continue; + } + let res_entity_ref = world.entity(res_entity); let Some(reflect) = cmp.reflect(res_entity_ref) else { continue; }; - let serializer = TypedReflectSerializer::new(reflect.as_partial_reflect(), types); let toml_value = if let Some(settings_key) = settings_key { @@ -459,10 +462,10 @@ fn apply_settings_to_world( let reflect_component = ty.data::().unwrap(); let component_id = world.components().get_id(*tid); - let res_entity = component_id.map(|cid| cid.entity()); - if let Some(res_entity) = res_entity { + if let Some(component_id) = component_id { // Resource already exists, so apply toml properties to it. + let res_entity = component_id.entity(); let res_entity_mut = world.entity_mut(res_entity); let Some(mut reflect) = reflect_component.reflect_mut(res_entity_mut) else { continue; @@ -484,9 +487,11 @@ fn apply_settings_to_world( } } else { // The resource does not exist, so create a default. + let component_id = reflect_component.register_component(world); + let mut res_entity = world.spawn_empty_at(component_id.entity()).unwrap(); + let reflect_default = ty.data::().unwrap(); let mut default_value = reflect_default.default(); - let mut res_entity = world.spawn_empty(); if let Some(toml) = toml && let Some(value) = toml.get(settings_group) @@ -899,6 +904,7 @@ mod tests { // Serialize to TOML let table = resources_to_toml(&world, &types, &manifest); + println!("{}", table.len()); // Create a new world and apply the TOML let mut new_world = World::new(); From a6d7ea3e9fab6a10cfb9d823dd0b25b92725ca31 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Wed, 24 Jun 2026 00:28:34 +0200 Subject: [PATCH 09/44] forgot to remove a println! --- crates/bevy_settings/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_settings/src/lib.rs b/crates/bevy_settings/src/lib.rs index 439aa1bdeeca1..b4699f52c3d98 100644 --- a/crates/bevy_settings/src/lib.rs +++ b/crates/bevy_settings/src/lib.rs @@ -904,7 +904,6 @@ mod tests { // Serialize to TOML let table = resources_to_toml(&world, &types, &manifest); - println!("{}", table.len()); // Create a new world and apply the TOML let mut new_world = World::new(); From 9056e8a4d658604d152554df575ddbb567fc9ab1 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Wed, 24 Jun 2026 01:19:04 +0200 Subject: [PATCH 10/44] fix benchmark --- benches/benches/bevy_ecs/empty_archetypes.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/benches/benches/bevy_ecs/empty_archetypes.rs b/benches/benches/bevy_ecs/empty_archetypes.rs index 4938120ae9a12..dc40bca48f501 100644 --- a/benches/benches/bevy_ecs/empty_archetypes.rs +++ b/benches/benches/bevy_ecs/empty_archetypes.rs @@ -166,7 +166,6 @@ fn empty_archetypes(criterion: &mut Criterion) { schedule.add_systems(iter); }); add_archetypes(&mut world, archetype_count); - world.clear_entities(); let mut e = world.spawn_empty(); e.insert(A::<0>(1.0)); e.insert(A::<1>(1.0)); @@ -197,7 +196,6 @@ fn empty_archetypes(criterion: &mut Criterion) { schedule.add_systems(for_each); }); add_archetypes(&mut world, archetype_count); - world.clear_entities(); let mut e = world.spawn_empty(); e.insert(A::<0>(1.0)); e.insert(A::<1>(1.0)); @@ -228,7 +226,6 @@ fn empty_archetypes(criterion: &mut Criterion) { schedule.add_systems(par_for_each); }); add_archetypes(&mut world, archetype_count); - world.clear_entities(); let mut e = world.spawn_empty(); e.insert(A::<0>(1.0)); e.insert(A::<1>(1.0)); From 63cd08984dea69bd8959c5a4ca48966746f94724 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 25 Jun 2026 15:07:39 +0200 Subject: [PATCH 11/44] improved migration guide, added iter_registered_ids, addressed review --- .../components-as-entities.md | 19 +++++-------------- crates/bevy_ecs/src/component/info.rs | 5 +++++ crates/bevy_ecs/src/world/mod.rs | 8 ++------ .../src/dynamic_world_builder.rs | 6 ++---- .../src/world_asset.rs | 10 ++-------- 5 files changed, 16 insertions(+), 32 deletions(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index e242bed56d3b5..537a8b0e9b01b 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -3,18 +3,9 @@ title: "Components as Entities" pull_requests: [24728] --- -- `ComponentId::new` now takes `Entity` as an argument instead of `usize`. -- `ComponentId::index` was removed. -- `ComponentId::from_u32` was added. -- `ComponentId` now implements `ContainsEntity` so the entity can be gotten through `ComponentId::entity`. +- `ComponentId::new` now takes `Entity` as an argument instead of `usize`. For debugging, you can use `ComponentId::index`. +- `ComponentId::index` was removed in favor of implementing `ContainsEntity`, call `ComponentId::entity` to get the underlying entity. - `ComponentIdSet` is now an `EntityEquivalentHashSet` instead of a `FixedBitSet`. This means that methods like `union_with` no longer work, use `bitor_assign` instead. -- `ComponentIds` has been removed. -- `ComponentsRegistrator::new` now takes `EntityAllocator` instead of `ComponentIds`. -- `ComponentsQueuedRegistrator::new` not takes `RemoteAllocator` instead of `ComponentIds`. -- `Access` no longer derives `Hash`. -- `EcsAccessType` no longer derives `Hash`. -- Removed `SparseArray::iter`. Not user-facing but worth mentioning. -- Removed `EntityWorldMut::resource_entities`. -- Removed `World::resource_entities`. -- Removed `UnsafeWorldCell::resource_entities`. -- Removed `ResourceEntities`. +- `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while` ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. +- `Access` and `EcsAccessType` no longer derive `Hash`. +- `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`. diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 9d3d2952e0634..765d2de31c603 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -670,6 +670,11 @@ impl Components { self.components.values() } + /// Gets an iterator over all `ComponentId`s fully registered with this instance. + pub fn iter_registered_ids(&self) -> impl Iterator + '_ { + self.components.keys().copied() + } + pub(crate) fn get_relationship_accessor_mut( &mut self, component_id: ComponentId, diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index d157457ce58e3..1e76f0d4b149b 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -3317,15 +3317,11 @@ impl World { /// This can easily cause systems expecting certain resources to immediately start panicking. /// Use with caution. pub fn clear_resources(&mut self) { - let ids: Vec = self - .components() - .iter_registered() - .map(ComponentInfo::id) - .collect(); + let ids: Vec = self.components().iter_registered_ids().collect(); for component_id in ids { let entity = component_id.entity(); if self.entities().contains_spawned(entity) { - // only resource entities should have a component where the component_id matches the entity. + // only resource entities with a matching component_id should have a component. self.entity_mut(entity).remove_by_id(component_id); } } diff --git a/crates/bevy_world_serialization/src/dynamic_world_builder.rs b/crates/bevy_world_serialization/src/dynamic_world_builder.rs index 3fd74964085a3..76d2650f24f71 100644 --- a/crates/bevy_world_serialization/src/dynamic_world_builder.rs +++ b/crates/bevy_world_serialization/src/dynamic_world_builder.rs @@ -5,7 +5,7 @@ use crate::{DynamicEntity, DynamicWorld, WorldFilter}; use alloc::collections::BTreeMap; use bevy_ecs::resource::IS_RESOURCE; use bevy_ecs::{ - component::{Component, ComponentId, ComponentInfo}, + component::{Component, ComponentId}, entity::ContainsEntity, entity_disabling::DefaultQueryFilters, prelude::Entity, @@ -381,10 +381,8 @@ impl<'w> DynamicWorldBuilder<'w> { let ids: Vec = self .original_world .components() - .iter_registered() - .map(ComponentInfo::id) + .iter_registered_ids() .collect(); - for component_id in ids { let entity = component_id.entity(); diff --git a/crates/bevy_world_serialization/src/world_asset.rs b/crates/bevy_world_serialization/src/world_asset.rs index fcecf98548509..57e03ee393fb6 100644 --- a/crates/bevy_world_serialization/src/world_asset.rs +++ b/crates/bevy_world_serialization/src/world_asset.rs @@ -5,7 +5,7 @@ use crate::{DynamicWorld, WorldInstanceSpawnError}; use bevy_asset::Asset; use bevy_ecs::resource::IS_RESOURCE; use bevy_ecs::{ - component::{ComponentCloneBehavior, ComponentId, ComponentInfo}, + component::{ComponentCloneBehavior, ComponentId}, entity::{ContainsEntity, Entity, EntityHashMap, SceneEntityMapper}, entity_disabling::DefaultQueryFilters, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, @@ -74,13 +74,7 @@ impl WorldAsset { .components() .get_id(TypeId::of::()); - let ids: Vec = self - .world - .components() - .iter_registered() - .map(ComponentInfo::id) - .collect(); - + let ids: Vec = self.world.components().iter_registered_ids().collect(); // Resources archetype for component_id in ids { let source_entity = component_id.entity(); From 50ec23353ad417bdffdb5c517de5e2b7cffefc30 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 25 Jun 2026 15:13:21 +0200 Subject: [PATCH 12/44] markdown error --- _release-content/migration-guides/components-as-entities.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index 537a8b0e9b01b..eb80a5dcbb289 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -6,6 +6,6 @@ pull_requests: [24728] - `ComponentId::new` now takes `Entity` as an argument instead of `usize`. For debugging, you can use `ComponentId::index`. - `ComponentId::index` was removed in favor of implementing `ContainsEntity`, call `ComponentId::entity` to get the underlying entity. - `ComponentIdSet` is now an `EntityEquivalentHashSet` instead of a `FixedBitSet`. This means that methods like `union_with` no longer work, use `bitor_assign` instead. -- `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while` ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. +- `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while `ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. - `Access` and `EcsAccessType` no longer derive `Hash`. - `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`. From 731b5e6e53d572d6e779eb5fbad6f4f2113caf58 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 28 Jun 2026 00:05:12 +0200 Subject: [PATCH 13/44] addressed most of chescocks review --- .../components-as-entities.md | 2 +- crates/bevy_ecs/src/entity/hash_set.rs | 17 ++++ crates/bevy_ecs/src/query/access.rs | 43 +++++---- crates/bevy_ecs/src/world/mod.rs | 89 ++++++++++--------- crates/bevy_ecs/src/world/reflect.rs | 2 +- crates/bevy_settings/src/lib.rs | 4 +- .../src/dynamic_world_builder.rs | 7 +- 7 files changed, 88 insertions(+), 76 deletions(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index eb80a5dcbb289..45f94ee9c09a6 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -3,7 +3,7 @@ title: "Components as Entities" pull_requests: [24728] --- -- `ComponentId::new` now takes `Entity` as an argument instead of `usize`. For debugging, you can use `ComponentId::index`. +- `ComponentId::new` now takes `Entity` as an argument instead of `usize`. For debugging, you can use `ComponentId::from_u32`. - `ComponentId::index` was removed in favor of implementing `ContainsEntity`, call `ComponentId::entity` to get the underlying entity. - `ComponentIdSet` is now an `EntityEquivalentHashSet` instead of a `FixedBitSet`. This means that methods like `union_with` no longer work, use `bitor_assign` instead. - `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while `ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. diff --git a/crates/bevy_ecs/src/entity/hash_set.rs b/crates/bevy_ecs/src/entity/hash_set.rs index aa0ed49f03023..342f9ed20992b 100644 --- a/crates/bevy_ecs/src/entity/hash_set.rs +++ b/crates/bevy_ecs/src/entity/hash_set.rs @@ -88,6 +88,23 @@ impl EntityEquivalentHashSet { } } +impl EntityEquivalentHashSet { + /// In-place union of two `EntityEquivalentHashSet`s.. + pub fn union_with(&mut self, rhs: &Self) { + self.0.bitor_assign(&rhs.0); + } + + /// In-place intersection of two `EntityEquivalentHashSet`s. + pub fn intersect_with(&mut self, rhs: &Self) { + self.0.bitand_assign(&rhs.0); + } + + /// In-place difference of two `EntityEquivalentHashSet`s. + pub fn difference_with(&mut self, rhs: &Self) { + self.0.sub_assign(&rhs.0); + } +} + impl Deref for EntityEquivalentHashSet { type Target = HashSet; diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index c140033f17612..57442bc6b77e6 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -5,7 +5,6 @@ use crate::{ }; use alloc::{format, string::String, vec, vec::Vec}; use core::fmt::Debug; -use core::ops::{BitAnd, BitAndAssign, BitOrAssign, Sub, SubAssign}; use derive_more::From; use thiserror::Error; @@ -239,7 +238,7 @@ impl Access { &other.writes, other.writes_inverted, ); - self.archetypal.bitor_assign(&other.archetypal); + self.archetypal.union_with(&other.archetypal); } /// Removes any access from `self` that would conflict with `other`. @@ -387,11 +386,11 @@ impl Access { let temp_conflicts: ComponentIdSet = match (lhs_writes_inverted, rhs_reads_and_writes_inverted) { (true, true) => return AccessConflicts::All, - (false, true) => lhs_writes.sub(rhs_reads_and_writes), - (true, false) => rhs_reads_and_writes.sub(lhs_writes), - (false, false) => lhs_writes.bitand(rhs_reads_and_writes), + (false, true) => lhs_writes - rhs_reads_and_writes, + (true, false) => rhs_reads_and_writes - lhs_writes, + (false, false) => lhs_writes & rhs_reads_and_writes, }; - conflicts.bitor_assign(&temp_conflicts); + conflicts.union_with(&temp_conflicts); } AccessConflicts::Individual(conflicts) @@ -500,13 +499,13 @@ fn invertible_union_with( other_inverted: bool, ) { match (*self_inverted, other_inverted) { - (true, true) => self_set.bitand_assign(other_set), - (true, false) => self_set.sub_assign(other_set), + (true, true) => self_set.intersect_with(other_set), + (true, false) => self_set.difference_with(other_set), (false, true) => { *self_inverted = true; - *self_set = other_set.clone().sub(self_set); + *self_set = other_set - self_set; } - (false, false) => self_set.bitor_assign(other_set), + (false, false) => self_set.union_with(other_set), } } @@ -804,14 +803,14 @@ impl FilteredAccess { /// `Or<((With, With), (With, Without), (Without, With), (Without, Without))>`. pub fn extend(&mut self, other: &FilteredAccess) { self.access.extend(&other.access); - self.required.bitor_assign(&other.required); + self.required.union_with(&other.required); // We can avoid allocating a new array of bitsets if `other` contains just a single set of filters: // in this case we can short-circuit by performing an in-place union for each bitset. if other.filter_sets.len() == 1 { for filter in &mut self.filter_sets { - filter.with.bitor_assign(&other.filter_sets[0].with); - filter.without.bitor_assign(&other.filter_sets[0].without); + filter.with.union_with(&other.filter_sets[0].with); + filter.without.union_with(&other.filter_sets[0].without); } return; } @@ -820,8 +819,8 @@ impl FilteredAccess { for filter in &self.filter_sets { for other_filter in &other.filter_sets { let mut new_filter = filter.clone(); - new_filter.with.bitor_assign(&other_filter.with); - new_filter.without.bitor_assign(&other_filter.without); + new_filter.with.union_with(&other_filter.with); + new_filter.without.union_with(&other_filter.without); new_filters.push(new_filter); } } @@ -1116,8 +1115,6 @@ impl FilteredAccessSet { #[cfg(test)] mod tests { - use core::ops::{BitAndAssign, BitOrAssign, SubAssign}; - use super::{invertible_difference_with, invertible_union_with}; use crate::{ component::{ComponentId, ComponentIdSet}, @@ -1658,27 +1655,27 @@ mod tests { let set_23 = set_from_u32s(vec![2, 3]); let mut s = set_13.clone(); - s.bitor_assign(&set_23); + s.union_with(&set_23); assert_eq!(s, set_from_u32s(vec![1, 2, 3])); let mut s = set_23.clone(); - s.bitor_assign(&set_13); + s.union_with(&set_13); assert_eq!(s, set_from_u32s(vec![1, 2, 3])); let mut s = set_13.clone(); - s.bitand_assign(&set_23); + s.intersect_with(&set_23); assert_eq!(s, set_from_u32s(vec![3])); let mut s = set_23.clone(); - s.bitand_assign(&set_13); + s.intersect_with(&set_13); assert_eq!(s, set_from_u32s(vec![3])); let mut s = set_13.clone(); - s.sub_assign(&set_23); + s.difference_with(&set_23); assert_eq!(s, set_from_u32s(vec![1])); let mut s = set_23.clone(); - s.sub_assign(&set_13); + s.difference_with(&set_13); assert_eq!(s, set_from_u32s(vec![2])); } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 1e76f0d4b149b..a2436f68ecf0b 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1940,7 +1940,9 @@ impl World { let resource = func(self); move_as_ptr!(resource); - let entity_mut = self.spawn_at_with_caller(entity, resource, caller).unwrap(); + // cannot panic because self.get_entity(entity) was None (entity hasn't been spawned) + // and self.register_component ensures that the entity is not invalid. + let entity_mut = self.spawn_at_unchecked(entity, resource, caller); (resource_id, entity_mut) } @@ -2988,7 +2990,7 @@ impl World { let mut entity_mut = if self.entities().contains_spawned(entity) { self.entity_mut(entity) } else { - self.spawn_empty_at(entity).unwrap() + self.spawn_empty_at_unchecked(entity, caller) }; // SAFETY: pointer valid for this component id per precondition unsafe { @@ -3522,18 +3524,15 @@ impl World { /// ``` #[inline] pub fn iter_resources(&self) -> impl Iterator)> { - let ids: Vec = self - .components() + self.components() .iter_registered() - .map(ComponentInfo::id) - .collect(); - ids.into_iter().filter_map(|component_id| { - let entity = component_id.entity(); - let component_info = self.components().get_info(component_id)?; - let entity_cell = self.get_entity(entity).ok()?; - let resource = entity_cell.get_by_id(component_id).ok()?; - Some((component_info, resource)) - }) + .filter_map(|component_info| { + let component_id = component_info.id(); + let entity = component_id.entity(); + let entity_cell = self.get_entity(entity).ok()?; + let resource = entity_cell.get_by_id(component_id).ok()?; + Some((component_info, resource)) + }) } /// Mutably iterates over all resources in the world. @@ -3602,32 +3601,25 @@ impl World { /// # assert_eq!(world.resource::().0, 3); /// ``` pub fn iter_resources_mut(&mut self) -> impl Iterator)> { - let ids: Vec = self - .components() - .iter_registered() - .map(ComponentInfo::id) - .collect(); - let unsafe_world = self.as_unsafe_world_cell(); - let components = unsafe_world.components(); - - ids.into_iter().filter_map(move |component_id| { - // SAFETY: If a resource has been initialized, a corresponding ComponentInfo must exist with its ID. - let component_info = - unsafe { components.get_info(component_id).debug_checked_unwrap() }; - - let entity_cell = unsafe_world.get_entity(component_id.entity()).ok()?; - // SAFETY: - // - We have exclusive world access - // - `UnsafeEntityCell::get_mut_by_id` doesn't access components - // or resource_entities mutably - // - `resource_entities` doesn't contain duplicate entities, so - // no duplicate references are created - let mut_untyped = unsafe { entity_cell.get_mut_by_id(component_id).ok()? }; - - Some((component_info, mut_untyped)) - }) + unsafe_world + .components() + .iter_registered() + .filter_map(move |component_info| { + let component_id = component_info.id(); + let entity_cell = unsafe_world.get_entity(component_id.entity()).ok()?; + + // SAFETY: + // - We have exclusive world access + // - `UnsafeEntityCell::get_mut_by_id` doesn't access components + // or resource_entities mutably + // - `resource_entities` doesn't contain duplicate entities, so + // no duplicate references are created + let mut_untyped = unsafe { entity_cell.get_mut_by_id(component_id).ok()? }; + + Some((component_info, mut_untyped)) + }) } /// Gets a pointer to `!Send` data with the id [`ComponentId`] if it exists. @@ -3956,7 +3948,7 @@ mod tests { prelude::{DetectChanges, Event, Mut, On, Res}, ptr::OwningPtr, resource::Resource, - world::{error::EntityMutableFetchError, DeferredWorld}, + world::{error::EntityMutableFetchError, DeferredWorld, MutUntyped}, }; use alloc::{ borrow::ToOwned, @@ -3967,6 +3959,7 @@ mod tests { }; use bevy_ecs_macros::Component; use bevy_platform::collections::{HashMap, HashSet}; + use bevy_ptr::Ptr; use bevy_utils::prelude::DebugName; use core::{ any::TypeId, @@ -4148,9 +4141,14 @@ mod tests { world.insert_resource(TestResource3); world.remove_resource::(); - let mut iter = world.iter_resources(); + let mut resources = world + .iter_resources() + .collect::)>>(); + resources.sort_by(|a, b| a.0.id().cmp(&b.0.id())); - let (info, ptr) = iter.next().unwrap(); + assert_eq!(resources.len(), 2); + + let (info, ptr) = resources[0]; assert_eq!(info.name(), DebugName::type_name::()); assert_eq!( // SAFETY: We know that the resource is of type `TestResource2` @@ -4158,12 +4156,10 @@ mod tests { &"Hello, world!".to_string() ); - let (info, ptr) = iter.next().unwrap(); + let (info, ptr) = resources[1]; assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource` assert_eq!(unsafe { ptr.deref::().0 }, 42); - - assert!(iter.next().is_none()); } #[test] @@ -4176,7 +4172,12 @@ mod tests { world.insert_resource(TestResource3); world.remove_resource::(); - let mut iter = world.iter_resources_mut(); + let mut resources = world + .iter_resources_mut() + .collect::)>>(); + resources.sort_by(|a, b| a.0.id().cmp(&b.0.id())); + + let mut iter = resources.into_iter(); let (info, mut mut_untyped) = iter.next().unwrap(); assert_eq!(info.name(), DebugName::type_name::()); diff --git a/crates/bevy_ecs/src/world/reflect.rs b/crates/bevy_ecs/src/world/reflect.rs index 2dd24a3c68e6b..b7c5dfee15b3e 100644 --- a/crates/bevy_ecs/src/world/reflect.rs +++ b/crates/bevy_ecs/src/world/reflect.rs @@ -204,7 +204,7 @@ impl World { self.entity_mut(entity).insert_reflect(reflected_resource); } else { self.spawn_empty_at(entity) - .unwrap() + .expect("entity isn't already spawned") .insert_reflect(reflected_resource); } } diff --git a/crates/bevy_settings/src/lib.rs b/crates/bevy_settings/src/lib.rs index b4699f52c3d98..b2c158f6f52c4 100644 --- a/crates/bevy_settings/src/lib.rs +++ b/crates/bevy_settings/src/lib.rs @@ -488,7 +488,9 @@ fn apply_settings_to_world( } else { // The resource does not exist, so create a default. let component_id = reflect_component.register_component(world); - let mut res_entity = world.spawn_empty_at(component_id.entity()).unwrap(); + let mut res_entity = world + .spawn_empty_at(component_id.entity()) + .expect("entity was just allocated"); let reflect_default = ty.data::().unwrap(); let mut default_value = reflect_default.default(); diff --git a/crates/bevy_world_serialization/src/dynamic_world_builder.rs b/crates/bevy_world_serialization/src/dynamic_world_builder.rs index 76d2650f24f71..d53387289fbd4 100644 --- a/crates/bevy_world_serialization/src/dynamic_world_builder.rs +++ b/crates/bevy_world_serialization/src/dynamic_world_builder.rs @@ -378,12 +378,7 @@ impl<'w> DynamicWorldBuilder<'w> { .components() .get_valid_id(TypeId::of::()); - let ids: Vec = self - .original_world - .components() - .iter_registered_ids() - .collect(); - for component_id in ids { + for component_id in self.original_world.components().iter_registered_ids() { let entity = component_id.entity(); if !self.original_world.entities().contains_spawned(entity) { From 05c6204972287ca890db27acba39c553fc254b30 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 28 Jun 2026 00:37:45 +0200 Subject: [PATCH 14/44] clippy --- crates/bevy_ecs/src/world/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index a2436f68ecf0b..cd92f9e79563c 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -4144,7 +4144,7 @@ mod tests { let mut resources = world .iter_resources() .collect::)>>(); - resources.sort_by(|a, b| a.0.id().cmp(&b.0.id())); + resources.sort_by_key(|a| a.0.id()); assert_eq!(resources.len(), 2); @@ -4175,7 +4175,7 @@ mod tests { let mut resources = world .iter_resources_mut() .collect::)>>(); - resources.sort_by(|a, b| a.0.id().cmp(&b.0.id())); + resources.sort_by_key(|a| a.0.id()); let mut iter = resources.into_iter(); From cc02f45826caa4bb13baae41fde49ece74f7ab11 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 28 Jun 2026 21:08:26 +0200 Subject: [PATCH 15/44] address review pt 2 --- .../components-as-entities.md | 8 +++++ benches/benches/bevy_ecs/empty_archetypes.rs | 1 + crates/bevy_ecs/src/component/info.rs | 19 ++++------- crates/bevy_ecs/src/component/register.rs | 19 +++-------- crates/bevy_ecs/src/resource.rs | 34 +++++++++++++++---- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index 45f94ee9c09a6..fc362cc9f78d1 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -9,3 +9,11 @@ pull_requests: [24728] - `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while `ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. - `Access` and `EcsAccessType` no longer derive `Hash`. - `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`. +- Despawning a resource entity has been upgraded from a `warn!` to a `panic!`, moreover, removing `IsResource` from a resource entity also panics. + +In 0.19, you could attach components to a resource by simply calling `world.spawn((Res1, Comp1, Comp2))`. In 0.20, this no longer works as `Res1` needs to be on the resource entity allocated by `world.register_component()`. In 0.20, adding components looks as follows: + +```rust +let entity = world.register_component::().entity(); +world.spawn_at(entity, (Res1, Comp1, Comp2)); +``` diff --git a/benches/benches/bevy_ecs/empty_archetypes.rs b/benches/benches/bevy_ecs/empty_archetypes.rs index dc40bca48f501..1bd4a6cccf3f6 100644 --- a/benches/benches/bevy_ecs/empty_archetypes.rs +++ b/benches/benches/bevy_ecs/empty_archetypes.rs @@ -156,6 +156,7 @@ fn add_archetypes(world: &mut World, count: u16) { if i & (1 << 15) != 0 { e.insert(A::<28>(1.0)); } + e.despawn(); } } diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 765d2de31c603..def3f5e9d7b1b 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -387,21 +387,18 @@ pub struct Components { impl Components { /// This registers any descriptor, component or resource. /// - /// # Safety - /// - /// The id must have never been registered before. This must be a fresh registration. + /// This function panics if the id has never been registered before. #[inline] - pub(super) unsafe fn register_component_inner( + pub(super) fn register_component_inner( &mut self, id: ComponentId, mut descriptor: ComponentDescriptor, ) { descriptor.initialize(id, self); let info = ComponentInfo::new(id, descriptor); - // SAFETY: The id has never been registered before. - unsafe { - self.components.insert_unique_unchecked(id, info); - } + self.components + .try_insert(id, info) + .expect("this component has already been registered"); } /// Returns the number of components registered or queued with this instance. @@ -648,7 +645,6 @@ impl Components { /// # Safety /// /// The [`ComponentDescriptor`] must match the [`TypeId`]. - /// The [`ComponentId`] must be unique. /// The [`TypeId`] and [`ComponentId`] must not be registered or queued. #[inline] pub(super) unsafe fn register_non_send_unchecked( @@ -657,10 +653,7 @@ impl Components { component_id: ComponentId, descriptor: ComponentDescriptor, ) { - // SAFETY: ensured by caller - unsafe { - self.register_component_inner(component_id, descriptor); - } + self.register_component_inner(component_id, descriptor); let prev = self.indices.insert(type_id, component_id); debug_assert!(prev.is_none()); } diff --git a/crates/bevy_ecs/src/component/register.rs b/crates/bevy_ecs/src/component/register.rs index 74fa6213aedb0..8137b67cf3f1c 100644 --- a/crates/bevy_ecs/src/component/register.rs +++ b/crates/bevy_ecs/src/component/register.rs @@ -184,10 +184,7 @@ impl<'w> ComponentsRegistrator<'w> { register_required_components: fn(ComponentId, &mut RequiredComponentsRegistrator), update_from_component: fn(&mut ComponentHooks) -> &mut ComponentHooks, ) { - // SAFETY: ensured by caller. - unsafe { - self.components.register_component_inner(id, descriptor); - } + self.components.register_component_inner(id, descriptor); let prev = self.components.indices.insert(type_id, id); debug_assert!(prev.is_none()); @@ -244,10 +241,7 @@ impl<'w> ComponentsRegistrator<'w> { descriptor: ComponentDescriptor, ) -> ComponentId { let id = ComponentId::new(self.allocator.alloc()); - // SAFETY: The id is fresh. - unsafe { - self.components.register_component_inner(id, descriptor); - } + self.components.register_component_inner(id, descriptor); id } @@ -514,12 +508,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> { descriptor: ComponentDescriptor, ) -> ComponentId { self.register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| { - // SAFETY: Id uniqueness handled by caller. - unsafe { - registrator - .components - .register_component_inner(id, descriptor); - } + registrator + .components + .register_component_inner(id, descriptor); }) } diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 5a32063d00b04..15fe484217ba8 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -141,7 +141,7 @@ impl IsResource { } } - pub(crate) fn on_discard(mut world: DeferredWorld, context: HookContext) { + pub(crate) fn on_discard(world: DeferredWorld, context: HookContext) { let resource_component_id = world .entity(context.entity) .get::() @@ -151,15 +151,12 @@ impl IsResource { let original_entity = resource_component_id.entity(); if original_entity == context.entity { - world - .commands() - .entity(context.entity) - .remove_by_id(resource_component_id); + panic!("IsResource components should never be removed from their resource entity.") } } pub(crate) fn on_despawn(_world: DeferredWorld, _context: HookContext) { - warn!("Resource entities are not supposed to be despawned."); + panic!("Resource entities are not supposed to be despawned."); } } @@ -325,4 +322,29 @@ mod tests { 1 ); } + + #[test] + #[should_panic] + fn remove_resource_marker_should_panic() { + #[derive(Resource, Default)] + struct R; + + let mut world = World::new(); + world.init_resource::(); + let entity = world.register_component::().entity(); + let mut entity = world.entity_mut(entity); + entity.remove::(); + } + + #[test] + #[should_panic] + fn despawn_resource_should_panic() { + #[derive(Resource, Default)] + struct R; + + let mut world = World::new(); + world.init_resource::(); + let entity = world.register_component::().entity(); + world.despawn(entity); + } } From 958d2bb3e464af9b2a67fd91a9238666020f7501 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 28 Jun 2026 21:14:57 +0200 Subject: [PATCH 16/44] fix test --- crates/bevy_ecs/src/resource.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 15fe484217ba8..4a985510e5b98 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -250,15 +250,7 @@ mod tests { entity }; - // Removing IsResource should invalidate the current TestResource entity - // This uses commands because IsResource's despawn-on-removal invalidates the EntityWorldMut and panics - world.entity_mut(first_entity).remove::(); - assert!(world.get_resource::().is_none()); - - assert!( - !world.entity(first_entity).contains::(), - "Removing IsResource should also remove the Resource component it corresponds to" - ); + world.remove_resource::(); world.init_resource::(); let second_entity = { From 50af152b1d47ea038ca6d826f8e73d68750951a6 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 30 Jun 2026 02:17:18 +0200 Subject: [PATCH 17/44] address UB concerns --- crates/bevy_ecs/src/system/system_param.rs | 18 +++++++++++-- .../bevy_ecs/src/world/filtered_resource.rs | 25 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index fb8da35c900e5..76102e058196c 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -674,7 +674,14 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { type Item<'w, 's> = Res<'w, T>; fn init_state(world: &mut World) -> Self::State { - world.components_registrator().register_component::() + let component_id = world.components_registrator().register_component::(); + assert!( + !world + .get_required_components_by_id(component_id) + .is_none_or(|required| !required.direct.contains_key(&IS_RESOURCE)), + "resource does not have IsResource as a required component" + ); + component_id } fn init_access( @@ -731,7 +738,14 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> type Item<'w, 's> = ResMut<'w, T>; fn init_state(world: &mut World) -> Self::State { - world.components_registrator().register_component::() + let component_id = world.components_registrator().register_component::(); + assert!( + !world + .get_required_components_by_id(component_id) + .is_none_or(|required| !required.direct.contains_key(&IS_RESOURCE)), + "resource does not have IsResource as a required component" + ); + component_id } fn init_access( diff --git a/crates/bevy_ecs/src/world/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs index d6507204dbe73..eab378017c457 100644 --- a/crates/bevy_ecs/src/world/filtered_resource.rs +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -3,6 +3,7 @@ use crate::{ component::ComponentId, query::Access, resource::Resource, + resource::IS_RESOURCE, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; use bevy_ptr::Ptr; @@ -179,6 +180,18 @@ 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, ResourceFetchError> { + assert!( + // SAFETY: We only access required components + unsafe { + !self + .world + .world_metadata() + .get_required_components_by_id(component_id) + .is_none_or(|required| !required.direct.contains_key(&IS_RESOURCE)) + }, + "resource does not have IsResource as a required component" + ); + if !self.access.has_read(component_id) { return Err(ResourceFetchError::NoResourceAccess(component_id)); } @@ -464,6 +477,18 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { /// # Safety /// It is the callers responsibility to ensure that there are no conflicting borrows of anything in `access` for the duration of the returned value. unsafe fn get_mut_unchecked(&mut self) -> Result, ResourceFetchError> { + assert!( + // SAFETY: We only access required components + unsafe { + !self + .world + .world_metadata() + .get_required_components_by_id(component_id) + .is_none_or(|required| !required.direct.contains_key(&IS_RESOURCE)) + }, + "resource does not have IsResource as a required component" + ); + let component_id = self .world .components() From 3f3d76ac57f7bec15767926c594ccaf3773f5bb8 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 30 Jun 2026 02:46:53 +0200 Subject: [PATCH 18/44] I'm a big dumb idiot who should test their code --- crates/bevy_ecs/src/system/system_param.rs | 20 +++++++++++++++++++ .../bevy_ecs/src/world/filtered_resource.rs | 11 +++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 76102e058196c..cf4286e9f541c 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -2758,6 +2758,7 @@ impl Display for SystemParamValidationError { #[cfg(test)] mod tests { use super::*; + use crate::component::Component; use crate::query::Without; use crate::resource::IsResource; use crate::system::assert_is_system; @@ -3075,4 +3076,23 @@ mod tests { fn message_system(_: MessageReader) {} } + + #[test] + #[should_panic] + fn missing_resource_marker() { + #[derive(Component, Default)] + struct R; + // In order to prevent UB, one should always have `IsResource` by a required component + // for every type `R` that implements Resource, else using `Res` and `ResMut` panics. + impl Resource for R {} + + let mut world = World::new(); + world.init_resource::(); + + world + .run_system_cached( + |_: Option>, _: Option>>| {}, + ) + .unwrap(); + } } diff --git a/crates/bevy_ecs/src/world/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs index eab378017c457..6858237b8051c 100644 --- a/crates/bevy_ecs/src/world/filtered_resource.rs +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -477,6 +477,12 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { /// # Safety /// It is the callers responsibility to ensure that there are no conflicting borrows of anything in `access` for the duration of the returned value. unsafe fn get_mut_unchecked(&mut self) -> Result, ResourceFetchError> { + let component_id = self + .world + .components() + .valid_component_id::() + .ok_or(ResourceFetchError::NotRegistered)?; + assert!( // SAFETY: We only access required components unsafe { @@ -489,11 +495,6 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { "resource does not have IsResource as a required component" ); - let component_id = self - .world - .components() - .valid_component_id::() - .ok_or(ResourceFetchError::NotRegistered)?; // SAFETY: THe caller ensures that there are no conflicting borrows. unsafe { self.get_mut_by_id_unchecked(component_id) } // SAFETY: The underlying type of the resource is `R`. From de652382a6249026ac47ac1f6ae08fd715282240 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 30 Jun 2026 02:53:53 +0200 Subject: [PATCH 19/44] clippy --- crates/bevy_ecs/src/system/system_param.rs | 8 ++++---- crates/bevy_ecs/src/world/filtered_resource.rs | 10 ++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index cf4286e9f541c..1877127536bfc 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -676,9 +676,9 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { fn init_state(world: &mut World) -> Self::State { let component_id = world.components_registrator().register_component::(); assert!( - !world + world .get_required_components_by_id(component_id) - .is_none_or(|required| !required.direct.contains_key(&IS_RESOURCE)), + .is_some_and(|required| required.direct.contains_key(&IS_RESOURCE)), "resource does not have IsResource as a required component" ); component_id @@ -740,9 +740,9 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> fn init_state(world: &mut World) -> Self::State { let component_id = world.components_registrator().register_component::(); assert!( - !world + world .get_required_components_by_id(component_id) - .is_none_or(|required| !required.direct.contains_key(&IS_RESOURCE)), + .is_some_and(|required| required.direct.contains_key(&IS_RESOURCE)), "resource does not have IsResource as a required component" ); component_id diff --git a/crates/bevy_ecs/src/world/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs index 6858237b8051c..33b018efc1426 100644 --- a/crates/bevy_ecs/src/world/filtered_resource.rs +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -183,11 +183,10 @@ impl<'w, 's> FilteredResources<'w, 's> { assert!( // SAFETY: We only access required components unsafe { - !self - .world + self.world .world_metadata() .get_required_components_by_id(component_id) - .is_none_or(|required| !required.direct.contains_key(&IS_RESOURCE)) + .is_some_and(|required| required.direct.contains_key(&IS_RESOURCE)) }, "resource does not have IsResource as a required component" ); @@ -486,11 +485,10 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { assert!( // SAFETY: We only access required components unsafe { - !self - .world + self.world .world_metadata() .get_required_components_by_id(component_id) - .is_none_or(|required| !required.direct.contains_key(&IS_RESOURCE)) + .is_some_and(|required| required.direct.contains_key(&IS_RESOURCE)) }, "resource does not have IsResource as a required component" ); From b02a412e5e13d4cf5c30aa67e38b6329edeac8eb Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 30 Jun 2026 02:58:09 +0200 Subject: [PATCH 20/44] migration guide --- .../migration-guides/components-as-entities.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index fc362cc9f78d1..2778f4481eed9 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -17,3 +17,14 @@ In 0.19, you could attach components to a resource by simply calling `world.spaw let entity = world.register_component::().entity(); world.spawn_at(entity, (Res1, Comp1, Comp2)); ``` + +Additionally, manually implementing `Resource` through + +```rust +#[derive(Component, Default)] +struct R; + +impl Resource for R {} +``` + +has become less viable, as now `Res` and `ResMut` panic when `IsResource` has not been made a required component for a resource. From c2678eb0cfdefa55f3e8e2300820c1f4ba3a8c25 Mon Sep 17 00:00:00 2001 From: Trashtalk217 Date: Thu, 2 Jul 2026 01:32:25 +0200 Subject: [PATCH 21/44] Apply suggestions from code review Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com> --- crates/bevy_ecs/src/resource.rs | 6 +++--- crates/bevy_ecs/src/world/mod.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 4a985510e5b98..8fbb676e70b08 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -135,9 +135,9 @@ impl IsResource { .components() .get_name(resource_component_id) .expect("resource is registered"); - warn!("Tried inserting the resource {} while one already exists. \ - Resources are unique components stored on a single entity. \ - Inserting on a different entity, when one already exists, causes the new value to be removed.", name); + warn!("Tried inserting the resource {} on the wrong entity. \ + Resources are unique components stored on the entity matching their `component_id`. \ + Inserting on a different entity causes the new value to be removed.", name); } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index cd92f9e79563c..a8f78b15f01d3 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -3322,9 +3322,9 @@ impl World { let ids: Vec = self.components().iter_registered_ids().collect(); for component_id in ids { let entity = component_id.entity(); - if self.entities().contains_spawned(entity) { + if let Ok(entity) = self.get_entity_mut(entity) { // only resource entities with a matching component_id should have a component. - self.entity_mut(entity).remove_by_id(component_id); + entity.remove_by_id(component_id); } } } From 56009dc7cc4fc0f4340430646dbcc648836e1dec Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 2 Jul 2026 02:11:04 +0200 Subject: [PATCH 22/44] removed tests, changed bevy_settings --- .../components-as-entities.md | 1 + crates/bevy_ecs/src/query/access.rs | 147 ------------------ crates/bevy_ecs/src/world/mod.rs | 2 +- crates/bevy_settings/src/lib.rs | 7 +- 4 files changed, 5 insertions(+), 152 deletions(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index 2778f4481eed9..23b362ca301fb 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -28,3 +28,4 @@ impl Resource for R {} ``` has become less viable, as now `Res` and `ResMut` panic when `IsResource` has not been made a required component for a resource. +Use `#[derive(Resource)]` instead. diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 57442bc6b77e6..3eeea79e6d5aa 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -1499,27 +1499,6 @@ mod tests { assert_eq!((s, i), (set_from_u32s(vec![0]), true)); } - #[test] - fn invertible_union_with_different_lengths() { - // When adding a large inverted set to a small normal set, - // make sure we invert the bits beyond the original length. - // Failing to call `grow` before `toggle_range` would cause bit 1 to be zero, - // which would incorrectly treat it as included in the output set. - let mut self_set = set_from_u32s(vec![0]); - let mut self_inverted = false; - let other_set = set_from_u32s(vec![0, 1]); - let other_inverted = true; - invertible_union_with( - &mut self_set, - &mut self_inverted, - &other_set, - other_inverted, - ); - - // [0] | [2, ...] = [0, 2, ...] - assert_eq!((self_set, self_inverted), (set_from_u32s(vec![1]), true)); - } - #[test] fn invertible_difference_with_tests() { let invertible_difference = |mut self_inverted: bool, other_inverted: bool| { @@ -1552,130 +1531,4 @@ mod tests { // [2, 3, ...] - [1, 3, ...] = [2] assert_eq!((s, i), (set_from_u32s(vec![2]), false)); } - - #[test] - fn component_id_set_insert_remove_clear() { - let mut set = ComponentIdSet::new(); - assert!(!set.contains(&ComponentId::from_u32(0))); - assert!(!set.contains(&ComponentId::from_u32(1))); - assert!(!set.contains(&ComponentId::from_u32(2))); - assert!(set.is_empty()); - set.insert(ComponentId::from_u32(2)); - set.insert(ComponentId::from_u32(1)); - assert!(!set.contains(&ComponentId::from_u32(0))); - assert!(set.contains(&ComponentId::from_u32(1))); - assert!(set.contains(&ComponentId::from_u32(2))); - assert!(!set.is_empty()); - set.remove(&ComponentId::from_u32(1)); - assert!(!set.contains(&ComponentId::from_u32(0))); - assert!(!set.contains(&ComponentId::from_u32(1))); - assert!(set.contains(&ComponentId::from_u32(2))); - assert!(!set.is_empty()); - set.insert(ComponentId::from_u32(2)); - set.insert(ComponentId::from_u32(1)); - assert!(!set.contains(&ComponentId::from_u32(0))); - assert!(set.contains(&ComponentId::from_u32(1))); - assert!(set.contains(&ComponentId::from_u32(2))); - assert!(!set.is_empty()); - set.clear(); - assert!(!set.contains(&ComponentId::from_u32(0))); - assert!(!set.contains(&ComponentId::from_u32(1))); - assert!(!set.contains(&ComponentId::from_u32(2))); - assert!(set.is_empty()); - } - - #[test] - fn component_id_set_remove_out_of_range() { - let mut set = ComponentIdSet::new(); - set.remove(&ComponentId::from_u32(3)); - set.insert(ComponentId::from_u32(1)); - set.remove(&ComponentId::from_u32(4)); - assert!(set.iter().eq([&ComponentId::from_u32(1)])); - } - - #[test] - fn component_id_set_is_subset_is_disjoint() { - let set_1234 = set_from_u32s(vec![1, 2, 3, 4]); - let set_23 = set_from_u32s(vec![2, 3]); - let set_45 = set_from_u32s(vec![4, 5]); - assert!(set_23.is_subset(&set_1234)); - assert!(!set_1234.is_subset(&set_23)); - assert!(set_23.is_disjoint(&set_45)); - assert!(set_45.is_disjoint(&set_23)); - assert!(!set_1234.is_disjoint(&set_23)); - assert!(!set_23.is_disjoint(&set_1234)); - } - - #[test] - fn component_id_set_union_intersection_difference() { - let set_13 = set_from_u32s(vec![1, 3]); - let set_23 = set_from_u32s(vec![2, 3]); - - assert_eq!( - set_13.union(&set_23).copied().collect::(), - set_from_u32s(vec![1, 2, 3]) - ); - assert_eq!( - set_23.union(&set_13).copied().collect::(), - set_from_u32s(vec![1, 2, 3]) - ); - assert_eq!( - set_13 - .intersection(&set_23) - .copied() - .collect::(), - set_from_u32s(vec![3]) - ); - assert_eq!( - set_23 - .intersection(&set_13) - .copied() - .collect::(), - set_from_u32s(vec![3]) - ); - assert_eq!( - set_13 - .difference(&set_23) - .copied() - .collect::(), - set_from_u32s(vec![1]) - ); - assert_eq!( - set_23 - .difference(&set_13) - .copied() - .collect::(), - set_from_u32s(vec![2]) - ); - } - - #[test] - fn component_id_set_union_intersection_difference_with() { - let set_13 = set_from_u32s(vec![1, 3]); - let set_23 = set_from_u32s(vec![2, 3]); - - let mut s = set_13.clone(); - s.union_with(&set_23); - assert_eq!(s, set_from_u32s(vec![1, 2, 3])); - - let mut s = set_23.clone(); - s.union_with(&set_13); - assert_eq!(s, set_from_u32s(vec![1, 2, 3])); - - let mut s = set_13.clone(); - s.intersect_with(&set_23); - assert_eq!(s, set_from_u32s(vec![3])); - - let mut s = set_23.clone(); - s.intersect_with(&set_13); - assert_eq!(s, set_from_u32s(vec![3])); - - let mut s = set_13.clone(); - s.difference_with(&set_23); - assert_eq!(s, set_from_u32s(vec![1])); - - let mut s = set_23.clone(); - s.difference_with(&set_13); - assert_eq!(s, set_from_u32s(vec![2])); - } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index a8f78b15f01d3..1fd14b426d4af 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -3322,7 +3322,7 @@ impl World { let ids: Vec = self.components().iter_registered_ids().collect(); for component_id in ids { let entity = component_id.entity(); - if let Ok(entity) = self.get_entity_mut(entity) { + if let Ok(mut entity) = self.get_entity_mut(entity) { // only resource entities with a matching component_id should have a component. entity.remove_by_id(component_id); } diff --git a/crates/bevy_settings/src/lib.rs b/crates/bevy_settings/src/lib.rs index b2c158f6f52c4..2797677f3f981 100644 --- a/crates/bevy_settings/src/lib.rs +++ b/crates/bevy_settings/src/lib.rs @@ -461,11 +461,11 @@ fn apply_settings_to_world( let settings_key = reflect_settings_group.settings_key_name; let reflect_component = ty.data::().unwrap(); - let component_id = world.components().get_id(*tid); + let component_id = reflect_component.register_component(world); + let res_entity = component_id.entity(); - if let Some(component_id) = component_id { + if world.entities().contains_spawned(res_entity) { // Resource already exists, so apply toml properties to it. - let res_entity = component_id.entity(); let res_entity_mut = world.entity_mut(res_entity); let Some(mut reflect) = reflect_component.reflect_mut(res_entity_mut) else { continue; @@ -487,7 +487,6 @@ fn apply_settings_to_world( } } else { // The resource does not exist, so create a default. - let component_id = reflect_component.register_component(world); let mut res_entity = world .spawn_empty_at(component_id.entity()) .expect("entity was just allocated"); From 2c764e684810c01a6762bc75a1587401c6496398 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Thu, 2 Jul 2026 02:13:09 +0200 Subject: [PATCH 23/44] format --- crates/bevy_ecs/src/resource.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 8fbb676e70b08..f0a979fc552cf 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -135,9 +135,12 @@ impl IsResource { .components() .get_name(resource_component_id) .expect("resource is registered"); - warn!("Tried inserting the resource {} on the wrong entity. \ + warn!( + "Tried inserting the resource {} on the wrong entity. \ Resources are unique components stored on the entity matching their `component_id`. \ - Inserting on a different entity causes the new value to be removed.", name); + Inserting on a different entity causes the new value to be removed.", + name + ); } } From faf0adb3efa4ba725200e6a3dcf47252ca9e8c8c Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 5 Jul 2026 21:16:00 +0200 Subject: [PATCH 24/44] changed migration guide --- .../migration-guides/components-as-entities.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index 23b362ca301fb..1eb84da4f29e1 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -5,7 +5,12 @@ pull_requests: [24728] - `ComponentId::new` now takes `Entity` as an argument instead of `usize`. For debugging, you can use `ComponentId::from_u32`. - `ComponentId::index` was removed in favor of implementing `ContainsEntity`, call `ComponentId::entity` to get the underlying entity. -- `ComponentIdSet` is now an `EntityEquivalentHashSet` instead of a `FixedBitSet`. This means that methods like `union_with` no longer work, use `bitor_assign` instead. +- `ComponentIdSet` is now an `EntityEquivalentHashSet` instead of a `FixedBitSet`. + + `ComponentIdSet::is_clear` has changed to `ComponentIdSet::is_empty`. + + `ComponentIdSet::difference` has changed to `-`, i.e.: `difference = set - other`. + + `ComponentIdSet::intersection` has changed to `&`, i.e.: `intersection = set & other`. + + `ComponentIdSet::union` has changed to `|`, i.e.: `union = set | other`. + + Other methods have remained the same. - `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while `ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. - `Access` and `EcsAccessType` no longer derive `Hash`. - `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`. From 0d4bcf75d9c4859c501474b1dc89b24e3281f6da Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 5 Jul 2026 21:20:15 +0200 Subject: [PATCH 25/44] markdown fix --- .../migration-guides/components-as-entities.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index 1eb84da4f29e1..979871bc8b9fd 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -6,11 +6,11 @@ pull_requests: [24728] - `ComponentId::new` now takes `Entity` as an argument instead of `usize`. For debugging, you can use `ComponentId::from_u32`. - `ComponentId::index` was removed in favor of implementing `ContainsEntity`, call `ComponentId::entity` to get the underlying entity. - `ComponentIdSet` is now an `EntityEquivalentHashSet` instead of a `FixedBitSet`. - + `ComponentIdSet::is_clear` has changed to `ComponentIdSet::is_empty`. - + `ComponentIdSet::difference` has changed to `-`, i.e.: `difference = set - other`. - + `ComponentIdSet::intersection` has changed to `&`, i.e.: `intersection = set & other`. - + `ComponentIdSet::union` has changed to `|`, i.e.: `union = set | other`. - + Other methods have remained the same. + - `ComponentIdSet::is_clear` has changed to `ComponentIdSet::is_empty`. + - `ComponentIdSet::difference` has changed to `-`, i.e.: `difference = set - other`. + - `ComponentIdSet::intersection` has changed to `&`, i.e.: `intersection = set & other`. + - `ComponentIdSet::union` has changed to `|`, i.e.: `union = set | other`. + - Other methods have remained the same. - `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while `ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. - `Access` and `EcsAccessType` no longer derive `Hash`. - `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`. From ff207f8c0a55e1c4b8a9baa96a690a4f84f8f9d5 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 7 Jul 2026 02:10:05 +0200 Subject: [PATCH 26/44] cleaned up Res/Mut and hooks --- crates/bevy_ecs/macro_logic/src/component.rs | 7 + crates/bevy_ecs/macros/src/resource.rs | 12 +- crates/bevy_ecs/src/resource.rs | 146 +++++++------------ crates/bevy_ecs/src/system/system_param.rs | 62 ++++++-- 4 files changed, 107 insertions(+), 120 deletions(-) diff --git a/crates/bevy_ecs/macro_logic/src/component.rs b/crates/bevy_ecs/macro_logic/src/component.rs index 25851b55fce28..25bd493b3192a 100644 --- a/crates/bevy_ecs/macro_logic/src/component.rs +++ b/crates/bevy_ecs/macro_logic/src/component.rs @@ -51,6 +51,8 @@ pub struct DeriveComponent { pub map_entities: Option, /// Additional required component registrations that are added in `Component::register_required_components` pub additional_requires: Vec, + /// Additional `on_insert` hook + pub additional_insert_hook: Option, } impl DeriveComponent { @@ -70,6 +72,7 @@ impl DeriveComponent { clone_behavior: None, map_entities: None, additional_requires: Vec::new(), + additional_insert_hook: None, }; let mut require_paths = HashSet::new(); @@ -228,6 +231,10 @@ impl DeriveComponent { let mut on_despawn_path = Vec::from_iter(self.on_despawn.map(|path| path.to_token_stream(bevy_ecs))); + if let Some(extra_insert_hook) = self.additional_insert_hook { + on_insert_path.push(extra_insert_hook); + } + if relationship.is_some() { on_insert_path.push(quote!(::on_insert)); on_discard_path diff --git a/crates/bevy_ecs/macros/src/resource.rs b/crates/bevy_ecs/macros/src/resource.rs index 2401d63a35d7e..f0e9dee24b876 100644 --- a/crates/bevy_ecs/macros/src/resource.rs +++ b/crates/bevy_ecs/macros/src/resource.rs @@ -1,5 +1,4 @@ use bevy_ecs_macro_logic::component::{DeriveComponent, StorageAttribute, StorageTy}; -use bevy_macro_utils::fq_std::FQOption; use proc_macro2::TokenStream; use quote::quote; use syn::{DeriveInput, Path}; @@ -11,17 +10,12 @@ pub fn derive_resource(ast: &mut DeriveInput) -> TokenStream { Err(e) => return e.into_compile_error(), }; - let struct_name = &ast.ident; - let (_, type_generics, _) = &ast.generics.split_for_impl(); + // We add an extra insert hook to the resource + derive_component.additional_insert_hook = Some(quote!(#bevy_ecs::resource::on_resource_insert)); // We add the component_id existence check here to avoid recursive init during required components initialization. derive_component.additional_requires.push(quote! { - let resource_component_id = if let #FQOption::Some(id) = required_components.components_registrator().component_id::<#struct_name #type_generics>() { - id - } else { - required_components.components_registrator().register_component::<#struct_name #type_generics>() - }; - required_components.register_required::<#bevy_ecs::resource::IsResource>(move || #bevy_ecs::resource::IsResource::new(resource_component_id)); + required_components.register_required::<#bevy_ecs::resource::IsResource>(|| #bevy_ecs::resource::IsResource); }); let component_impl = match derive_component.impl_component(ast, &bevy_ecs, StorageTy::SparseSet) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index f0a979fc552cf..072fd9c7f2701 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -13,6 +13,46 @@ use {crate::reflect::ReflectComponent, bevy_reflect::Reflect}; // The derive macro for the `Resource` trait pub use bevy_ecs_macros::Resource; +/// The main hook that checks that a resource is inserted on the right entity, thereby guaranteeing uniqueness. +/// It's still possible to define your own insert hooks on resources, which will exist alongside this one. +pub fn on_resource_insert(mut world: DeferredWorld, context: HookContext) { + let resource_id = context.component_id; + let resource_entity = resource_id.entity(); + + if !world.entities().contains(resource_entity) { + let name = world + .components() + .get_name(resource_id) + .expect("resource is registered"); + warn!( + "Resource entity {} of {} has been despawned, when it's not supposed to be.", + resource_entity, name + ); + } + + if resource_entity != context.entity { + // the resource already exists and the new one should be removed + world + .commands() + .entity(context.entity) + .remove_by_id(resource_id); + world + .commands() + .entity(context.entity) + .remove_by_id(IS_RESOURCE); + let name = world + .components() + .get_name(resource_id) + .expect("resource is registered"); + warn!( + "Tried inserting the resource {} on the wrong entity. \ + Resources are unique components stored on the entity matching their `component_id`. \ + Inserting on a different entity causes the new value to be removed.", + name + ); + } +} + /// A type that can be inserted into a [`World`] as a singleton. /// /// You can access resource data in systems using the [`Res`] and [`ResMut`] system parameters @@ -87,79 +127,16 @@ pub trait Resource: Component {} /// A marker component for entities that have a Resource component. #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Debug))] #[derive(Component, Debug)] -#[component(on_insert, on_discard, on_despawn)] -pub struct IsResource(ComponentId); +#[component(on_discard, on_despawn)] +pub struct IsResource; impl IsResource { - /// Creates a new instance with the given `component_id` - pub fn new(component_id: ComponentId) -> Self { - Self(component_id) - } - - /// The [`ComponentId`] of the resource component (the _actual_ resource value component, not the [`IsResource`] component). - pub fn resource_component_id(&self) -> ComponentId { - self.0 - } - - pub(crate) fn on_insert(mut world: DeferredWorld, context: HookContext) { - let resource_component_id = world - .entity(context.entity) - .get::() - .unwrap() - .resource_component_id(); - - let original_entity = resource_component_id.entity(); - - if !world.entities().contains(original_entity) { - let name = world - .components() - .get_name(resource_component_id) - .expect("resource is registered"); - panic!( - "Resource entity {} of {} has been despawned, when it's not supposed to be.", - original_entity, name - ); - } - - if original_entity != context.entity { - // the resource already exists and the new one should be removed - world - .commands() - .entity(context.entity) - .remove_by_id(resource_component_id); - world - .commands() - .entity(context.entity) - .remove_by_id(context.component_id); - let name = world - .components() - .get_name(resource_component_id) - .expect("resource is registered"); - warn!( - "Tried inserting the resource {} on the wrong entity. \ - Resources are unique components stored on the entity matching their `component_id`. \ - Inserting on a different entity causes the new value to be removed.", - name - ); - } - } - - pub(crate) fn on_discard(world: DeferredWorld, context: HookContext) { - let resource_component_id = world - .entity(context.entity) - .get::() - .unwrap() - .resource_component_id(); - - let original_entity = resource_component_id.entity(); - - if original_entity == context.entity { - panic!("IsResource components should never be removed from their resource entity.") - } + pub(crate) fn on_discard(_world: DeferredWorld, _context: HookContext) { + warn!("IsResource components should not be removed from a resource entity") } pub(crate) fn on_despawn(_world: DeferredWorld, _context: HookContext) { - panic!("Resource entities are not supposed to be despawned."); + warn!("Resource entities are not supposed to be despawned."); } } @@ -248,8 +225,8 @@ mod tests { let first_entity = { let resources = query.iter(&world).collect::>(); assert_eq!(resources.len(), 1); - let (entity, _test_resource, is_resource) = resources[0]; - assert_eq!(is_resource.resource_component_id(), id); + let (entity, _test_resource, _is_resource) = resources[0]; + assert_eq!(entity, id.entity()); entity }; @@ -259,8 +236,8 @@ mod tests { let second_entity = { let resources = query.iter(&world).collect::>(); assert_eq!(resources.len(), 1); - let (entity, _test_resource, is_resource) = resources[0]; - assert_eq!(is_resource.resource_component_id(), id); + let (entity, _test_resource, _is_resource) = resources[0]; + assert_eq!(entity, id.entity()); entity }; @@ -317,29 +294,4 @@ mod tests { 1 ); } - - #[test] - #[should_panic] - fn remove_resource_marker_should_panic() { - #[derive(Resource, Default)] - struct R; - - let mut world = World::new(); - world.init_resource::(); - let entity = world.register_component::().entity(); - let mut entity = world.entity_mut(entity); - entity.remove::(); - } - - #[test] - #[should_panic] - fn despawn_resource_should_panic() { - #[derive(Resource, Default)] - struct R; - - let mut world = World::new(); - world.init_resource::(); - let entity = world.register_component::().entity(); - world.despawn(entity); - } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 1877127536bfc..9e94165a41b35 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -9,7 +9,7 @@ use crate::{ bundle::Bundles, change_detection::{ComponentTicksMut, ComponentTicksRef, Tick}, component::{ComponentId, Components, Mutable}, - entity::{Entities, EntityAllocator}, + entity::{ContainsEntity, Entities, EntityAllocator}, query::{ Access, FilteredAccess, FilteredAccessSet, IterQueryData, QueryData, QueryFilter, QuerySingleError, QueryState, ReadOnlyQueryData, @@ -32,6 +32,7 @@ use core::{ marker::PhantomData, ops::{Deref, DerefMut}, }; +use log::warn; use smallvec::SmallVec; use thiserror::Error; @@ -675,12 +676,19 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { fn init_state(world: &mut World) -> Self::State { let component_id = world.components_registrator().register_component::(); - assert!( - world - .get_required_components_by_id(component_id) - .is_some_and(|required| required.direct.contains_key(&IS_RESOURCE)), - "resource does not have IsResource as a required component" - ); + if world + .get_required_components_by_id(component_id) + .is_some_and(|required| required.direct.contains_key(&IS_RESOURCE)) + { + let name = world + .components() + .get_name(component_id) + .expect("resource is registered"); + warn!( + "Resource {} does not have IsResource as a required component, hence it cannot be queried through Res.", + name + ); + } component_id } @@ -718,6 +726,14 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { let (ptr, ticks) = world.get_resource_with_ticks(component_id).ok_or_else(|| { SystemParamValidationError::invalid::("Resource does not exist") })?; + if !world + .get_entity(component_id.entity()) + .is_ok_and(|entity| entity.contains_id(IS_RESOURCE)) + { + return Err(SystemParamValidationError::invalid::( + "Resource does not have IsResource", + )); + } Ok(Res { value: ptr.deref(), ticks: ComponentTicksRef { @@ -739,12 +755,19 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> fn init_state(world: &mut World) -> Self::State { let component_id = world.components_registrator().register_component::(); - assert!( - world - .get_required_components_by_id(component_id) - .is_some_and(|required| required.direct.contains_key(&IS_RESOURCE)), - "resource does not have IsResource as a required component" - ); + if world + .get_required_components_by_id(component_id) + .is_some_and(|required| required.direct.contains_key(&IS_RESOURCE)) + { + let name = world + .components() + .get_name(component_id) + .expect("resource is registered"); + warn!( + "Resource {} does not have IsResource as a required component, hence it cannot be queried through ResMut.", + name + ); + } component_id } @@ -779,9 +802,20 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> world: UnsafeWorldCell<'w>, change_tick: Tick, ) -> Result, SystemParamValidationError> { - let value = world.get_resource_mut_by_id(component_id).ok_or_else(|| { + let entity = component_id.entity(); + let entity_cell = world + .get_entity(entity) + .map_err(|_| SystemParamValidationError::invalid::("Resource does not exist"))?; + if !entity_cell.contains_id(IS_RESOURCE) { + return Err(SystemParamValidationError::invalid::( + "Resource does not have IsResource", + )); + } + // SAFETY: Through the scheduler we have unique access to this resource + let value = unsafe { entity_cell.get_mut_by_id(component_id).ok() }.ok_or_else(|| { SystemParamValidationError::invalid::("Resource does not exist") })?; + Ok(ResMut { value: value.value.deref_mut::(), ticks: ComponentTicksMut { From 38489bbba5e9f9915dc26e6219db1736402e237c Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 7 Jul 2026 13:37:11 +0200 Subject: [PATCH 27/44] cleanup --- .../components-as-entities.md | 2 +- .../bevy_ecs/src/change_detection/params.rs | 8 ++-- crates/bevy_ecs/src/resource.rs | 20 ++++++++-- crates/bevy_ecs/src/system/system_param.rs | 33 ++++++++-------- .../bevy_ecs/src/world/unsafe_world_cell.rs | 38 +++++++++++++++++-- 5 files changed, 72 insertions(+), 29 deletions(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index 979871bc8b9fd..2872a454b9021 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -14,7 +14,7 @@ pull_requests: [24728] - `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while `ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. - `Access` and `EcsAccessType` no longer derive `Hash`. - `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`. -- Despawning a resource entity has been upgraded from a `warn!` to a `panic!`, moreover, removing `IsResource` from a resource entity also panics. +- The `component_id` field has been removed from the `IsResource` component, along with `IsResource::new` and `IsResource::resource_component_id`. If you need the `ComponentId` for a resource dynamically, you can simply query `Entity` alongside the resource you want and wrap it: `ComponentId::new(entity)`. In 0.19, you could attach components to a resource by simply calling `world.spawn((Res1, Comp1, Comp2))`. In 0.20, this no longer works as `Res1` needs to be on the resource entity allocated by `world.register_component()`. In 0.20, adding components looks as follows: diff --git a/crates/bevy_ecs/src/change_detection/params.rs b/crates/bevy_ecs/src/change_detection/params.rs index 0d49e9f17f265..070ec1f0aca48 100644 --- a/crates/bevy_ecs/src/change_detection/params.rs +++ b/crates/bevy_ecs/src/change_detection/params.rs @@ -86,7 +86,7 @@ impl<'w> ContiguousComponentTicksRef<'w> { } /// Creates a new `ContiguousComponentTicksRef` using provided values or returns [`None`] if lengths of - /// `added`, `changed` and `changed_by` do not match + /// `added`, `changed` and `changed_by` do not match /// /// This is an advanced feature, `ContiguousComponentTicksRef`s are designed to be _created_ by /// engine-internal code and _consumed_ by end-user code. @@ -283,7 +283,7 @@ impl<'w> ContiguousComponentTicksMut<'w> { } /// Creates a new `ContiguousComponentTicksMut` using provided values or returns [`None`] if lengths of - /// `added`, `changed` and `changed_by` do not match + /// `added`, `changed` and `changed_by` do not match /// /// This is an advanced feature, `ContiguousComponentTicksMut`s are designed to be _created_ by /// engine-internal code and _consumed_ by end-user code. @@ -790,7 +790,7 @@ impl<'w, T> ContiguousRef<'w, T> { } /// Creates a new `ContiguousRef` using provided values or returns [`None`] if lengths of - /// `value`, `added`, `changed` and `changed_by` do not match + /// `value`, `added`, `changed` and `changed_by` do not match /// /// This is an advanced feature, `ContiguousRef`s are designed to be _created_ by /// engine-internal code and _consumed_ by end-user code. @@ -1046,7 +1046,7 @@ impl<'w, T> ContiguousMut<'w, T> { } /// Creates a new `ContiguousMut` using provided values or returns [`None`] if lengths of - /// `value`, `added`, `changed` and `changed_by` do not match + /// `value`, `added`, `changed` and `changed_by` do not match /// /// This is an advanced feature, `ContiguousMut`s are designed to be _created_ by /// engine-internal code and _consumed_ by end-user code. diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index 072fd9c7f2701..ff122f017a9a8 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -131,12 +131,24 @@ pub trait Resource: Component {} pub struct IsResource; impl IsResource { - pub(crate) fn on_discard(_world: DeferredWorld, _context: HookContext) { - warn!("IsResource components should not be removed from a resource entity") + pub(crate) fn on_discard(world: DeferredWorld, context: HookContext) { + let maybe_resource_id = ComponentId::new(context.entity); + + // If IsResource exists on a resource entity then maybe_resource_id is + // a valid component id and we should not remove it. + if world.components().is_id_valid(maybe_resource_id) { + warn!("IsResource components should not be removed from a resource entity") + } } - pub(crate) fn on_despawn(_world: DeferredWorld, _context: HookContext) { - warn!("Resource entities are not supposed to be despawned."); + pub(crate) fn on_despawn(world: DeferredWorld, context: HookContext) { + let maybe_resource_id = ComponentId::new(context.entity); + + // If IsResource exists on a resource entity then maybe_resource_id is + // a valid component id and we should not despawn the entity. + if world.components().is_id_valid(maybe_resource_id) { + warn!("Resource entities are not supposed to be despawned."); + } } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 9e94165a41b35..56b7f08c118a8 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -723,17 +723,20 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { world: UnsafeWorldCell<'w>, change_tick: Tick, ) -> Result, SystemParamValidationError> { - let (ptr, ticks) = world.get_resource_with_ticks(component_id).ok_or_else(|| { - SystemParamValidationError::invalid::("Resource does not exist") - })?; - if !world - .get_entity(component_id.entity()) - .is_ok_and(|entity| entity.contains_id(IS_RESOURCE)) - { + let entity = component_id.entity(); + let entity_cell = world + .get_entity(entity) + .map_err(|_| SystemParamValidationError::invalid::("Resource does not exist"))?; + if !entity_cell.contains_id(IS_RESOURCE) { return Err(SystemParamValidationError::invalid::( "Resource does not have IsResource", )); } + // SAFETY: Through the scheduler we have unique access to this resource + let (ptr, ticks) = + unsafe { entity_cell.get_by_id_with_ticks(component_id) }.ok_or_else(|| { + SystemParamValidationError::invalid::("Resource does not exist") + })?; Ok(Res { value: ptr.deref(), ticks: ComponentTicksRef { @@ -2795,7 +2798,7 @@ mod tests { use crate::component::Component; use crate::query::Without; use crate::resource::IsResource; - use crate::system::assert_is_system; + use crate::system::{assert_is_system, RegisteredSystemError}; use crate::world::EntityMut; use core::cell::RefCell; @@ -3112,21 +3115,19 @@ mod tests { } #[test] - #[should_panic] fn missing_resource_marker() { #[derive(Component, Default)] struct R; - // In order to prevent UB, one should always have `IsResource` by a required component - // for every type `R` that implements Resource, else using `Res` and `ResMut` panics. + // In order for Res and ResMut queries to work, there should always be `IsResource` + // on the resource entity for every type `R` that implements Resource. impl Resource for R {} let mut world = World::new(); world.init_resource::(); - world - .run_system_cached( - |_: Option>, _: Option>>| {}, - ) - .unwrap(); + assert!(matches!( + world.run_system_cached(|_: Res, _: Option>>| {}), + Err(RegisteredSystemError::Failed(_)) + ),); } } diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 6782d26f0e704..2ae09487a079d 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -638,15 +638,13 @@ impl<'w> UnsafeWorldCell<'w> { self, component_id: ComponentId, ) -> Option<(Ptr<'w>, ComponentTickCells<'w>)> { - let entity = component_id.entity(); - let storage_type = self.components().get_info(component_id)?.storage_type(); - let location = self.get_entity(entity).ok()?.location(); + let entity = self.get_entity(component_id.entity()).ok()?; // SAFETY: // - caller ensures there is no `&mut World` // - caller ensures there are no mutable borrows of this resource // - caller ensures that we have permission to access this resource // - storage_type and location are valid - unsafe { get_component_and_ticks(self, component_id, storage_type, entity, location) } + unsafe { entity.get_by_id_with_ticks(component_id) } } // Shorthand helper function for getting the data and change ticks for a resource. @@ -1081,6 +1079,38 @@ impl<'w> UnsafeEntityCell<'w> { } } + /// Gets the component and ticks of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + /// + /// Unlike [`UnsafeEntityCell::get`], this returns a raw pointer to the component, + /// which is only valid while the `'w` borrow of the lifetime is active. + /// + /// # Safety + /// It is the caller's responsibility to ensure that + /// - the [`UnsafeEntityCell`] has permission to access the component + /// - no other mutable references to the component exist at the same time + #[inline] + pub unsafe fn get_by_id_with_ticks( + self, + component_id: ComponentId, + ) -> Option<(Ptr<'w>, ComponentTickCells<'w>)> { + let info = self.world.components().get_info(component_id)?; + + // SAFETY: + unsafe { + get_component_and_ticks( + self.world, + component_id, + info.storage_type(), + self.entity, + self.location, + ) + } + } + /// Retrieves a mutable untyped reference to the given `entity`'s [`Component`] of the given [`ComponentId`]. /// Returns `None` if the `entity` does not have a [`Component`] of the given type, /// or if the component is immutable. From 9357ba225548d930dd26ff750a78cbffad0c2581 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 7 Jul 2026 14:45:00 +0200 Subject: [PATCH 28/44] add documentation --- crates/bevy_ecs/src/resource.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index ff122f017a9a8..d9b1363a3c157 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -112,6 +112,21 @@ pub fn on_resource_insert(mut world: DeferredWorld, context: HookContext) { /// } /// ``` /// +/// # Resources entities +/// +/// Under the hood, resource data is stored as a component on a resource entity. This resource +/// is first [allocated](crate::entity::EntityAllocator) upon registration, through +/// `world.register_component::()` or its alias `world.register_resource::()`. +/// When initializing or inserting a resource, we spawn the actual entity on the world. +/// Additionally, we add both the resource as a component *and* the [`IsResource`] marker to +/// the entity. Because [`IsResource`] is a required component for every resource this is done +/// automatically. +/// +/// From this point forward, hooks on [`IsResource`] warn against despawning the resource entity, +/// while a hook on the resource type ensures that it is unique. While one shouldn't despawn the +/// resource entity, you can still remove resources. This simply removes the resource component +/// from the resource entity. +/// /// [`Exclusive`]: https://doc.rust-lang.org/nightly/std/sync/struct.Exclusive.html /// [`World`]: crate::world::World /// [`Res`]: crate::system::Res @@ -137,7 +152,7 @@ impl IsResource { // If IsResource exists on a resource entity then maybe_resource_id is // a valid component id and we should not remove it. if world.components().is_id_valid(maybe_resource_id) { - warn!("IsResource components should not be removed from a resource entity") + warn!("IsResource components should not be removed from a resource entity"); } } From a7a29722d01edc4cca0a97744ae470da245ab12d Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 12 Jul 2026 15:21:09 +0200 Subject: [PATCH 29/44] fix bevy_dev_tools --- crates/bevy_dev_tools/src/schedule_data/serde.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_dev_tools/src/schedule_data/serde.rs b/crates/bevy_dev_tools/src/schedule_data/serde.rs index d336944dcc625..86db67406d0c2 100644 --- a/crates/bevy_dev_tools/src/schedule_data/serde.rs +++ b/crates/bevy_dev_tools/src/schedule_data/serde.rs @@ -129,11 +129,11 @@ impl AccessData { let writes = value.try_writes(); let (reads_inverted, reads) = match reads { - Ok(reads) => (false, trace.get_indexes(reads.iter())), + Ok(reads) => (false, trace.get_indexes(reads.iter().copied())), Err(_) => (true, vec![]), }; let (writes_inverted, writes) = match writes { - Ok(writes) => (false, trace.get_indexes(writes.iter())), + Ok(writes) => (false, trace.get_indexes(writes.iter().copied())), Err(_) => (true, vec![]), }; @@ -142,7 +142,7 @@ impl AccessData { writes, reads_inverted, writes_inverted, - archetypal: trace.get_indexes(value.archetypal().iter()), + archetypal: trace.get_indexes(value.archetypal().iter().copied()), } } } @@ -164,8 +164,8 @@ pub struct AccessFiltersData { impl AccessFiltersData { fn new(value: &bevy_ecs::query::AccessFilters, trace: &mut ComponentTrace) -> Self { Self { - with: trace.get_indexes(value.with().iter()), - without: trace.get_indexes(value.without().iter()), + with: trace.get_indexes(value.with().iter().copied()), + without: trace.get_indexes(value.without().iter().copied()), } } } From 9137f52751865d07ebfb315de9f5f7fd4163d9d1 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Tue, 25 Aug 2026 01:22:02 +0200 Subject: [PATCH 30/44] small fix --- crates/bevy_dev_tools/src/schedule_data/serde.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_dev_tools/src/schedule_data/serde.rs b/crates/bevy_dev_tools/src/schedule_data/serde.rs index b3ae56a550f29..c707563df3740 100644 --- a/crates/bevy_dev_tools/src/schedule_data/serde.rs +++ b/crates/bevy_dev_tools/src/schedule_data/serde.rs @@ -129,11 +129,11 @@ impl AccessData { let writes = value.writes().as_finite_set(); let (reads_inverted, reads) = match reads { - Some(reads) => (false, trace.get_indexes(reads.iter())), + Some(reads) => (false, trace.get_indexes(reads.iter().copied())), None => (true, vec![]), }; let (writes_inverted, writes) = match writes { - Some(writes) => (false, trace.get_indexes(writes.iter())), + Some(writes) => (false, trace.get_indexes(writes.iter().copied())), None => (true, vec![]), }; From aacc2bba75fbd00841b22fdec0b347b29a0ea296 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Mon, 14 Sep 2026 15:39:10 +0200 Subject: [PATCH 31/44] doc fix --- _release-content/migration-guides/components-as-entities.md | 2 +- crates/bevy_ecs/src/world/mod.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/_release-content/migration-guides/components-as-entities.md b/_release-content/migration-guides/components-as-entities.md index 2872a454b9021..6408add58a080 100644 --- a/_release-content/migration-guides/components-as-entities.md +++ b/_release-content/migration-guides/components-as-entities.md @@ -13,7 +13,7 @@ pull_requests: [24728] - Other methods have remained the same. - `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while `ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`. - `Access` and `EcsAccessType` no longer derive `Hash`. -- `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`. +- `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. It can also no longer be used as a system param. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`. - The `component_id` field has been removed from the `IsResource` component, along with `IsResource::new` and `IsResource::resource_component_id`. If you need the `ComponentId` for a resource dynamically, you can simply query `Entity` alongside the resource you want and wrap it: `ComponentId::new(entity)`. In 0.19, you could attach components to a resource by simply calling `world.spawn((Res1, Comp1, Comp2))`. In 0.20, this no longer works as `Res1` needs to be on the resource entity allocated by `world.register_component()`. In 0.20, adding components looks as follows: diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 4888bc2d2c8ad..f4957bea234dd 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -3519,7 +3519,7 @@ impl World { /// # world.insert_resource(A(1)); /// # world.insert_resource(B(2)); /// let mut total = 0; - /// for (info, _) in world.iter_resources() { + /// for (_, info, _) in world.iter_resources() { /// println!("Resource: {}", info.name()); /// println!("Size: {} bytes", info.layout().size()); /// total += info.layout().size(); @@ -3568,7 +3568,7 @@ impl World { /// })); /// /// // Iterate all resources, in order to run the closures for each matching resource type - /// for (info, ptr) in world.iter_resources() { + /// for (_, info, ptr) in world.iter_resources() { /// let Some(type_id) = info.type_id() else { /// // It's possible for resources to not have a `TypeId` (e.g. non-Rust resources /// // dynamically inserted via a scripting language) in which case we can't match them. @@ -3643,7 +3643,7 @@ impl World { /// })); /// /// // Iterate all resources, in order to run the mutator closures for each matching resource type - /// for (info, mut mut_untyped) in world.iter_resources_mut() { + /// for (_, info, mut mut_untyped) in world.iter_resources_mut() { /// let Some(type_id) = info.type_id() else { /// // It's possible for resources to not have a `TypeId` (e.g. non-Rust resources /// // dynamically inserted via a scripting language) in which case we can't match them. From fb25d44fd360c71c9f89f88c375a4160b4e98763 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Mon, 14 Sep 2026 16:09:23 +0200 Subject: [PATCH 32/44] remove component sparse arrays --- crates/bevy_ecs/src/archetype.rs | 33 ++++++++++++++--------- crates/bevy_ecs/src/storage/sparse_set.rs | 16 +++++++++++ crates/bevy_ecs/src/storage/table/mod.rs | 19 +++++++------ 3 files changed, 45 insertions(+), 23 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index fd853c1e0956d..700e8c9fb8b48 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -21,12 +21,14 @@ use crate::{ bundle::BundleId, - component::{ComponentId, Components, RequiredComponentConstructor, StorageType}, + component::{ + ComponentId, ComponentIdMap, Components, RequiredComponentConstructor, StorageType, + }, entity::{Entity, EntityEquivalentHashMap, EntityLocation}, event::{Event, EventKey}, observer::Observers, query::DebugCheckedUnwrap, - storage::{ImmutableSparseSet, SparseArray, SparseSet, TableId, TableRow}, + storage::{SparseArray, TableId, TableRow}, }; use alloc::{boxed::Box, vec::Vec}; use bevy_platform::collections::{hash_map::Entry, HashMap}; @@ -391,7 +393,8 @@ pub struct Archetype { table_id: TableId, edges: Edges, entities: Vec, - components: ImmutableSparseSet, + component_ids: Vec, + archetype_components: ComponentIdMap, pub(crate) flags: ArchetypeFlags, } @@ -409,12 +412,14 @@ impl Archetype { let (min_table, _) = table_components.size_hint(); let (min_sparse, _) = sparse_set_components.size_hint(); let mut flags = ArchetypeFlags::empty(); - let mut archetype_components = SparseSet::with_capacity(min_table + min_sparse); + let mut component_ids = Vec::with_capacity(min_table + min_sparse); + let mut archetype_components = ComponentIdMap::with_capacity(min_table + min_sparse); for (idx, component_id) in table_components.enumerate() { // SAFETY: We are creating an archetype that includes this component so it must exist let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); observers.update_archetype_flags(component_id, &mut flags); + component_ids.push(component_id); archetype_components.insert( component_id, ArchetypeComponentInfo { @@ -435,6 +440,7 @@ impl Archetype { let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); observers.update_archetype_flags(component_id, &mut flags); + component_ids.push(component_id); archetype_components.insert( component_id, ArchetypeComponentInfo { @@ -450,7 +456,8 @@ impl Archetype { id, table_id, entities: Vec::new(), - components: archetype_components.into_immutable(), + component_ids, + archetype_components, edges: Default::default(), flags, } @@ -510,7 +517,7 @@ impl Archetype { /// [`Table`]: crate::storage::Table #[inline] pub fn table_components(&self) -> impl Iterator + '_ { - self.components + self.archetype_components .iter() .filter(|(_, component)| component.storage_type == StorageType::Table) .map(|(id, _)| *id) @@ -523,7 +530,7 @@ impl Archetype { /// [`ComponentSparseSet`]: crate::storage::ComponentSparseSet #[inline] pub fn sparse_set_components(&self) -> impl Iterator + '_ { - self.components + self.archetype_components .iter() .filter(|(_, component)| component.storage_type == StorageType::SparseSet) .map(|(id, _)| *id) @@ -534,7 +541,7 @@ impl Archetype { /// All of the IDs are unique. #[inline] pub fn components(&self) -> &[ComponentId] { - self.components.indices() + self.component_ids.as_slice() } /// Gets an iterator of all of the components in the archetype. @@ -542,13 +549,13 @@ impl Archetype { /// All of the IDs are unique. #[inline] pub fn iter_components(&self) -> impl Iterator + Clone { - self.components.indices().iter().copied() + self.component_ids.as_slice().iter().copied() } /// Returns the total number of components in the archetype #[inline] pub fn component_count(&self) -> usize { - self.components.len() + self.archetype_components.len() } /// Fetches an immutable reference to the archetype's [`Edges`], a cache of @@ -656,7 +663,7 @@ impl Archetype { /// Checks if the archetype contains a specific component. This runs in `O(1)` time. #[inline] pub fn contains(&self, component_id: ComponentId) -> bool { - self.components.contains(component_id) + self.archetype_components.contains_key(&component_id) } /// Gets the type of storage where a component in the archetype can be found. @@ -664,8 +671,8 @@ impl Archetype { /// This runs in `O(1)` time. #[inline] pub fn get_storage_type(&self, component_id: ComponentId) -> Option { - self.components - .get(component_id) + self.archetype_components + .get(&component_id) .map(|info| info.storage_type) } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 9b4d7af2d6424..e75bc69ee51ae 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -65,6 +65,10 @@ impl SparseArray { macro_rules! impl_sparse_array { ($ty:ident) => { + #[allow( + dead_code, + reason = "ImmutableSparseArray may be used again in the future." + )] impl $ty { /// Returns `true` if the collection contains a value for the specified `index`. #[inline] @@ -129,6 +133,10 @@ impl SparseArray { } /// Converts the [`SparseArray`] into an immutable variant. + #[allow( + dead_code, + reason = "ImmutableSparseArray may be used again in the future." + )] pub(crate) fn into_immutable(self) -> ImmutableSparseArray { ImmutableSparseArray { values: self.values.into_boxed_slice(), @@ -543,6 +551,10 @@ pub(crate) struct ImmutableSparseSet { macro_rules! impl_sparse_set { ($ty:ident) => { + #[allow( + dead_code, + reason = "ImmutableSparseArray may be used again in the future." + )] impl $ty { /// Returns the number of elements in the sparse set. #[inline] @@ -722,6 +734,10 @@ impl SparseSet { } /// Converts the sparse set into its immutable variant. + #[allow( + dead_code, + reason = "ImmutableSparseArray may be used again in the future." + )] pub(crate) fn into_immutable(self) -> ImmutableSparseSet { ImmutableSparseSet { dense: self.dense.into_boxed_slice(), diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index a7ea069b724a6..29f31b11f8d5a 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -4,10 +4,10 @@ pub use column::Column; use crate::{ change_detection::{AtomicTick, CheckChangeTicks, ComponentTicks, MaybeLocation, Tick}, - component::{ComponentId, ComponentInfo, Components}, + component::{ComponentId, ComponentIdMap, ComponentInfo, Components}, entity::Entity, query::DebugCheckedUnwrap, - storage::{AbortOnPanic, ImmutableSparseSet, SparseSet}, + storage::AbortOnPanic, }; use alloc::{boxed::Box, vec, vec::Vec}; use bevy_platform::collections::HashMap; @@ -139,7 +139,7 @@ impl TableRow { // it must be the correct capacity to allocate, reallocate, and deallocate all columns. This // means the safety invariant must be enforced even in `TableBuilder`. pub(crate) struct TableBuilder { - columns: SparseSet, + columns: ComponentIdMap, entities: Vec, } @@ -148,7 +148,7 @@ impl TableBuilder { /// `column_capacity` (How many columns?) and `capacity` (How many entities per column?). pub fn with_capacity(capacity: usize, column_capacity: usize) -> Self { Self { - columns: SparseSet::with_capacity(column_capacity), + columns: ComponentIdMap::with_capacity(column_capacity), entities: Vec::with_capacity(capacity), } } @@ -176,9 +176,8 @@ impl TableBuilder { /// - If the table's columns were not added in order, sorted by [`ComponentId`]. #[must_use] pub fn build(self) -> Table { - assert!(self.columns.indices().is_sorted()); Table { - columns: self.columns.into_immutable(), + columns: self.columns, entities: self.entities, } } @@ -202,7 +201,7 @@ impl TableBuilder { // it must be the correct capacity to allocate, reallocate, and deallocate all columns. This // means the safety invariant must be enforced even in `TableBuilder`. pub struct Table { - columns: ImmutableSparseSet, + columns: ComponentIdMap, entities: Vec, } @@ -373,7 +372,7 @@ impl Table { /// [`Component`]: crate::component::Component #[inline] pub fn get_column(&self, component_id: ComponentId) -> Option<&Column> { - self.columns.get(component_id) + self.columns.get(&component_id) } /// Fetches a mutable reference to the [`Column`] for a given [`Component`] within the @@ -384,7 +383,7 @@ impl Table { /// [`Component`]: crate::component::Component #[inline] pub(crate) fn get_column_mut(&mut self, component_id: ComponentId) -> Option<&mut Column> { - self.columns.get_mut(component_id) + self.columns.get_mut(&component_id) } /// Checks if the table contains a [`Column`] for a given [`Component`]. @@ -394,7 +393,7 @@ impl Table { /// [`Component`]: crate::component::Component #[inline] pub fn has_column(&self, component_id: ComponentId) -> bool { - self.columns.contains(component_id) + self.columns.contains_key(&component_id) } /// Reserves `additional` elements worth of capacity within the table. From fe62ef1daa72fd34dae338cf79f80e0141eb7efc Mon Sep 17 00:00:00 2001 From: Christian Hughes Date: Wed, 16 Sep 2026 19:42:17 -0500 Subject: [PATCH 33/44] dont use RemoteAllocator in component queued registration --- crates/bevy_ecs/src/component/register.rs | 23 +++++++++-------------- crates/bevy_ecs/src/storage/table/mod.rs | 5 ++--- crates/bevy_ecs/src/world/mod.rs | 13 ++++--------- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/crates/bevy_ecs/src/component/register.rs b/crates/bevy_ecs/src/component/register.rs index 51fc6e224b7df..996838ac05cb2 100644 --- a/crates/bevy_ecs/src/component/register.rs +++ b/crates/bevy_ecs/src/component/register.rs @@ -4,20 +4,20 @@ use bevy_utils::TypeIdHashMap; use core::any::Any; use core::{any::TypeId, fmt::Debug, ops::Deref}; -use crate::component::{enforce_no_required_components_recursion, RequiredComponentsRegistrator}; -use crate::entity::{EntityAllocator, RemoteAllocator}; -use crate::lifecycle::ComponentHooks; use crate::{ component::{ - Component, ComponentDescriptor, ComponentId, Components, RequiredComponents, StorageType, + enforce_no_required_components_recursion, Component, ComponentDescriptor, ComponentId, + Components, RequiredComponents, RequiredComponentsRegistrator, StorageType, }, + entity::EntityAllocator, + lifecycle::ComponentHooks, query::DebugCheckedUnwrap as _, }; /// A [`Components`] wrapper that enables additional features, like registration. pub struct ComponentsRegistrator<'w> { pub(super) components: &'w mut Components, - pub(super) allocator: &'w mut EntityAllocator, + pub(super) allocator: &'w EntityAllocator, pub(super) recursion_check_stack: Vec, } @@ -35,7 +35,7 @@ impl<'w> ComponentsRegistrator<'w> { /// # Safety /// /// The [`Components`] and [`EntityAllocator`] must come from the same world. - pub unsafe fn new(components: &'w mut Components, allocator: &'w mut EntityAllocator) -> Self { + pub unsafe fn new(components: &'w mut Components, allocator: &'w EntityAllocator) -> Self { Self { components, allocator, @@ -48,12 +48,7 @@ impl<'w> ComponentsRegistrator<'w> { /// It is generally not a good idea to queue a registration when you can instead register directly on this type. pub fn as_queued(&self) -> ComponentsQueuedRegistrator<'_> { // SAFETY: ensured by the caller that created self. - unsafe { - ComponentsQueuedRegistrator::new( - self.components, - self.allocator.build_remote_allocator(), - ) - } + unsafe { ComponentsQueuedRegistrator::new(self.components, self.allocator) } } /// Applies every queued registration. @@ -377,7 +372,7 @@ impl Debug for QueuedComponents { /// Use this only if you need to know the id of a component but do not need to modify the contents of the world based on that id. pub struct ComponentsQueuedRegistrator<'w> { components: &'w Components, - allocator: RemoteAllocator, + allocator: &'w EntityAllocator, } impl Deref for ComponentsQueuedRegistrator<'_> { @@ -394,7 +389,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> { /// # Safety /// /// The [`Components`] and [`RemoteAllocator`] must come from the same world. - pub unsafe fn new(components: &'w Components, allocator: RemoteAllocator) -> Self { + pub unsafe fn new(components: &'w Components, allocator: &'w EntityAllocator) -> Self { Self { components, allocator, diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 29f31b11f8d5a..915b3c268e138 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -881,10 +881,9 @@ mod tests { #[test] fn table() { let mut components = Components::default(); - let mut allocator = EntityAllocator::default(); + let allocator = EntityAllocator::default(); // SAFETY: They are both new. - let mut registrator = - unsafe { ComponentsRegistrator::new(&mut components, &mut allocator) }; + let mut registrator = unsafe { ComponentsRegistrator::new(&mut components, &allocator) }; let component_id = registrator.register_component::>(); let columns = &[component_id]; let mut table = TableBuilder::with_capacity(0, columns.len()) diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index f4957bea234dd..34e99e8acbf14 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -266,19 +266,14 @@ impl World { #[inline] pub fn components_queue(&self) -> ComponentsQueuedRegistrator<'_> { // SAFETY: These are from the same world. - unsafe { - ComponentsQueuedRegistrator::new( - &self.components, - self.entity_allocator().build_remote_allocator(), - ) - } + unsafe { ComponentsQueuedRegistrator::new(&self.components, &self.entity_allocator) } } /// Prepares a [`ComponentsRegistrator`] for the world. #[inline] pub fn components_registrator(&mut self) -> ComponentsRegistrator<'_> { // SAFETY: These are from the same world. - unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.entity_allocator) } + unsafe { ComponentsRegistrator::new(&mut self.components, &self.entity_allocator) } } /// Retrieves this world's [`Storages`] collection. @@ -3407,7 +3402,7 @@ impl World { // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. let mut registrator = - unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.entity_allocator) }; + unsafe { ComponentsRegistrator::new(&mut self.components, &self.entity_allocator) }; // SAFETY: `registrator`, `self.storages` and `self.bundles` all come from this world. unsafe { @@ -3424,7 +3419,7 @@ impl World { // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. let mut registrator = - unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.entity_allocator) }; + unsafe { ComponentsRegistrator::new(&mut self.components, &self.entity_allocator) }; // SAFETY: `registrator`, `self.bundles` and `self.storages` are all from this world. unsafe { From ee670da2217befcd213d59a1674ccc517f187405 Mon Sep 17 00:00:00 2001 From: Christian Hughes Date: Wed, 16 Sep 2026 20:04:47 -0500 Subject: [PATCH 34/44] Remove ArchetypeComponentInfo and stop storing it in Archetype --- crates/bevy_ecs/src/archetype.rs | 76 ++----------------- crates/bevy_ecs/src/bundle/insert.rs | 17 ++++- crates/bevy_ecs/src/bundle/remove.rs | 32 +++++--- .../src/world/entity_access/world_mut.rs | 8 +- 4 files changed, 51 insertions(+), 82 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 700e8c9fb8b48..e782ca7f75a1f 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -21,9 +21,7 @@ use crate::{ bundle::BundleId, - component::{ - ComponentId, ComponentIdMap, Components, RequiredComponentConstructor, StorageType, - }, + component::{ComponentId, Components, RequiredComponentConstructor}, entity::{Entity, EntityEquivalentHashMap, EntityLocation}, event::{Event, EventKey}, observer::Observers, @@ -356,13 +354,6 @@ pub(crate) struct ArchetypeSwapRemoveResult { pub(crate) table_row: TableRow, } -/// Internal metadata for a [`Component`] within a given [`Archetype`]. -/// -/// [`Component`]: crate::component::Component -struct ArchetypeComponentInfo { - storage_type: StorageType, -} - bitflags::bitflags! { /// Flags used to keep track of metadata about the component in this [`Archetype`] /// @@ -393,8 +384,7 @@ pub struct Archetype { table_id: TableId, edges: Edges, entities: Vec, - component_ids: Vec, - archetype_components: ComponentIdMap, + component_ids: Box<[ComponentId]>, pub(crate) flags: ArchetypeFlags, } @@ -413,19 +403,12 @@ impl Archetype { let (min_sparse, _) = sparse_set_components.size_hint(); let mut flags = ArchetypeFlags::empty(); let mut component_ids = Vec::with_capacity(min_table + min_sparse); - let mut archetype_components = ComponentIdMap::with_capacity(min_table + min_sparse); for (idx, component_id) in table_components.enumerate() { // SAFETY: We are creating an archetype that includes this component so it must exist let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); observers.update_archetype_flags(component_id, &mut flags); component_ids.push(component_id); - archetype_components.insert( - component_id, - ArchetypeComponentInfo { - storage_type: StorageType::Table, - }, - ); // NOTE: the `table_components` are sorted AND they were inserted in the `Table` in the same // sorted order, so the index of the `Column` in the `Table` is the same as the index of the // component in the `table_components` vector @@ -441,12 +424,6 @@ impl Archetype { info.update_archetype_flags(&mut flags); observers.update_archetype_flags(component_id, &mut flags); component_ids.push(component_id); - archetype_components.insert( - component_id, - ArchetypeComponentInfo { - storage_type: StorageType::SparseSet, - }, - ); component_index .entry(component_id) .or_default() @@ -456,8 +433,7 @@ impl Archetype { id, table_id, entities: Vec::new(), - component_ids, - archetype_components, + component_ids: component_ids.into_boxed_slice(), edges: Default::default(), flags, } @@ -510,38 +486,12 @@ impl Archetype { ) } - /// Gets an iterator of all of the components stored in [`Table`]s. - /// - /// All of the IDs are unique. - /// - /// [`Table`]: crate::storage::Table - #[inline] - pub fn table_components(&self) -> impl Iterator + '_ { - self.archetype_components - .iter() - .filter(|(_, component)| component.storage_type == StorageType::Table) - .map(|(id, _)| *id) - } - - /// Gets an iterator of all of the components stored in [`ComponentSparseSet`]s. - /// - /// All of the IDs are unique. - /// - /// [`ComponentSparseSet`]: crate::storage::ComponentSparseSet - #[inline] - pub fn sparse_set_components(&self) -> impl Iterator + '_ { - self.archetype_components - .iter() - .filter(|(_, component)| component.storage_type == StorageType::SparseSet) - .map(|(id, _)| *id) - } - /// Returns a slice of all of the components in the archetype. /// /// All of the IDs are unique. #[inline] pub fn components(&self) -> &[ComponentId] { - self.component_ids.as_slice() + &self.component_ids } /// Gets an iterator of all of the components in the archetype. @@ -549,13 +499,13 @@ impl Archetype { /// All of the IDs are unique. #[inline] pub fn iter_components(&self) -> impl Iterator + Clone { - self.component_ids.as_slice().iter().copied() + self.component_ids.iter().copied() } /// Returns the total number of components in the archetype #[inline] pub fn component_count(&self) -> usize { - self.archetype_components.len() + self.component_ids.len() } /// Fetches an immutable reference to the archetype's [`Edges`], a cache of @@ -660,20 +610,10 @@ impl Archetype { self.entities.is_empty() } - /// Checks if the archetype contains a specific component. This runs in `O(1)` time. + /// Checks if the archetype contains a specific component. This runs in `O(N)` time. #[inline] pub fn contains(&self, component_id: ComponentId) -> bool { - self.archetype_components.contains_key(&component_id) - } - - /// Gets the type of storage where a component in the archetype can be found. - /// Returns `None` if the component is not part of the archetype. - /// This runs in `O(1)` time. - #[inline] - pub fn get_storage_type(&self, component_id: ComponentId) -> Option { - self.archetype_components - .get(&component_id) - .map(|info| info.storage_type) + self.component_ids.contains(&component_id) } /// Clears all entities from the archetype. diff --git a/crates/bevy_ecs/src/bundle/insert.rs b/crates/bevy_ecs/src/bundle/insert.rs index b0dce8a76c040..cc7622e0deea4 100644 --- a/crates/bevy_ecs/src/bundle/insert.rs +++ b/crates/bevy_ecs/src/bundle/insert.rs @@ -617,12 +617,21 @@ impl BundleInfo { // The archetype changes when we insert this bundle. Prepare the new archetype and storages. { let current_archetype = &archetypes[archetype_id]; + let (current_table_components, current_sparse_set_components): (Vec<_>, Vec<_>) = + current_archetype + .iter_components() + .partition(|&component_id| { + // SAFETY: Every component in an archetype is registered in this world. + unsafe { components.get_info_unchecked(component_id) }.storage_type() + == StorageType::Table + }); + table_components = if new_table_components.is_empty() { // If there are no new table components, we can keep using this table. table_id = current_archetype.table_id(); - current_archetype.table_components().collect() + current_table_components } else { - new_table_components.extend(current_archetype.table_components()); + new_table_components.extend(current_table_components); // Sort to ignore order while hashing. new_table_components.sort_unstable(); // SAFETY: all component ids in `new_table_components` exist @@ -636,9 +645,9 @@ impl BundleInfo { }; sparse_set_components = if new_sparse_set_components.is_empty() { - current_archetype.sparse_set_components().collect() + current_sparse_set_components } else { - new_sparse_set_components.extend(current_archetype.sparse_set_components()); + new_sparse_set_components.extend(current_sparse_set_components); // Sort to ignore order while hashing. new_sparse_set_components.sort_unstable(); new_sparse_set_components diff --git a/crates/bevy_ecs/src/bundle/remove.rs b/crates/bevy_ecs/src/bundle/remove.rs index 4e3edf9cf4516..d61f6e52817c1 100644 --- a/crates/bevy_ecs/src/bundle/remove.rs +++ b/crates/bevy_ecs/src/bundle/remove.rs @@ -238,9 +238,12 @@ impl<'w> BundleRemover<'w> { if old_archetype.contains(component_id) { world.removed_components.write(component_id, entity); + // SAFETY: `component_id` is valid because it comes from the bundle's components. + let storage_type = + unsafe { world.components.get_info_unchecked(component_id) }.storage_type(); // Make sure to drop components stored in sparse sets. // Dense components are dropped later in `move_to_and_drop_missing_unchecked`. - if let Some(StorageType::SparseSet) = old_archetype.get_storage_type(component_id) { + if storage_type == StorageType::SparseSet { world .storages .sparse_sets @@ -401,10 +404,7 @@ impl BundleInfo { // This bundle removal result is cached. Just return that! (result, false) } else { - let mut next_table_components; - let mut next_sparse_set_components; - let next_table_id; - { + let (next_table_components, next_sparse_set_components, next_table_id) = { let current_archetype = &mut archetypes[archetype_id]; let mut removed_table_components = Vec::new(); let mut removed_sparse_set_components = Vec::new(); @@ -432,15 +432,23 @@ impl BundleInfo { // Archetype components are already sorted. removed_table_components.sort_unstable(); removed_sparse_set_components.sort_unstable(); - next_table_components = current_archetype.table_components().collect(); - next_sparse_set_components = current_archetype.sparse_set_components().collect(); + + let (mut next_table_components, mut next_sparse_set_components): (Vec<_>, Vec<_>) = + current_archetype + .iter_components() + .partition(|&component_id| { + // SAFETY: Every archetype component is registered in this world. + unsafe { components.get_info_unchecked(component_id) }.storage_type() + == StorageType::Table + }); + sorted_remove(&mut next_table_components, &removed_table_components); sorted_remove( &mut next_sparse_set_components, &removed_sparse_set_components, ); - next_table_id = if removed_table_components.is_empty() { + let next_table_id = if removed_table_components.is_empty() { current_archetype.table_id() } else { // SAFETY: all components in next_table_components exist @@ -450,7 +458,13 @@ impl BundleInfo { .get_id_or_insert(&next_table_components, components) } }; - } + + ( + next_table_components, + next_sparse_set_components, + next_table_id, + ) + }; // SAFETY: // - table id was created if it doesn't exist diff --git a/crates/bevy_ecs/src/world/entity_access/world_mut.rs b/crates/bevy_ecs/src/world/entity_access/world_mut.rs index 8fe8f7208d6a4..1c2f9cd7e7bd3 100644 --- a/crates/bevy_ecs/src/world/entity_access/world_mut.rs +++ b/crates/bevy_ecs/src/world/entity_access/world_mut.rs @@ -1815,7 +1815,13 @@ impl<'w> EntityWorldMut<'w> { } table_row = remove_result.table_row; - for component_id in archetype.sparse_set_components() { + let sparse_set_components = archetype.iter_components().filter(|&id| { + // SAFETY: Every archetype component is registered in this world. + unsafe { self.world.components.get_info_unchecked(id) }.storage_type() + == StorageType::SparseSet + }); + + for component_id in sparse_set_components { // set must have existed for the component to be added. let sparse_set = self .world From 35c6b0e848e39ee285b837eb3958667ba8c90f67 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Fri, 18 Sep 2026 00:27:51 +0200 Subject: [PATCH 35/44] fix tests --- crates/bevy_ecs/src/entity_disabling.rs | 8 ++++---- crates/bevy_ecs/src/storage/sparse_set.rs | 2 +- crates/bevy_ecs/src/system/access.rs | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index 1db2c3adc0a16..b02f183589c22 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -255,9 +255,9 @@ mod tests { #[test] fn filters_modify_access() { let ids = EntityAllocator::default(); - let id_1 = ComponentId(ids.alloc()); - let id_2 = ComponentId(ids.alloc()); - let id_4 = ComponentId(ids.alloc()); + let id_1 = ComponentId::new(ids.alloc()); + let id_2 = ComponentId::new(ids.alloc()); + let id_4 = ComponentId::new(ids.alloc()); let mut filters = DefaultQueryFilters::empty(); filters.register_disabling_component(id_1); @@ -295,7 +295,7 @@ mod tests { let mut applied_access = component_access.clone(); filters.modify_access(&mut applied_access); assert_eq!( - vec![id_1, id_4], + vec![id_4, id_1], applied_access.with_filters().collect::>() ); assert_eq!(0, applied_access.without_filters().count()); diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index ab43e8dd8bcb3..85eca1b789ad5 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -915,7 +915,7 @@ mod tests { #[test] fn sparse_sets() { - let mut ids = EntityAllocator::default(); + let ids = EntityAllocator::default(); let mut sets = SparseSets::default(); #[derive(Component, Default, Debug)] diff --git a/crates/bevy_ecs/src/system/access.rs b/crates/bevy_ecs/src/system/access.rs index b2c78b714e3e6..1f608634d60f4 100644 --- a/crates/bevy_ecs/src/system/access.rs +++ b/crates/bevy_ecs/src/system/access.rs @@ -357,7 +357,7 @@ mod tests { #[test] fn check_compatibility() { - let mut ids = EntityAllocator::default(); + let ids = EntityAllocator::default(); let access_none = SystemAccess::None; let access_shared = SystemAccess::Shared({ let mut set = FilteredAccessSet::default(); @@ -381,7 +381,7 @@ mod tests { #[test] fn conflict_reporting() { - let mut ids = EntityAllocator::default(); + let ids = EntityAllocator::default(); let access_none = SystemAccess::None; let access_shared = SystemAccess::Shared({ let mut set = FilteredAccessSet::default(); @@ -450,7 +450,7 @@ mod tests { #[test] fn conversion_to_access_sets() { - let mut ids = EntityAllocator::default(); + let ids = EntityAllocator::default(); let id_1 = ComponentId::new(ids.alloc()); let access_none = SystemAccess::None; @@ -481,13 +481,13 @@ mod tests { #[test] fn extending_access() { - let mut ids = EntityAllocator::default(); + let ids = EntityAllocator::default(); let mut access = SystemAccess::default(); let access_none = SystemAccess::None; let access_shared = SystemAccess::Shared({ let mut set = FilteredAccessSet::default(); - set.add_unfiltered_component_read(ComponentId(ids.alloc())); + set.add_unfiltered_component_read(ComponentId::new(ids.alloc())); set }); let access_exclusive = SystemAccess::Exclusive; From 97fcd159498a0ae6266be34789c2fff542f2eebf Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 19 Sep 2026 19:28:37 +0200 Subject: [PATCH 36/44] remove migration guide --- .../migration-guides/component_info_id.md | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 _release-content/migration-guides/component_info_id.md diff --git a/_release-content/migration-guides/component_info_id.md b/_release-content/migration-guides/component_info_id.md deleted file mode 100644 index 9c63756b52d2b..0000000000000 --- a/_release-content/migration-guides/component_info_id.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: "ComponentInfo no longer stores the component ID" -pull_requests: [25774, 24728] ---- - -`ComponentInfo::id()` has been removed, and `ComponentInfo` no longer stores the component ID. Component IDs are already used as keys by the collections that store `ComponentInfo` instances; if you need both values, retain the `ComponentId` when -retrieving the `ComponentInfo` from the collection: - -```rust -// 0.19 -let info = components.get_info(component_id).unwrap(); -let id = info.id(); - -// 0.20 -let id = component_id; -let info = components.get_info(id).unwrap(); -``` - -Additionally, `World::iter_resources` and `World::iter_resources_mut` now return an iterator over `(ComponentId, ComponentInfo, ...)` instead of `(ComponentInfo, ...)`. From 0309e8d4b9c8b79fe4959ab99692cc37457dd877 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sat, 19 Sep 2026 19:35:30 +0200 Subject: [PATCH 37/44] fix world_serialization --- .../bevy_world_serialization/src/dynamic_world_builder.rs | 2 +- crates/bevy_world_serialization/src/world_asset.rs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/bevy_world_serialization/src/dynamic_world_builder.rs b/crates/bevy_world_serialization/src/dynamic_world_builder.rs index d53387289fbd4..eb30fa10e0035 100644 --- a/crates/bevy_world_serialization/src/dynamic_world_builder.rs +++ b/crates/bevy_world_serialization/src/dynamic_world_builder.rs @@ -378,7 +378,7 @@ impl<'w> DynamicWorldBuilder<'w> { .components() .get_valid_id(TypeId::of::()); - for component_id in self.original_world.components().iter_registered_ids() { + for (component_id, _) in self.original_world.components().iter_registered() { let entity = component_id.entity(); if !self.original_world.entities().contains_spawned(entity) { diff --git a/crates/bevy_world_serialization/src/world_asset.rs b/crates/bevy_world_serialization/src/world_asset.rs index 57e03ee393fb6..40162a4e95d1c 100644 --- a/crates/bevy_world_serialization/src/world_asset.rs +++ b/crates/bevy_world_serialization/src/world_asset.rs @@ -74,7 +74,12 @@ impl WorldAsset { .components() .get_id(TypeId::of::()); - let ids: Vec = self.world.components().iter_registered_ids().collect(); + let ids: Vec = self + .world + .components() + .iter_registered() + .map(|(id, _)| id) + .collect(); // Resources archetype for component_id in ids { let source_entity = component_id.entity(); From 43d6314df60714ed545cef832aa9b8fcd848a2e3 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 20 Sep 2026 00:30:24 +0200 Subject: [PATCH 38/44] fix clippy warnings --- crates/bevy_ecs/src/query/state.rs | 4 +++ crates/bevy_ecs/src/query/world_query.rs | 4 +++ crates/bevy_ecs/src/storage/sparse_set.rs | 29 ++++++++++------------ crates/bevy_ecs/src/system/access.rs | 4 +++ crates/bevy_ecs/src/system/system_param.rs | 4 +++ 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index a4b4f81bd0537..9bc2b24a5ba8a 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -195,6 +195,10 @@ impl QueryState { /// Adds all access from this query and any nested queries to the `component_access_set`. /// Panics if the access from this query and any nested queries conflict with each other /// or with any previous access. + #[expect( + clippy::result_large_err, + reason = "Boxing `FilteredAccessSet` adds unnecessary noise." + )] pub fn init_access( &self, component_access_set: &mut FilteredAccessSet, diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index dda8a21f4920e..ddba0a4fa6f4e 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -128,6 +128,10 @@ pub unsafe trait WorldQuery { /// /// This is used for queries to request access to entities other than the current one, /// such as to read resources or to follow relations. + #[expect( + clippy::result_large_err, + reason = "Boxing `FilteredAccessSet` adds unnecessary noise for implementors of this trait" + )] fn init_nested_access( state: &Self::State, component_access_set: &mut FilteredAccessSet, diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 85eca1b789ad5..99396f2ff431f 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -40,7 +40,10 @@ pub(crate) struct SparseArray { /// /// However, it may use a lot of excess memory if the /// values are large or the set is sparsely populated. - +#[expect( + dead_code, + reason = "ImmutableSparseArray may be used again in the future." +)] #[derive(Debug)] pub(crate) struct ImmutableSparseArray { values: Box<[Option]>, @@ -65,10 +68,6 @@ impl SparseArray { macro_rules! impl_sparse_array { ($ty:ident) => { - #[allow( - dead_code, - reason = "ImmutableSparseArray may be used again in the future." - )] impl $ty { /// Returns `true` if the collection contains a value for the specified `index`. #[inline] @@ -90,7 +89,8 @@ macro_rules! impl_sparse_array { } impl_sparse_array!(SparseArray); -impl_sparse_array!(ImmutableSparseArray); +// ImmutableSparseArray may be used again in the future. +// impl_sparse_array!(ImmutableSparseArray); impl SparseArray { /// Inserts `value` at `index` in the array. @@ -133,10 +133,6 @@ impl SparseArray { } /// Converts the [`SparseArray`] into an immutable variant. - #[allow( - dead_code, - reason = "ImmutableSparseArray may be used again in the future." - )] pub(crate) fn into_immutable(self) -> ImmutableSparseArray { ImmutableSparseArray { values: self.values.into_boxed_slice(), @@ -533,6 +529,10 @@ pub struct SparseSet { /// the dense storage of values takes less memory when `V` is large, /// although the overhead of tracking which entries have values /// may make it larger when `V` is small or the set is densely populated. +#[expect( + dead_code, + reason = "ImmutableSparseSet may be used again in the future." +)] #[derive(Debug)] pub(crate) struct ImmutableSparseSet { /// The mapping from dense index to value. @@ -551,10 +551,6 @@ pub(crate) struct ImmutableSparseSet { macro_rules! impl_sparse_set { ($ty:ident) => { - #[allow( - dead_code, - reason = "ImmutableSparseArray may be used again in the future." - )] impl $ty { /// Returns the number of elements in the sparse set. #[inline] @@ -618,7 +614,8 @@ macro_rules! impl_sparse_set { } impl_sparse_set!(SparseSet); -impl_sparse_set!(ImmutableSparseSet); +// ImmutableSparseSet may be used again in the future. +// impl_sparse_set!(ImmutableSparseSet); impl Default for SparseSet { fn default() -> Self { @@ -734,7 +731,7 @@ impl SparseSet { } /// Converts the sparse set into its immutable variant. - #[allow( + #[expect( dead_code, reason = "ImmutableSparseArray may be used again in the future." )] diff --git a/crates/bevy_ecs/src/system/access.rs b/crates/bevy_ecs/src/system/access.rs index 785df8e2e0499..d32dd31fc34cd 100644 --- a/crates/bevy_ecs/src/system/access.rs +++ b/crates/bevy_ecs/src/system/access.rs @@ -23,6 +23,10 @@ pub enum SystemAccess { Exclusive, } +#[expect( + clippy::result_large_err, + reason = "Boxing `SystemAccess` adds unnecessary noise to the various methods / APIs" +)] impl SystemAccess { /// Returns true if the system does not access the world at all, so it can run /// in parallel with any other system. diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 8345942ec458b..cee2d0b1731b9 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -398,6 +398,10 @@ unsafe impl SystemParam for Qu unsafe { QueryState::new_unchecked(world) } } + #[expect( + clippy::result_large_err, + reason = "Boxing `FilteredAccessSet` adds unnecessary noise." + )] fn init_access( state: &Self::State, _system_meta: &mut SystemMeta, From bf979584df9bae9dee6b50854b32df3cadb78b09 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 20 Sep 2026 00:58:26 +0200 Subject: [PATCH 39/44] cleaned up AI code --- .../src/inspection/component_inspection.rs | 18 ++++++++---------- .../src/inspection/world_summary.rs | 9 ++++++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/bevy_dev_tools/src/inspection/component_inspection.rs b/crates/bevy_dev_tools/src/inspection/component_inspection.rs index b59d6847405aa..e047644c397c8 100644 --- a/crates/bevy_dev_tools/src/inspection/component_inspection.rs +++ b/crates/bevy_dev_tools/src/inspection/component_inspection.rs @@ -175,10 +175,9 @@ impl ComponentMetadataMap { pub fn generate(world: &World) -> Self { let mut map = HashMap::new(); - for index in 0..world.components().num_registered() { - let component_id = ComponentId::new(index); - if let Ok(metadata) = ComponentTypeMetadata::new(world, component_id) { - map.insert(component_id, metadata); + for (id, _) in world.components().iter_registered() { + if let Ok(metadata) = ComponentTypeMetadata::new(world, id) { + map.insert(id, metadata); } } @@ -209,12 +208,11 @@ impl ComponentMetadataMap { /// Adds entries for component types that are not yet in the map, leaving existing entries alone. pub fn update(&mut self, world: &World) { - for index in 0..world.components().num_registered() { - let component_id = ComponentId::new(index); - if !self.map.contains_key(&component_id) - && let Ok(metadata) = ComponentTypeMetadata::new(world, component_id) + for (id, _) in world.components().iter_registered() { + if !self.map.contains_key(&id) + && let Ok(metadata) = ComponentTypeMetadata::new(world, id) { - self.map.insert(component_id, metadata); + self.map.insert(id, metadata); } } } @@ -449,7 +447,7 @@ mod tests { #[test] fn unregistered_component_id_returns_error() { let world = test_world(); - let component_id = ComponentId::new(usize::MAX); + let component_id = ComponentId::from_u32(464149); let result = ComponentTypeMetadata::new(&world, component_id); diff --git a/crates/bevy_dev_tools/src/inspection/world_summary.rs b/crates/bevy_dev_tools/src/inspection/world_summary.rs index 8a015d1c18e62..fed269058cf7d 100644 --- a/crates/bevy_dev_tools/src/inspection/world_summary.rs +++ b/crates/bevy_dev_tools/src/inspection/world_summary.rs @@ -2,7 +2,10 @@ //! //! See [`WorldSummary`] for the output, and [`WorldSummaryExt`] to generate. -use bevy_ecs::{archetype::ArchetypeId, component::ComponentId, system::Commands, world::World}; +use bevy_ecs::{ + archetype::ArchetypeId, component::ComponentId, storage::SparseSetIndex, system::Commands, + world::World, +}; use bevy_log::info; use bevy_utils::{memory_size::MemorySize, prelude::DebugName}; use core::{cmp::Reverse, fmt}; @@ -143,7 +146,7 @@ pub trait WorldSummaryExt { impl WorldSummaryExt for World { fn summarize(&self, settings: SummarySettings) -> WorldSummary { let total_entities = self.entities().count_spawned(); - let total_send_resources = self.resource_entities().iter().count(); + let total_send_resources = self.iter_resources().count(); let total_non_send_resources = self.storages().non_sends.len(); let total_archetypes = self.archetypes().len(); let mut archetype_summaries: Vec = self @@ -161,7 +164,7 @@ impl WorldSummaryExt for World { self.components() .get_name(*component_id) .unwrap_or_else(|| { - let component_index = component_id.index(); + let component_index = component_id.sparse_set_index(); DebugName::owned(format!("Component #{component_index}")) }) }) From be0ccf411e9324b4a006a518febb163da3674ae1 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 20 Sep 2026 01:33:11 +0200 Subject: [PATCH 40/44] fix clippy tests --- crates/bevy_ecs/src/query/access.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index fefb7716cf886..01e357a134aae 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -1188,7 +1188,7 @@ mod tests { fn generate_ids(count: u32) -> Vec { let ids = EntityAllocator::default(); - ids.alloc_many(count).map(|e| ComponentId::new(e)).collect() + ids.alloc_many(count).map(ComponentId::new).collect() } #[test] From d53700e812799d1e86ca384560d5e44c22371162 Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 20 Sep 2026 02:23:48 +0200 Subject: [PATCH 41/44] fix issues --- crates/bevy_ecs/src/archetype.rs | 3 +++ crates/bevy_ecs/src/component/required.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 33ebd0bbc30b1..7a1b350d726f6 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -428,6 +428,9 @@ impl Archetype { .or_default() .insert(id, ArchetypeRecord { column: None }); } + + component_ids.sort(); + Self { id, table_id, diff --git a/crates/bevy_ecs/src/component/required.rs b/crates/bevy_ecs/src/component/required.rs index ed5fd1965e45d..ccd2379be3aa0 100644 --- a/crates/bevy_ecs/src/component/required.rs +++ b/crates/bevy_ecs/src/component/required.rs @@ -972,7 +972,7 @@ mod tests { assert!(world.entity(e).contains::()); assert!(world.entity(e).contains::()); - world.entity_mut(e).insert(X); + world.entity_mut(e).insert(X); // fails for some reason. assert!(world.entity(e).contains::()); assert!(world.entity(e).contains::()); assert!(world.entity(e).contains::()); From 8256f1d42e7812555ecbfe325abaa087e3fd047c Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 20 Sep 2026 13:19:26 +0200 Subject: [PATCH 42/44] scatter shot debuggin --- crates/bevy_ecs/src/archetype.rs | 12 +++++++----- crates/bevy_ecs/src/component/register.rs | 2 +- crates/bevy_ecs/src/component/required.rs | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 7a1b350d726f6..fbf7d2c6a22cc 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -9,7 +9,7 @@ //! Archetypes are not to be confused with [`Table`]s. Each archetype stores its table //! components in one table, and each archetype uniquely points to one table, but multiple //! archetypes may store their table components in the same table. These archetypes -//! differ only by the [`SparseSet`] components. +//! differ only by the [`crate::storage::SparseSet`] components. //! //! Like tables, archetypes can be created but are never cleaned up. Empty archetypes are //! not removed, and persist until the world is dropped. @@ -407,7 +407,9 @@ impl Archetype { let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); observers.update_archetype_flags(component_id, &mut flags); - component_ids.push(component_id); + if !component_ids.contains(&component_id) { + component_ids.push(component_id); + } // NOTE: the `table_components` are sorted AND they were inserted in the `Table` in the same // sorted order, so the index of the `Column` in the `Table` is the same as the index of the // component in the `table_components` vector @@ -422,15 +424,15 @@ impl Archetype { let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); observers.update_archetype_flags(component_id, &mut flags); - component_ids.push(component_id); + if !component_ids.contains(&component_id) { + component_ids.push(component_id); + } component_index .entry(component_id) .or_default() .insert(id, ArchetypeRecord { column: None }); } - component_ids.sort(); - Self { id, table_id, diff --git a/crates/bevy_ecs/src/component/register.rs b/crates/bevy_ecs/src/component/register.rs index 996838ac05cb2..0001083253944 100644 --- a/crates/bevy_ecs/src/component/register.rs +++ b/crates/bevy_ecs/src/component/register.rs @@ -388,7 +388,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> { /// /// # Safety /// - /// The [`Components`] and [`RemoteAllocator`] must come from the same world. + /// The [`Components`] and [`EntityAllocator`] must come from the same world. pub unsafe fn new(components: &'w Components, allocator: &'w EntityAllocator) -> Self { Self { components, diff --git a/crates/bevy_ecs/src/component/required.rs b/crates/bevy_ecs/src/component/required.rs index ccd2379be3aa0..ed5fd1965e45d 100644 --- a/crates/bevy_ecs/src/component/required.rs +++ b/crates/bevy_ecs/src/component/required.rs @@ -972,7 +972,7 @@ mod tests { assert!(world.entity(e).contains::()); assert!(world.entity(e).contains::()); - world.entity_mut(e).insert(X); // fails for some reason. + world.entity_mut(e).insert(X); assert!(world.entity(e).contains::()); assert!(world.entity(e).contains::()); assert!(world.entity(e).contains::()); From 7bd9d4b42bf47bb6753fb586facca64ced5ccd8b Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 20 Sep 2026 19:01:12 +0200 Subject: [PATCH 43/44] fix soundness bug --- crates/bevy_ecs/src/storage/table/mod.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 915b3c268e138..af367a98724c2 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -763,21 +763,10 @@ impl Tables { // - The caller ensures that all new columns will be written to immediately. let dst_row = unsafe { dst_table.allocate(src_table.entities.swap_remove(row.index())) }; - let mut dst_iter = dst_table.columns.iter_mut().peekable(); - for (src_component_id, src_column) in src_table.columns.iter_mut() { - // Skip past any destination columns that don't exist in the source table. - // The caller is responsible for initializing those columns. - while dst_iter - .next_if(|(dst_component_id, _)| *dst_component_id < src_component_id) - .is_some() - {} - - // Then move the value in the source column if it exists in the destination table, + // Move the value in the source column if it exists in the destination table, // or remove it if it does not. - if let Some((_, dst_column)) = - dst_iter.next_if(|(dst_component_id, _)| *dst_component_id == src_component_id) - { + if let Some(dst_column) = dst_table.columns.get_mut(src_component_id) { // SAFETY: // - `src_column` and `dst_column` correspond to the same `ComponentId`. // - The caller ensures `row` is in-bounds for `src_column`. @@ -806,9 +795,6 @@ impl Tables { } } - // Need to end the mutable borrow so we can return `dst_table`. - drop(dst_iter); - TableMoveResult { new_table: dst_table, new_row: dst_row, From 0bdcb2b23fb9efc0712a56a7fffff605190b446f Mon Sep 17 00:00:00 2001 From: Trashtalk Date: Sun, 20 Sep 2026 19:40:05 +0200 Subject: [PATCH 44/44] fix test --- crates/bevy_ecs/src/archetype.rs | 10 ++++------ crates/bevy_ecs/src/storage/sparse_set.rs | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index fbf7d2c6a22cc..c91946d2cacd6 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -407,9 +407,8 @@ impl Archetype { let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); observers.update_archetype_flags(component_id, &mut flags); - if !component_ids.contains(&component_id) { - component_ids.push(component_id); - } + component_ids.push(component_id); + // NOTE: the `table_components` are sorted AND they were inserted in the `Table` in the same // sorted order, so the index of the `Column` in the `Table` is the same as the index of the // component in the `table_components` vector @@ -424,9 +423,8 @@ impl Archetype { let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); observers.update_archetype_flags(component_id, &mut flags); - if !component_ids.contains(&component_id) { - component_ids.push(component_id); - } + component_ids.push(component_id); + component_index .entry(component_id) .or_default() diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 99396f2ff431f..5dc1c4c0c7f3f 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -939,7 +939,7 @@ mod tests { .map(|(id, set)| (id, set.len())) .collect::>(); collected_sets.sort(); - assert_eq!(collected_sets, vec![(id_1, 0), (id_2, 0),]); + assert_eq!(collected_sets, vec![(id_2, 0), (id_1, 0),]); fn register_component(sets: &mut SparseSets, id: ComponentId) { let descriptor = ComponentDescriptor::new::();