Skip to content

Remove invalid entity locations #19433

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

ElliottjPierce
Copy link
Contributor

@ElliottjPierce ElliottjPierce commented May 29, 2025

Objective

This is the first step of #19430 and is a follow up for #19132.

Now that ArchetypeRow has a niche, we can use Option instead of needing INVALID everywhere.

This was especially concerning since INVALID really was valid!

Using options here made the code clearer and more data-driven.

Solution

Replace all uses of INVALID entity locations (and archetype/table rows) with None.

Testing

CI

@ElliottjPierce ElliottjPierce added A-ECS Entities, components, systems, and events X-Uncontroversial This work is generally agreed upon D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward C-Code-Quality A section of code that is hard to understand or change labels May 29, 2025
if meta.generation != EntityGeneration::FIRST
|| meta.location.archetype_id != ArchetypeId::INVALID
{
if meta.location.is_some() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is right, but honestly, not even sure why we need the if.

Copy link
Contributor

@urben1680 urben1680 May 29, 2025

Choose a reason for hiding this comment

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

I think this is not right, the tick may also be indirectly returned by entity_get_spawned_or_despawned even if meta.location is None in some cases.

If the the tick is considered valid in such a case, then it needs to be checked upon in this method here too.

It exposes that we somehow lost the ability to reliably detect when the tick is indeed unused and only a default value. But honestly I would not bother given how cold this method here is. Just check all ticks in here and with that be sure all edge cases are covered.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. I'll remove the if.

@@ -973,7 +972,7 @@ impl Entities {
/// - `location` must be valid for the entity at `index` or immediately made valid afterwards
/// before handing control to unknown code.
#[inline]
pub(crate) unsafe fn set(&mut self, index: u32, location: EntityLocation) {
pub(crate) unsafe fn set(&mut self, index: u32, location: Option<EntityLocation>) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that this method is always called with Some location. Do you expect calls with None in the future or another PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added it for completeness but yes.

We used to have alloc_at which let you respawn an entity and reuse its index. There have been requests to support that again. That's one of the next steps in my tracking issue. Basically, we'll set the location to None instead of fully despawning.

pub(crate) fn assert_not_despawned(&self) {
if self.location.archetype_id == ArchetypeId::INVALID {
self.panic_despawned();
pub(crate) fn assert_not_despawned(&self) -> EntityLocation {
Copy link
Contributor

@urben1680 urben1680 May 29, 2025

Choose a reason for hiding this comment

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

Since this and EntityWorldMut::location do the same now, this one can probably be removed and usages switch to the location method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out it's used for other things too, so I've kept it but reverted to how it was.

if meta.generation != EntityGeneration::FIRST
|| meta.location.archetype_id != ArchetypeId::INVALID
{
if meta.location.is_some() {
Copy link
Contributor

@urben1680 urben1680 May 29, 2025

Choose a reason for hiding this comment

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

I think this is not right, the tick may also be indirectly returned by entity_get_spawned_or_despawned even if meta.location is None in some cases.

If the the tick is considered valid in such a case, then it needs to be checked upon in this method here too.

It exposes that we somehow lost the ability to reliably detect when the tick is indeed unused and only a default value. But honestly I would not bother given how cold this method here is. Just check all ticks in here and with that be sure all edge cases are covered.

Comment on lines -1319 to -1327
impl EntityLocation {
/// location for **pending entity** and **invalid entity**
pub(crate) const INVALID: EntityLocation = EntityLocation {
archetype_id: ArchetypeId::INVALID,
archetype_row: ArchetypeRow::INVALID,
table_id: TableId::INVALID,
table_row: TableRow::INVALID,
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

feels_good_man.jpg

Copy link
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

Looks good! Just left some style suggestions.

Some(meta.location)
(meta.generation == entity.generation)
.then_some(meta.location)
.flatten()
Copy link
Contributor

Choose a reason for hiding this comment

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

then_some().flatten() is a little odd. I think that might be Option::filter()?

Hmm, or could this whole thing be replaced with filter().and_then() like

self.meta.get(entity.index() as usize)
    .filter(|meta| meta.generation == entity.generation)
    .and_then(|meta| meta.location)

?

(Sorry, I can't get GitHub to let me write that as a suggestion.)

/// Must not be called while reserved entities are awaiting `flush()`.
pub fn free(&mut self, entity: Entity) -> Option<EntityLocation> {
pub fn free(&mut self, entity: Entity) -> Option<Option<EntityLocation>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Option<Option<T>> can be hard to reason about. Does anyone care about Some(None) vs None, or could we flatten this and return a plain Option<EntityLocation>? It looks like the only place this is called changed from .expect() to .expect().expect(), so it doesn't seem to care.

Otherwise, it might be worth returning a Result with some new enum that gives names to the two kinds of None.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 30, 2025
ElliottjPierce and others added 2 commits May 30, 2025 12:04
Co-Authored-By: Chris Russell <[email protected]>
Co-Authored-By: Chris Russell <[email protected]>
@ElliottjPierce
Copy link
Contributor Author

As far as Option<Option<EntityLocation>> goes, I agree that, for now, flattening is the right way to go, especially given the inconsistency between get and free.

But as a heads up, I think IMO we should expose this fully. Maybe a struct MaybeEntityLocation(Option<EntityLocation>);? This will be useful for constructing/destructing entities in the future, but I'll save that for another pr.

@ElliottjPierce
Copy link
Contributor Author

Update: unfortunately tests do seem to need the full location information, so I have added it back.

But I used a type alias to hopefully make things a little more clear.

@urben1680
Copy link
Contributor

urben1680 commented May 30, 2025

An alternative could also be Result<Option<EntityLocation>, EntityDoesNotExistError>, though it is not so nice since Ok(None) also means the entity kinda does not exist.

@ElliottjPierce
Copy link
Contributor Author

An alternative could also be Result<Option<EntityLocation>, EntityDoesNotExistError>, though it is not so nice since Ok(None) also means the entity kinda does not exist.

Option<EntityIdLocation> to Result<EntityIdLocation, EndityDoesNotExistError> is not a bad idea. I'm not opposed to it, but I'm not sure what it would buy us. We still need EntityIdLocation for set, flush, etc. Maybe I'll try it out in a future pr.

///
/// Setting a location to `None` is often helpful when you want to destruct an entity or yank it from the ECS without allowing another system to reuse the id for something else.
/// It is also useful for reserving an id; commands will often allocate an `Entity` but not provide it a location until the command is applied.
pub type EntityIdLocation = Option<EntityLocation>;
Copy link
Contributor

@NyxAlexandra NyxAlexandra May 30, 2025

Choose a reason for hiding this comment

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

I'm not sure if this name is very clear ("location of an entity" versus "location of an entity identifier"). Then again, I'm not sure of a better name (nor am I a maintaner or anything like that).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants