Skip to content

Nonmax all rows #19132

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

Merged
merged 10 commits into from
May 26, 2025
Merged

Nonmax all rows #19132

merged 10 commits into from
May 26, 2025

Conversation

ElliottjPierce
Copy link
Contributor

@ElliottjPierce ElliottjPierce commented May 8, 2025

Objective

Since #18704 is done, we can track the length of unique entity row collections with only a u32 and identify an index within that collection with only a NonMaxU32. This leaves an opportunity for performance improvements.

Solution

  • Use EntityRow in sparse sets.
  • Change table, entity, and query lengths to be u32 instead of usize.
  • Keep batching module usize based since that is reused for events, which may exceed u32::MAX.
  • Change according Range<usize> to Range<u32>. This is more efficient and helps justify safety.
  • Change ArchetypeRow and TableRow to wrap NonMaxU32 instead of u32.

Justifying NonMaxU32::new_unchecked everywhere is predicated on this safety comment in Entities::set: "location must be valid for the entity at index or immediately made valid afterwards before handing control to unknown code." This ensures no entity is in two table rows for example. That fact is used to argue uniqueness of the entity rows in each table, archetype, sparse set, query, etc. So if there's no duplicates, and a maximum total entities of u32::MAX none of the corresponding row ids / indexes can exceed NonMaxU32.

Testing

CI

@ElliottjPierce ElliottjPierce added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times C-Code-Quality A section of code that is hard to understand or change M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 8, 2025
Copy link
Contributor

github-actions bot commented May 8, 2025

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Please review the instructions for writing migration guides, then expand or revise the content in the migration guides directory to reflect your changes.

Copy link
Contributor

@ItsDoot ItsDoot 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! I like the additional cleanup with Archetype::entities_with_location

Co-authored-by: Christian Hughes <9044780+ItsDoot@users.noreply.github.com>
Comment on lines +600 to +601
// SAFETY: An entity can not have multiple archetype rows and there can not be more than u32::MAX entities.
let archetype_row = unsafe { ArchetypeRow::new(NonMaxU32::new_unchecked(self.len())) };
Copy link
Contributor

@urben1680 urben1680 May 21, 2025

Choose a reason for hiding this comment

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

Something is off here.

"there can not be more than u32::MAX entities" semantically means "there can be up to u32::MAX entities". That would mean this can be unsound if there really end up being u32::MAX entities, not fitting into NonMaxU32.

If I understand #18704 correctly, then this is not a incorrectly worded comment: There can be u32::MAX entities indeed, thus an archetype containing them all can have a size of u32::MAX.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is admittedly tricky to reason about.

The index of the Entity id, may be up to (but not equal to) u32::MAX. But the number of entities is the maximum index + 1. So there may well be u32::MAX entities. But even if there are, none of them will have u32::MAX as an index. The opposite applies too. If the maximum number of entities is u32::MAX, each entity has an index less than u32::MAX.

So the safety comment is correct everywhere. It's just a bit hard to get one's head around it unless you're familiar with the entity id scheme.

If you can think of clearer wording, I'm open to suggestions. But off by one errors are so common, I don't think we need to bend over backwards to explain it at each safety comment. I think it's fine as it is.

Copy link
Contributor

Choose a reason for hiding this comment

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

So the idea is this:

  • This method is called when an entity is about to be put into the archetype
  • This implies that this new entity is not already part of the archetype, so archetype.len() cannot return u32::MAX, it only could once this call is over
  • When u32::MAX entities are reached, this method here will not called again as that implied there would be more entities than u32::MAX, thus this is safe

If I am correct then this is safe. I am just not sure if the method safety docs need to note that one should not make an Entity and TableRow out of thin air and call this when there are already u32::MAX entities.

Maybe add to the comment I marked here that at the time of the call there can not be u32::MAX entities inside this archetype. That would more reflect on the self.len() call here and not these mental steps you have to do.

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// SAFETY: An entity can not have multiple archetype rows and there can not be more than u32::MAX entities.
let archetype_row = unsafe { ArchetypeRow::new(NonMaxU32::new_unchecked(self.len())) };
// SAFETY: An entity can not have multiple archetype rows and with this at most
// u32::MAX'th entity being new to this archetype, self.len() cannot return u32::MAX.
let archetype_row = unsafe { ArchetypeRow::new(NonMaxU32::new_unchecked(self.len())) };

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the idea is this: ...

That's a good summary.

If I am correct then this is safe. I am just not sure if the method safety docs need to note that one should not make an Entity and TableRow out of thin air and call this when there are already u32::MAX entities.

If I'm understanding your concerns correctly, we're on the same page that the only logical problem here is if the caller is inventing these values outside of an entity allocator. As long as these values come from an allocator, and the relevant EntityLocation safety comments are upheld by when interacting with Entities, this is 100% safe.

The only danger is creating rows and entities "out of thin air". Nobody is doing that. Further, this is already expressly unsafe. Safety comments usually require these ids to be valid, etc.

I don't disagree that this safety issue would be nice to address, but it should be somewhat obvious that the Entity and TableRow must be valid. Making the safety comments explicitly say this would be a big effort (I think) because this assumption that Entity ids are valid, have valid location data, etc, are so fundamental that adding this to all safety comments would be a big PR.

At the end of the day, I'm not opposed to this; I just don't think that should be grouped into this PR.

Copy link
Contributor

@urben1680 urben1680 left a comment

Choose a reason for hiding this comment

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

I think I did not understand all logic behind this part of the codebase, but you did not change much from how it was so I focused on the safety comments.


impl ArchetypeRow {
/// Index indicating an invalid archetype row.
/// This is meant to be used as a placeholder.
pub const INVALID: ArchetypeRow = ArchetypeRow(u32::MAX);
pub const INVALID: ArchetypeRow = ArchetypeRow(NonMaxU32::MAX);
Copy link
Contributor

@urben1680 urben1680 May 21, 2025

Choose a reason for hiding this comment

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

Regarding my earlier comment, even if there are not u32::MAX entities but NonMaxU32::MAX entities, that last one would have a row equal to INVALID? If they all end up in the same archetype.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. It was that way before though. This will let us trade INVALID for None though in the future!

Copy link
Contributor

@urben1680 urben1680 May 22, 2025

Choose a reason for hiding this comment

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

It was that way before though

I am not so happy with this reasoning when stumbling over a potential bug. 😅

If I am not mistaken this is only used in EntityLocation::INVALID and these are checked upon by looking at the ArchetypeId being invalid or not, so this seems to be no concern. Maybe this constant could be renamed to PLACEHOLDER and warn in the docs that this may be a valid row in a full archetype.

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 we're on the same page here. It's not good that INVALID is, in fact, valid.

In the past, it wrapped u32, so we were forced to use u32::MAX as None (Option would take 64 bits!). In the future, making it non-max will let us remove INVALID and just use Option.

Also note that this is only ever used in coordination with ArchetypeId, which has the same issue.

We're in agreement that this would be better called PLACEHOLDER or something or removed and replaced with a proper Option. But I think changing names/semantics like this is better left for a different PR. For now, I just want to tighten up the type, now that entity rows are themselves NonMax.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would you be more comfortable with adding a "todo: deprecate because this row can indeed be valid" comment on the constant? That could be addressed in a future 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.

Good idea! I've added a todo item. I suspect I can fulfill it in a future PR in the near future. I'd just like remote reservation to be merged first for convenience.

@ElliottjPierce ElliottjPierce 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 23, 2025
Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

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

This approval is limited to the migration guide, which looks great! Thanks again for the detail.

Very pleased to see people updating/expanding existing migration guides. This is exactly the intended use. Great job.

@ElliottjPierce
Copy link
Contributor Author

The new migration guide system is like 10x better than the old. It feels so much better now. Thanks for making it!

When I finish my entity pr marathon, I'm going to make a pr just to consolidate/polish the entity migration guides a little more.(I know we'll do that again before release, but keeping the guides as files lets us do this iteratively too.) Amazing!

@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 26, 2025
Merged via the queue into bevyengine:main with commit b6b5491 May 26, 2025
34 checks passed
github-merge-queue bot pushed a commit that referenced this pull request May 31, 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

---------

Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
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 C-Performance A change motivated by improving speed, memory usage or compile times M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants