Skip to content
Merged
Show file tree
Hide file tree
Changes from 79 commits
Commits
Show all changes
104 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
d8dac49
fixed most reviewsuggestions
ElliottjPierce Oct 29, 2025
26eb50c
fixed missed construction naming
ElliottjPierce Oct 29, 2025
a16cafd
fix ci
ElliottjPierce Oct 29, 2025
ac34213
made `despawn` not assume allocation responsibility
ElliottjPierce Oct 30, 2025
be9f104
remove documentation
ElliottjPierce Oct 30, 2025
f525517
use index terms instead of row terms
ElliottjPierce Oct 30, 2025
2abdf9e
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Oct 30, 2025
4d074e4
fixed merge issue
ElliottjPierce Oct 30, 2025
cc71f17
fix typos from find and replace
ElliottjPierce Oct 30, 2025
46e370b
fix row migrattion
ElliottjPierce Oct 30, 2025
3781e72
cleaned up despawn changes
ElliottjPierce Oct 30, 2025
3d766ae
Errors _will_ happen under certain conditions, they won't _maybe_ hap…
cart Oct 30, 2025
9ecbb2d
Remove "null" entity terminology
cart Oct 30, 2025
dd28156
Remove "spreadsheet" section and expand entity id lifecycle section
cart Oct 30, 2025
46b6b83
Merge branch 'main' into Remove-entity-reserving/pending/flushing-system
ElliottjPierce Nov 3, 2025
ffb495d
Renaming missed row() -> index()
cart Nov 3, 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_constructed() 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_constructed(swapped_entity).debug_checked_unwrap() };
entities.update_location(
swapped_entity.row(),
Copy link
Member

Choose a reason for hiding this comment

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

I do indeed think this should be index and not row. Entities is not a Table functionally. It is an array. Yes every array in a certain light could be considered to be a single column table. But that is not how we approach that in the context of Bevy or Rust. Even in the context of Bevy ECS specifically I find it mismatched, as Table is a specific thing, and Entities is not that thing. I'd prefer to fix this everywhere in this PR, rather than spread the row terminology further.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I have lots to say here. If you want to unilaterally declare "no rows", I understand and will do that, but before I do that, I just want to explain myself a little bit here, because I much prefer row to index.

You mentioned that using row names makes it hard to untie it from Table storage. That's fair. I don't have a good answer for that.

But, in the very common "ecs as a spreadsheet" example, this makes a lot of sense. For people learning the ecs way, I think this name is much more approachable than index. Index naming ties the high level concept to the implementation of the Entities collection.

And that implementation is not unlikely to change. For example, with entity paging, this "index" becomes a key into a 2-layer array map. You mentioned elsewhere, I assume for the sake of argument, that Entity might become a UUID, and that should be possible without needing to change names. In that case, the EntityRow would be the key in a hashmap, not an index in an array.

I guess what I'm saying is that using the index name ties the name to its implementation details, an index in an array, rather than the high-level concept, a row in a spreadsheet. And that seems contrary to your motivations behind other naming suggestions, most of which I agree with.

Again, I'll happily rename everything to index. I just don't want that to come back to bite us latter if we do entity paging, for example, and people start wondering "Why is this called index? It's not an index into anything.".

What do you think?

Copy link
Member

@cart cart Oct 29, 2025

Choose a reason for hiding this comment

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

I remain unconvinced. The "ECS is just tables" statement is perhaps a helpful lie, but it is not the truth of the matter. Bevy ECS is partially tables and partially other things, and that will continue to be true. Given that it is partially real tables, pretending everything is tables makes the internals unnecessarily confusing, and it makes communicating how the system actually works to developers more challenging. That being said, even if we were to choose to tell that lie, I'll assert that this naming scheme is doing it wrong.

First: what is a "row" in a table? It is a specific entry in the table. A "row identifier" is a unique name for the row that can be stored (ex: the "first" row might be row A, row 0, or row 1, depending on the naming scheme). In our case, our naming scheme is "array indices". If we are storing the "row identifier", we are storing the array index of the row. If I have a field or type called row, I expect it to be the "row identifier".

Calling it Entity::row, makes no sense because it is not the row identifier. It is a unique index that identifies the entity, not the row it is stored in. The row an entity is stored in can be constantly changing, while the entity's index remains unchanged. That index points to a location in an array that stores arbitrary information about that entity, which includes the archetype it is stored in, the table, etc. There is a world where that entity metadata stores multiple table rows, if we decide to support grouping some sets of components into separate tables (for performance reasons ... Sander and I have discussed this at various points and Flecs might actually already support it).

Even in the "ECS is just tables" / "ECS is just a database" world, Entity::index would be the "primary key" stored as a column in a row, not the "row":

Table

Entity ComponentA ComponentB
4 A("hello") B("world")
9 A("foo") B("bar")

Sparse Set

Entity ComponentC
9 C("I'm sparsely stored!")

Where that primary key then has an acceleration structure to find it's locations (filling exactly the same role that Entities fills)

The index name ties the name to its implementation details
Again, I'll happily rename everything to index. I just don't want that to come back to bite us latter if we do entity paging, for example, and people start wondering "Why is this called index? It's not an index into anything.".

I agree that index is an implementation detail. If someone is relying on Entity::index, they are relying on a Bevy ECS implementation detail. If we change that to include paging, or to use a UUID, the consumer of index in many cases will need to contend with that. A contract (and perhaps naming) change is warranted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similarly to construct vs spawn_at, I still very much disagree here but am also ok with you're decision.

For one, I think you're conflating tables + sparse sets as implementation and ecs as a database with "think of an ecs as a spreadsheet." The row in the spreadsheet has nothing to do with how the spreadsheet is represented in memory or how it is implemented. The "row in a spreadsheet" is purely allegorical to help users. I think it is the most common mental framework for thinking of an ecs (again, not in implementation but in concept). At least that's how I was introduced to it and still think about ecs philosophy. I think that's very common, and is the typical mental framework that users are coming from. The rest of the ecs is just figuring out how to make reading the spreadsheet and finding the components of a row in the spreadsheet faster than it has any right to be.

I'm also still concerned about tying the names to implementation details instead of higher level concepts, especially when you've argued for the exact opposite for other names. (I don't mean to attack you here. But I am confused.) Those names were more user-focused. Maybe that's the difference? IDK. Maybe I'm not understanding, but what's confusing me a little is that I'm not sure what criteria you're using to determine implementation-centered names vs concept-centered names. And that makes it much harder (for me) to follow your logic.

Anyway, I don't think I'm going to convince you, and that's ok.

If I understand correctly, you want this name in particular to reflect how Entities is implemented. And that means renaming to index for now and then later, with entity paging, maybe renaming again to "key" or "entry" or something.

Assuming that's correct, I'll finish this up either latter tonight or tomorrow. But do let me know otherwise. And I'd also still like some help (just for future reference and my own learning) understanding the criteria you're using to determine implementation-centered names vs concept-centered names. I don't have a good mental picture for that.

Copy link
Member

Choose a reason for hiding this comment

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

I'm also still concerned about tying the names to implementation details instead of higher level concepts,
I'd also still like some help (just for future reference and my own learning) understanding the criteria you're using to determine implementation-centered names vs concept-centered names

The distinction is that Entity and Entity::index are at different levels of abstraction, and abstractions have to end somewhere. From my perspective, Entity should be an opaque unique primary key, which people can use as such without worrying about whats going on inside. The internals are an implementation detail, and therefore benefit from being functionally descriptive. People can reach in to look at what is inside, but they are just "internals", and generally should not be depended in user code.

This is of course an art and not a science. We could decide to abstract over the internals of Entity, with the goal to make them stable. But I don't really see the point of doing that. The cost of functional clarity and additional layers of abstraction isn't worth the stability, at that level, as we already have a higher level abstraction providing that stability.

I still very much disagree here but am also ok with you're decision.

Cool lets move forward with the "index' terminology, and cut the "ecs as spreadsheets" section for now. I'm not fully against using the "ECS is like a spreadsheet" angle as a teaching tool, but I still strongly object to the Entity == Row angle, and I really don't want to block this PR on that conversation.

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_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_constructed(swapped_entity).debug_checked_unwrap() };
entities.update_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_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_constructed(swapped_entity).debug_checked_unwrap() };

entities.set(
swapped_entity.index(),
entities.update_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_constructed(swapped_entity).unwrap();

world.entities.set(
swapped_entity.index(),
world.entities.update_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_constructed(swapped_entity).unwrap();

world.entities.set(
swapped_entity.index(),
world.entities.update_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_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 construct<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.new_location(entity.row(), Some(location));
entities.mark_construct_or_destruct(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.construct(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
45 changes: 26 additions & 19 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,
world::{unsafe_world_cell::UnsafeEntityCell, 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,22 @@ 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 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_archetype, source_entity) = world
.get_entity(source)
.ok()
.and_then(|source| source.archetype().map(|archetype| (archetype, source)))
.expect("Source entity must exist constructed");

#[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,14 +592,14 @@ 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")
.archetype()
.ok()
.and_then(UnsafeEntityCell::archetype)
.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.

});

if state.move_components {
Expand Down Expand Up @@ -641,7 +648,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