Skip to content
Open
Show file tree
Hide file tree
Changes from 45 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
2 changes: 1 addition & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1582,7 +1582,7 @@ mod tests {
app.add_systems(EnterMainMenu, (foo, bar));

app.world_mut().run_schedule(EnterMainMenu);
assert_eq!(app.world().entities().len(), 2);
assert_eq!(app.world().entities().count_constructed(), 2);
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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_constructed() as f64);
}
}
75 changes: 44 additions & 31 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ use crate::{
Component, ComponentId, Components, ComponentsRegistrator, RequiredComponentConstructor,
RequiredComponents, StorageType, Tick,
},
entity::{Entities, Entity, EntityLocation},
entity::{Entities, EntitiesAllocator, Entity, EntityLocation},
lifecycle::{ADD, INSERT, REMOVE, REPLACE},
observer::Observers,
prelude::World,
Expand Down Expand Up @@ -1248,9 +1248,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_constructed(swapped_entity).debug_checked_unwrap() };
entities.update(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: location.archetype_row,
Expand All @@ -1260,7 +1260,7 @@ impl<'w> BundleInserter<'w> {
);
}
let new_location = new_archetype.allocate(entity, result.table_row);
entities.set(entity.index(), Some(new_location));
entities.update(entity.row(), Some(new_location));
let after_effect = bundle_info.write_components(
table,
sparse_sets,
Expand Down Expand Up @@ -1297,9 +1297,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_constructed(swapped_entity).debug_checked_unwrap() };
entities.update(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: location.archetype_row,
Expand All @@ -1312,16 +1312,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(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_constructed(swapped_entity).debug_checked_unwrap() };

entities.set(
swapped_entity.index(),
entities.update(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: swapped_location.archetype_row,
Expand Down Expand Up @@ -1637,10 +1637,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_constructed(swapped_entity).unwrap();

world.entities.set(
swapped_entity.index(),
world.entities.update(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: location.archetype_row,
Expand Down Expand Up @@ -1678,10 +1678,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_constructed(swapped_entity).unwrap();

world.entities.set(
swapped_entity.index(),
world.entities.update(
swapped_entity.row(),
Some(EntityLocation {
archetype_id: swapped_location.archetype_id,
archetype_row: swapped_location.archetype_row,
Expand All @@ -1703,7 +1703,7 @@ 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(entity.row(), Some(new_location));
}

(new_location, pre_remove_result)
Expand Down Expand Up @@ -1778,10 +1778,14 @@ impl<'w> BundleSpawner<'w> {
}

/// # Safety
/// `entity` must be allocated (but non-existent), `T` must match this [`BundleInfo`]'s type
/// `T` must match this [`BundleInfo`]'s type
///
/// # Panics
///
/// Panics if the entity has already been constructed.
#[inline]
#[track_caller]
pub unsafe fn spawn_non_existent<T: DynamicBundle>(
pub unsafe fn construct<T: DynamicBundle>(
&mut self,
entity: Entity,
bundle: T,
Expand Down Expand Up @@ -1812,8 +1816,17 @@ impl<'w> BundleSpawner<'w> {
InsertMode::Replace,
caller,
);
entities.set(entity.index(), Some(location));
entities.mark_spawn_despawn(entity.index(), caller, self.change_tick);

let was_at = entities.declare(entity.row(), Some(location));
// We need to assert here.
// Even if we can ensure that this entity is fresh from an allocator,
// it does not prevent users constructing arbitrary rows, which may overlap with the allocator.
// One alternative would be making `Entity` creation unsafe, but this is a good safety net anyway.
assert!(
was_at.is_none(),
"Constructing an {entity} that was already constructed is not allowed."
);
entities.mark_construct_or_destruct(entity.row(), caller, self.change_tick);
(location, after_effect)
};

Expand Down Expand Up @@ -1858,6 +1871,12 @@ impl<'w> BundleSpawner<'w> {
(location, after_effect)
}

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

/// # Safety
/// `T` must match this [`BundleInfo`]'s type
#[inline]
Expand All @@ -1866,18 +1885,12 @@ impl<'w> BundleSpawner<'w> {
bundle: T,
caller: MaybeLocation,
) -> (Entity, T::Effect) {
let entity = self.entities().alloc();
let entity = self.allocator().alloc();
// SAFETY: entity is allocated (but non-existent), `T` matches this BundleInfo's type
let (_, after_effect) = unsafe { self.spawn_non_existent(entity, bundle, caller) };
let (_, after_effect) = unsafe { self.construct(entity, bundle, caller) };
(entity, after_effect)
}

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

/// # Safety
/// - `Self` must be dropped after running this function as it may invalidate internal pointers.
#[inline]
Expand Down
58 changes: 32 additions & 26 deletions crates/bevy_ecs/src/entity/clone_entities.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, vec::Vec};
use bevy_platform::collections::{HashMap, HashSet};
use bevy_ptr::{Ptr, PtrMut};
use bevy_utils::prelude::DebugName;
use bumpalo::Bump;
use core::any::TypeId;

use crate::{
archetype::Archetype,
bundle::Bundle,
component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo},
entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper},
entity::{hash_map::EntityHashMap, Entity, EntityMapper},
query::DebugCheckedUnwrap,
relationship::RelationshipHookMode,
world::World,
};
use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, vec::Vec};
use bevy_platform::collections::{HashMap, HashSet};
use bevy_ptr::{Ptr, PtrMut};
use bevy_utils::prelude::DebugName;
use bumpalo::Bump;
use core::any::TypeId;

use super::EntitiesAllocator;

/// Provides read access to the source component (the component being cloned) in a [`ComponentCloneFn`].
pub struct SourceComponent<'a> {
Expand Down Expand Up @@ -77,7 +78,7 @@ pub struct ComponentCloneCtx<'a, 'b> {
target_component_written: 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 @@ -103,7 +104,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 EntityCloner,
mapper: &'a mut dyn EntityMapper,
Expand All @@ -117,7 +118,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> {
bundle_scratch,
target_component_written: false,
bundle_scratch_allocator,
entities,
allocator,
mapper,
component_info,
entity_cloner,
Expand Down Expand Up @@ -270,7 +271,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.entity_cloner.clone_queue.push_back(entity);
}
Expand Down Expand Up @@ -348,6 +349,8 @@ pub struct EntityCloner {
move_components: bool,
linked_cloning: bool,
default_clone_fn: ComponentCloneFn,
/// Represents a queue of entities to clone.
/// These will have targets in the entity map, which will need to be constructed.
clone_queue: VecDeque<Entity>,
deferred_commands: VecDeque<Box<dyn FnOnce(&mut World, &mut dyn EntityMapper)>>,
}
Expand Down Expand Up @@ -458,18 +461,24 @@ 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.construct_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 world = world.as_unsafe_world_cell();
let source_entity = world.get_entity(source).expect("Source entity must exist");
let target_archetype = (!self.filter_required.is_empty()).then(|| {
world
.get_entity(target)
.expect("Target entity must exist")
.archetype()
});
let target_archetype = (!self.filter_required.is_empty())
.then(|| {
world
.get_entity(target)
.expect("Target entity must exist")
.archetype()
})
.flatten();

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

let archetype = source_entity.archetype();
let Some(archetype) = source_entity.archetype() else {
// If the source has no archetype, there is nothing to clone.
return target;
};
bundle_scratch = BundleScratch::with_capacity(archetype.component_count());

for component in archetype.components() {
Expand Down Expand Up @@ -523,7 +535,7 @@ impl EntityCloner {
target,
&bundle_scratch_allocator,
&mut bundle_scratch,
world.entities(),
world.entities_allocator(),
info,
self,
mapper,
Expand All @@ -535,16 +547,10 @@ impl EntityCloner {
}
}

world.flush();

for deferred in self.deferred_commands.drain(..) {
(deferred)(world, mapper);
}

if !world.entities.contains(target) {
panic!("Target entity does not exist");
}

if self.move_components {
world
.entity_mut(source)
Expand Down
Loading