Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
6a7c2e9
built the new alllocator
ElliottjPierce May 30, 2025
65a7ea9
completely redid entities
ElliottjPierce May 30, 2025
4c6f613
construct and destruct
ElliottjPierce May 30, 2025
4eab25c
finished most easy fixes
ElliottjPierce May 30, 2025
8556b19
fixed commands
ElliottjPierce May 31, 2025
f4e06ad
entity cloning and mapping
ElliottjPierce May 31, 2025
3df6ab0
fix final few errors
ElliottjPierce May 31, 2025
7b044c0
docs
ElliottjPierce May 31, 2025
5c58266
added back get_entity
ElliottjPierce May 31, 2025
01d6785
use bulk allocator
ElliottjPierce May 31, 2025
c100f9e
improve perf
ElliottjPierce May 31, 2025
ea9a3be
fixed tests
ElliottjPierce May 31, 2025
0c1c9c3
docs
ElliottjPierce May 31, 2025
5a69ebf
guard against arbitrary constructions
ElliottjPierce May 31, 2025
019c154
add world-level destruction
ElliottjPierce May 31, 2025
58ee663
remove no longer needed flush
ElliottjPierce May 31, 2025
85b0d03
entity refs can have no location
ElliottjPierce May 31, 2025
4844dda
fix error
ElliottjPierce May 31, 2025
7e39f9d
much, much better error handling
ElliottjPierce May 31, 2025
9cce28f
fix docs
ElliottjPierce May 31, 2025
b415dc8
small perf improvement
ElliottjPierce May 31, 2025
38a9710
fix scenes test
ElliottjPierce May 31, 2025
8956adc
fix doc tests for errors
ElliottjPierce May 31, 2025
0c194b7
doc improvements
ElliottjPierce May 31, 2025
180b349
all destructs increment the generation
ElliottjPierce May 31, 2025
20a6ee3
fix warning
ElliottjPierce May 31, 2025
068a2a9
fix doc
ElliottjPierce May 31, 2025
763877f
fix test
ElliottjPierce May 31, 2025
9baf15a
fixed potential bug
ElliottjPierce May 31, 2025
63effde
add migration guide
ElliottjPierce May 31, 2025
7967807
fix command doc
ElliottjPierce May 31, 2025
866476e
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce May 31, 2025
531658c
handle despawn better
ElliottjPierce Jun 1, 2025
d899b38
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jun 11, 2025
eabc340
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jun 15, 2025
9df7a76
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jun 16, 2025
6a596cb
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jun 19, 2025
5ca8e38
fix new system param
ElliottjPierce Jun 19, 2025
91f439c
small clarity updates to migration guide
ElliottjPierce Jun 20, 2025
9d86e69
Full module docs
ElliottjPierce Jun 20, 2025
d86d241
More improved entity comments
ElliottjPierce Jun 21, 2025
d0ed587
Better command docs phrasing
ElliottjPierce Jun 21, 2025
80479d0
better command docs
ElliottjPierce Jun 21, 2025
185a6de
better construct/destruct vs spawn/despawn docs
ElliottjPierce Jun 21, 2025
4f956bd
fix ci
ElliottjPierce Jun 21, 2025
6bbfee8
Apply suggestions from code review
ElliottjPierce Jun 23, 2025
b543a4a
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jun 23, 2025
10f11ba
make the new `entity_count` more precise
ElliottjPierce Jun 23, 2025
e78deec
better docs
ElliottjPierce Jun 23, 2025
5422813
improved docs per discord discussion
ElliottjPierce Jun 23, 2025
e1b5386
remove all uses of "conceptual entity"
ElliottjPierce Jun 23, 2025
271c189
Apply suggestions from code review
ElliottjPierce Jul 2, 2025
9de86df
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jul 5, 2025
0b3da34
fixed merge conflicts hopefully
ElliottjPierce Jul 5, 2025
fb897d2
hopefully fix CI
ElliottjPierce Jul 5, 2025
1b6f327
Tried to clarify doc comment some more
ElliottjPierce Jul 5, 2025
5085d4f
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jul 8, 2025
20386ff
Apply suggestions from code review
ElliottjPierce Jul 9, 2025
4c0d752
More suggestions from review
ElliottjPierce Jul 9, 2025
ab0856b
Update crates/bevy_ecs/src/entity/mod.rs
ElliottjPierce Jul 9, 2025
71675a3
fix rename
ElliottjPierce Jul 9, 2025
fdf1b55
various improvements per review
ElliottjPierce Jul 9, 2025
f9b26d6
use usize for new allocator cursor
ElliottjPierce Jul 9, 2025
e129d77
Apply suggestions from code review
ElliottjPierce Jul 10, 2025
d42d9ea
tighten up expects to save binary size.
ElliottjPierce Jul 10, 2025
bac7bd2
rename update and declare
ElliottjPierce Jul 10, 2025
e9df300
more allocator doc comments
ElliottjPierce Jul 10, 2025
3fcbb24
Include more lifecycle info in storage docs
ElliottjPierce Jul 10, 2025
d0ca8b1
Merge branch 'Remove-entity-reserving/pending/flushing-system' of htt…
ElliottjPierce Jul 10, 2025
4bb74b2
fix doc
ElliottjPierce Jul 10, 2025
deb6b4d
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jul 10, 2025
5a058c0
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Jul 17, 2025
e73f7bf
hopefully finished merge
ElliottjPierce Jul 17, 2025
3062966
Update release-content/migration-guides/entities_apis.md
ElliottjPierce Oct 9, 2025
01aa792
Merge branch 'Remove-entity-reserving/pending/flushing-system' of htt…
ElliottjPierce Oct 22, 2025
caf0581
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Oct 22, 2025
9c56341
fixed merge conflicts
ElliottjPierce Oct 22, 2025
9981716
fixed bugs from merge
ElliottjPierce Oct 22, 2025
762c65b
fixed ci
ElliottjPierce Oct 22, 2025
b743a44
Doc improvements
ElliottjPierce Oct 23, 2025
2add4ca
all entity pointers assume they are constructed.
ElliottjPierce Oct 23, 2025
97047a9
simplified the despawn error
ElliottjPierce Oct 23, 2025
fedc90a
fix ci
ElliottjPierce Oct 23, 2025
ecf617f
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Oct 23, 2025
6d12925
The grand renaming
ElliottjPierce Oct 25, 2025
45780c2
fix ci please
ElliottjPierce Oct 25, 2025
57816f7
fix ci pretty please
ElliottjPierce Oct 25, 2025
5c69793
hopefully fixed ci
ElliottjPierce Oct 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ impl EntityCountDiagnosticsPlugin {

/// Updates entity count measurement.
pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) {
diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.len() as f64);
diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.count_spawned() as f64);
}
}
22 changes: 11 additions & 11 deletions crates/bevy_ecs/src/bundle/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,9 @@ impl<'w> BundleInserter<'w> {
if let Some(swapped_entity) = result.swapped_entity {
let swapped_location =
// SAFETY: If the swap was successful, swapped_entity must be valid.
unsafe { entities.get(swapped_entity).debug_checked_unwrap() };
entities.set(
swapped_entity.index(),
unsafe { entities.get_spawned(swapped_entity).debug_checked_unwrap() };
entities.update_existing_location(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: location.archetype_row,
Expand All @@ -243,7 +243,7 @@ impl<'w> BundleInserter<'w> {
);
}
let new_location = new_archetype.allocate(entity, result.table_row);
entities.set(entity.index(), Some(new_location));
entities.update_existing_location(entity.row(), Some(new_location));
bundle_info.write_components(
table,
sparse_sets,
Expand Down Expand Up @@ -280,9 +280,9 @@ impl<'w> BundleInserter<'w> {
if let Some(swapped_entity) = result.swapped_entity {
let swapped_location =
// SAFETY: If the swap was successful, swapped_entity must be valid.
unsafe { entities.get(swapped_entity).debug_checked_unwrap() };
entities.set(
swapped_entity.index(),
unsafe { entities.get_spawned(swapped_entity).debug_checked_unwrap() };
entities.update_existing_location(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: location.archetype_row,
Expand All @@ -295,16 +295,16 @@ impl<'w> BundleInserter<'w> {
// redundant copies
let move_result = table.move_to_superset_unchecked(result.table_row, new_table);
let new_location = new_archetype.allocate(entity, move_result.new_row);
entities.set(entity.index(), Some(new_location));
entities.update_existing_location(entity.row(), Some(new_location));

// If an entity was moved into this entity's table spot, update its table row.
if let Some(swapped_entity) = move_result.swapped_entity {
let swapped_location =
// SAFETY: If the swap was successful, swapped_entity must be valid.
unsafe { entities.get(swapped_entity).debug_checked_unwrap() };
unsafe { entities.get_spawned(swapped_entity).debug_checked_unwrap() };

entities.set(
swapped_entity.index(),
entities.update_existing_location(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: swapped_location.archetype_row,
Expand Down
16 changes: 9 additions & 7 deletions crates/bevy_ecs/src/bundle/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,10 @@ impl<'w> BundleRemover<'w> {
.swap_remove(location.archetype_row);
// if an entity was moved into this entity's archetype row, update its archetype row
if let Some(swapped_entity) = remove_result.swapped_entity {
let swapped_location = world.entities.get(swapped_entity).unwrap();
let swapped_location = world.entities.get_spawned(swapped_entity).unwrap();

world.entities.set(
swapped_entity.index(),
world.entities.update_existing_location(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: location.archetype_row,
Expand Down Expand Up @@ -269,10 +269,10 @@ impl<'w> BundleRemover<'w> {

// if an entity was moved into this entity's table row, update its table row
if let Some(swapped_entity) = move_result.swapped_entity {
let swapped_location = world.entities.get(swapped_entity).unwrap();
let swapped_location = world.entities.get_spawned(swapped_entity).unwrap();

world.entities.set(
swapped_entity.index(),
world.entities.update_existing_location(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: swapped_location.archetype_row,
Expand All @@ -294,7 +294,9 @@ impl<'w> BundleRemover<'w> {

// SAFETY: The entity is valid and has been moved to the new location already.
unsafe {
world.entities.set(entity.index(), Some(new_location));
world
.entities
.update_existing_location(entity.row(), Some(new_location));
}

(new_location, pre_remove_result)
Expand Down
27 changes: 10 additions & 17 deletions crates/bevy_ecs/src/bundle/spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ use bevy_ptr::{ConstNonNull, MovingPtr};
use crate::{
archetype::{Archetype, ArchetypeCreated, ArchetypeId, SpawnBundleStatus},
bundle::{Bundle, BundleId, BundleInfo, DynamicBundle, InsertMode},
change_detection::MaybeLocation,
change_detection::Tick,
entity::{Entities, Entity, EntityLocation},
change_detection::{MaybeLocation, Tick},
entity::{EntitiesAllocator, Entity, EntityLocation},
event::EntityComponentsTrigger,
lifecycle::{Add, Insert, ADD, INSERT},
relationship::RelationshipHookMode,
Expand Down Expand Up @@ -89,7 +88,7 @@ impl<'w> BundleSpawner<'w> {
/// [`apply_effect`]: crate::bundle::DynamicBundle::apply_effect
#[inline]
#[track_caller]
pub unsafe fn spawn_non_existent<T: DynamicBundle>(
pub unsafe fn spawn_at<T: DynamicBundle>(
&mut self,
entity: Entity,
bundle: MovingPtr<'_, T>,
Expand Down Expand Up @@ -120,8 +119,8 @@ impl<'w> BundleSpawner<'w> {
InsertMode::Replace,
caller,
);
entities.set(entity.index(), Some(location));
entities.mark_spawn_despawn(entity.index(), caller, self.change_tick);
entities.set_location(entity.row(), Some(location));
entities.mark_spawned_or_despawned(entity.row(), caller, self.change_tick);
location
};

Expand Down Expand Up @@ -186,22 +185,16 @@ impl<'w> BundleSpawner<'w> {
bundle: MovingPtr<'_, T>,
caller: MaybeLocation,
) -> Entity {
let entity = self.entities().alloc();
// SAFETY:
// - `entity` is allocated above
// - The caller ensures that `T` matches this `BundleSpawner`'s type.
// - The caller ensures that if `T::Effect: !NoBundleEffect.`, then [`apply_effect`] must be called exactly once on `bundle`
// after this function returns before returning to safe code.
// - The caller ensures that the value pointed to by `bundle` must not be accessed for anything other than [`apply_effect`]
// or dropped.
unsafe { self.spawn_non_existent::<T>(entity, bundle, caller) };
let entity = self.allocator().alloc();
// SAFETY: entity is allocated (but non-existent), `T` matches this BundleInfo's type
let _ = unsafe { self.spawn_at(entity, bundle, caller) };
entity
}

#[inline]
pub(crate) fn entities(&mut self) -> &mut Entities {
pub(crate) fn allocator(&mut self) -> &'w mut EntitiesAllocator {
// SAFETY: No outstanding references to self.world, changes to entities cannot invalidate our internal pointers
unsafe { &mut self.world.world_mut().entities }
unsafe { &mut self.world.world_mut().allocator }
}

/// # Safety
Expand Down
39 changes: 22 additions & 17 deletions crates/bevy_ecs/src/entity/clone_entities.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
use alloc::{boxed::Box, collections::VecDeque, vec::Vec};
use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet};
use bevy_ptr::{Ptr, PtrMut};
use bevy_utils::prelude::DebugName;
use bumpalo::Bump;
use core::{any::TypeId, cell::LazyCell, ops::Range};
use derive_more::derive::From;

use crate::{
archetype::Archetype,
bundle::{Bundle, BundleRemover, InsertMode},
change_detection::MaybeLocation,
component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo},
entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper},
entity::{hash_map::EntityHashMap, EntitiesAllocator, Entity, EntityMapper},
query::DebugCheckedUnwrap,
relationship::RelationshipHookMode,
world::World,
};
use alloc::{boxed::Box, collections::VecDeque, vec::Vec};
use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet};
use bevy_ptr::{Ptr, PtrMut};
use bevy_utils::prelude::DebugName;
use bumpalo::Bump;
use core::{any::TypeId, cell::LazyCell, ops::Range};
use derive_more::From;

/// Provides read access to the source component (the component being cloned) in a [`ComponentCloneFn`].
pub struct SourceComponent<'a> {
Expand Down Expand Up @@ -80,7 +79,7 @@ pub struct ComponentCloneCtx<'a, 'b> {
target_component_moved: bool,
bundle_scratch: &'a mut BundleScratch<'b>,
bundle_scratch_allocator: &'b Bump,
entities: &'a Entities,
allocator: &'a EntitiesAllocator,
source: Entity,
target: Entity,
component_info: &'a ComponentInfo,
Expand All @@ -106,7 +105,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> {
target: Entity,
bundle_scratch_allocator: &'b Bump,
bundle_scratch: &'a mut BundleScratch<'b>,
entities: &'a Entities,
allocator: &'a EntitiesAllocator,
component_info: &'a ComponentInfo,
entity_cloner: &'a mut EntityClonerState,
mapper: &'a mut dyn EntityMapper,
Expand All @@ -121,7 +120,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> {
target_component_written: false,
target_component_moved: false,
bundle_scratch_allocator,
entities,
allocator,
mapper,
component_info,
state: entity_cloner,
Expand Down Expand Up @@ -279,7 +278,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> {

/// Queues the `entity` to be cloned by the current [`EntityCloner`]
pub fn queue_entity_clone(&mut self, entity: Entity) {
let target = self.entities.reserve_entity();
let target = self.allocator.alloc();
self.mapper.set_mapped(entity, target);
self.state.clone_queue.push_back(entity);
}
Expand Down Expand Up @@ -565,14 +564,21 @@ impl EntityCloner {
relationship_hook_insert_mode: RelationshipHookMode,
) -> Entity {
let target = mapper.get_mapped(source);
// The target may need to be constructed if it hasn't been already.
// If this fails, it either didn't need to be constructed (ok) or doesn't exist (caught better later).
let _ = world.spawn_at_empty(target);

// PERF: reusing allocated space across clones would be more efficient. Consider an allocation model similar to `Commands`.
let bundle_scratch_allocator = Bump::new();
let mut bundle_scratch: BundleScratch;
let mut moved_components: Vec<ComponentId> = Vec::new();
let mut deferred_cloned_component_ids: Vec<ComponentId> = Vec::new();
{
let world = world.as_unsafe_world_cell();
let source_entity = world.get_entity(source).expect("Source entity must exist");
let source_entity = world
.get_entity(source)
.expect("Source entity must exist constructed");
let source_archetype = source_entity.archetype();

#[cfg(feature = "bevy_reflect")]
// SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone
Expand All @@ -585,13 +591,12 @@ impl EntityCloner {
#[cfg(not(feature = "bevy_reflect"))]
let app_registry = Option::<()>::None;

let source_archetype = source_entity.archetype();
bundle_scratch = BundleScratch::with_capacity(source_archetype.component_count());

let target_archetype = LazyCell::new(|| {
world
.get_entity(target)
.expect("Target entity must exist")
.expect("Target entity must exist constructed")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the "(caught better later)" mentioned in a comment further up? Is this considered to be better because the alternative would be panicking above and unsafely unwrap here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the two expects are. Before, this didn't worry about the unconstructed/null case. So the additional construct and expect lines just let us continue in that assumption for the rest of this.

.archetype()
});

Expand Down Expand Up @@ -641,7 +646,7 @@ impl EntityCloner {
target,
&bundle_scratch_allocator,
&mut bundle_scratch,
world.entities(),
world.entities_allocator(),
info,
state,
mapper,
Expand Down
42 changes: 12 additions & 30 deletions crates/bevy_ecs/src/entity/map_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ use super::EntityIndexSet;
pub trait MapEntities {
/// Updates all [`Entity`] references stored inside using `entity_mapper`.
///
/// Implementors should look up any and all [`Entity`] values stored within `self` and
/// Implementers should look up any and all [`Entity`] values stored within `self` and
/// update them to the mapped values via `entity_mapper`.
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E);
}
Expand Down Expand Up @@ -202,7 +202,7 @@ impl<T: MapEntities, A: smallvec::Array<Item = T>> MapEntities for SmallVec<A> {
///
/// More generally, this can be used to map [`Entity`] references between any two [`Worlds`](World).
///
/// This is used by [`MapEntities`] implementors.
/// This is used by [`MapEntities`] implementers.
///
/// ## Example
///
Expand Down Expand Up @@ -336,14 +336,10 @@ impl<'m> SceneEntityMapper<'m> {
}

/// Creates a new [`SceneEntityMapper`], spawning a temporary base [`Entity`] in the provided [`World`]
pub fn new(map: &'m mut EntityHashMap<Entity>, world: &mut World) -> Self {
// We're going to be calling methods on `Entities` that require advance
// flushing, such as `alloc` and `free`.
world.flush_entities();
pub fn new(map: &'m mut EntityHashMap<Entity>, world: &World) -> Self {
Self {
map,
// SAFETY: Entities data is kept in a valid state via `EntityMapper::world_scope`
dead_start: unsafe { world.entities_mut().alloc() },
dead_start: world.allocator.alloc(),
generations: 0,
}
}
Expand All @@ -352,10 +348,13 @@ impl<'m> SceneEntityMapper<'m> {
/// [`Entity`] while reserving extra generations. Because this makes the [`SceneEntityMapper`] unable to
/// safely allocate any more references, this method takes ownership of `self` in order to render it unusable.
pub fn finish(self, world: &mut World) {
// SAFETY: Entities data is kept in a valid state via `EntityMap::world_scope`
let entities = unsafe { world.entities_mut() };
assert!(entities.free(self.dead_start).is_some());
assert!(entities.reserve_generations(self.dead_start.index(), self.generations));
// SAFETY: We never constructed the entity and never released it for something else to construct.
let reuse_row = unsafe {
world
.entities
.mark_free(self.dead_start.row(), self.generations)
};
world.allocator.free(reuse_row);
}

/// Creates an [`SceneEntityMapper`] from a provided [`World`] and [`EntityHashMap<Entity>`], then calls the
Expand Down Expand Up @@ -388,7 +387,7 @@ mod tests {
fn entity_mapper() {
let mut map = EntityHashMap::default();
let mut world = World::new();
let mut mapper = SceneEntityMapper::new(&mut map, &mut world);
let mut mapper = SceneEntityMapper::new(&mut map, &world);

let mapped_ent = Entity::from_raw_u32(1).unwrap();
let dead_ref = mapper.get_mapped(mapped_ent);
Expand Down Expand Up @@ -431,21 +430,4 @@ mod tests {
.cmp_approx(&dead_ref.generation())
.is_gt());
}

#[test]
fn entity_mapper_no_panic() {
let mut world = World::new();
// "Dirty" the `Entities`, requiring a flush afterward.
world.entities.reserve_entity();
assert!(world.entities.needs_flush());

// Create and exercise a SceneEntityMapper - should not panic because it flushes
// `Entities` first.
SceneEntityMapper::world_scope(&mut Default::default(), &mut world, |_, m| {
m.get_mapped(Entity::PLACEHOLDER);
});

// The SceneEntityMapper should leave `Entities` in a flushed state.
assert!(!world.entities.needs_flush());
}
}
Loading