Skip to content

EntityGeneration ordering #19421

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

Conversation

ElliottjPierce
Copy link
Contributor

Objective

Recently the u32 Entity::generation was replaced with the new EntityGeneration in #19121.
This made meanings a lot more clear, and prevented accidental misuse.

One common misuse was assuming that u32s that were greater than others came after those others.
Wrapping makes this assumption false.
When EntityGeneration was created, it retained the u32 ordering, which was useless at best and wrong at worst.
This pr fixes the ordering implementation, so new generations are greater than older generations.

Some users were already accounting for this ordering issue (which was still present in 0.16 and before) by manually accessing the u32 representation. This made migrating difficult for avian physics; see here.

I am generally of the opinion that this type should be kept opaque to prevent accidental misuse.
As we find issues like this, the functionality should be added to EntityGeneration directly.

Solution

Fix the ordering implementation through Ord.

Alternatively, we could keep Ord the same and make a cmp_age method, but I think this is better, even though sorting entity ids may be marginally slower now (but more correct). This is a tradeoff.

Testing

I improved documentation for aliasing and ordering, adding some doc tests.

@ElliottjPierce ElliottjPierce added D-Trivial Nice and easy! A great choice to get started with Bevy A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 29, 2025
@Victoronz
Copy link
Contributor

I do think we need better docs here, and supporting the use case of older vs newer generation comparison should be done, however I think we should be careful about adding meaning to this Ord impl while the Ord behavior of Entity, EntityRow and EntityGeneration are inconsistent:
Ideally, for a collection of Entity, if the row field is always equal, then Entity should compare the same as if we were just comparing its generation field, and vice-versa. I.e. Entity should compare like (EntityRow, EntityGeneration).

But those fields are both NonMaxU32, which break this expectation! (This type does not compare the bits directly, Entity does)
While not ideal, I am under the impression that we currently have a "There is a default order, but it has no guaranteed meaning" stance (like with query iteration order), which lessens the impact of this.

But if we make the default order of EntityGeneration meaningful, we exacerbate this problem.
This gives us the option of either adjusting the Ord impl of Entity to match the behavior of its parts, or adding a dedicated age_cmp method. The latter strikes me as less invasive for now!

@Victoronz Victoronz added the X-Contentious There are nontrivial implications that should be thought through label May 29, 2025
@ElliottjPierce
Copy link
Contributor Author

I do think we need better docs here, and supporting the use case of older vs newer generation comparison should be done, however I think we should be careful about adding meaning to this Ord impl while the Ord behavior of Entity, EntityRow and EntityGeneration are inconsistent: Ideally, for a collection of Entity, if the row field is always equal, then Entity should compare the same as if we were just comparing its generation field, and vice-versa. I.e. Entity should compare like (EntityRow, EntityGeneration).

I don't actually think this is an issue. If we openly state "There is a default order, but it has no guaranteed meaning" for Entity, I have no issue with that diverging from (EntityRow, EntityGeneration). Ordering Entity is for disambiguating, etc. Ordering EntityRow has a well defined meaning (which row comes first), and ordering EntityGeneration should have a well defined meaning (which came first).

So I don't see an issue here; though, granted, it is unconventional.

That said, you do make a compelling argument. I started writing an "I agree" comment before I realized the "There is a default order, but it has no guaranteed meaning" stance itself made room for this. So if others disagree, I'd be happy to back track and make age_cmp or something. I just would guess that users would intuitively prefer < than .age_cmp with a maches! or something.

@Victoronz
Copy link
Contributor

Victoronz commented May 29, 2025

I don't actually think this is an issue. If we openly state "There is a default order, but it has no guaranteed meaning" for Entity, I have no issue with that diverging from (EntityRow, EntityGeneration). Ordering Entity is for disambiguating, etc. Ordering EntityRow has a well defined meaning (which row comes first), and ordering EntityGeneration should have a well defined meaning (which came first).

So I don't see an issue here; though, granted, it is unconventional.

That said, you do make a compelling argument. I started writing an "I agree" comment before I realized the "There is a default order, but it has no guaranteed meaning" stance itself made room for this. So if others disagree, I'd be happy to back track and make age_cmp or something. I just would guess that users would intuitively prefer < than .age_cmp with a maches! or something.

I've looked at the docs for Entity again, and I've now noticed that the field order for it isn't actually public, so this is even less of an immediate issue.
I would like to see this! It seems like the obvious behavior for how generations should be ordered.
What made me apprehensive is that while we do not guarantee order, the state of affairs strikes me as less of a "we will not guarantee this" and more of a "we have not yet decided/worked on the semantics of this".

The question of "What do we do about sorted tables/iteration order/entity order" seems to accumulate an ever-growing pile of implementation quirks to deal with! Even if the discrepancy is a non-issue right now, I suspect it will resurface there later. It would seem that we'll then have to live with either an inconsistency or a performance hit somewhere.

That being said, I wonder how much actual cost it would carry to switch over the comparison behavior of Entity to the combined impls of EntityRow and EntityGeneration. Then again, I'd wager that the main impact would be of some code no longer autovectorizing, which is hard to test for.

Copy link
Contributor

@Victoronz Victoronz left a comment

Choose a reason for hiding this comment

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

Ultimately, I think we can do this, given we both the public non-guarantee, and an internal word of caution is documented.

It might lead to some unfortunate performance hits to maintain consistency later on, but I think the proper generation behavior/semantics should come before that.

Finally, if it does become an issue, we can still decide otherwise down the line!

@Victoronz Victoronz 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 29, 2025
@alice-i-cecile alice-i-cecile added this to the 0.17 milestone May 29, 2025
@alice-i-cecile alice-i-cecile added this pull request to the merge queue May 29, 2025
Merged via the queue into bevyengine:main with commit 1966e44 May 29, 2025
45 checks passed
@ElliottjPierce
Copy link
Contributor Author

The question of "What do we do about sorted tables/iteration order/entity order" seems to accumulate an ever-growing pile of implementation quirks to deal with! Even if the discrepancy is a non-issue right now, I suspect it will resurface there later. It would seem that we'll then have to live with either an inconsistency or a performance hit somewhere.

That being said, I wonder how much actual cost it would carry to switch over the comparison behavior of Entity to the combined impls of EntityRow and EntityGeneration. Then again, I'd wager that the main impact would be of some code no longer autovectorizing, which is hard to test for.

I could not agree more. We may need to revisit this later. The choice is between fast and meaningless or slow(er) and meaningful. And we'll have to solidify and document that choice at some point.

I would personally think "fast and meaningless" is better. That way, if someone wants a particular ordering, they can absolutely do that by getting the inner row and generation, and the cost of that would be transparent. But that's just me.

Something to think about...

github-merge-queue bot pushed a commit that referenced this pull request Jun 7, 2025
…lone method (#19432)

# Objective

#19421 implemented `Ord` for `EntityGeneration` along the lines of [the
impl from
slotmap](https://docs.rs/slotmap/latest/src/slotmap/util.rs.html#8):
```rs
/// Returns if a is an older version than b, taking into account wrapping of
/// versions.
pub fn is_older_version(a: u32, b: u32) -> bool {
    let diff = a.wrapping_sub(b);
    diff >= (1 << 31)
}
```

But that PR and the slotmap impl are different:

**slotmap impl**
- if `(1u32 << 31)` is greater than `a.wrapping_sub(b)`, then `a` is
older than `b`
- if `(1u32 << 31)` is equal to `a.wrapping_sub(b)`, then `a` is older
than `b`
- if `(1u32 << 31)` is less than `a.wrapping_sub(b)`, then `a` is equal
or newer than `b`

**previous PR impl**
- if `(1u32 << 31)` is greater than `a.wrapping_sub(b)`, then `a` is
older than `b`
- if `(1u32 << 31)` is equal to `a.wrapping_sub(b)`, then `a` is equal
to `b` ⚠️
- if `(1u32 << 31)` is less than `a.wrapping_sub(b)`, then `a` is newer
than `b` ⚠️

This ordering is also not transitive, therefore it should not implement
`PartialOrd`.

## Solution

Fix the impl in a standalone method, remove the `Partialord`/`Ord`
implementation.

## Testing

Given the first impl was wrong and got past reviews, I think a new unit
test is justified.
VitalyAnkh pushed a commit to VitalyAnkh/bevy that referenced this pull request Jun 8, 2025
…lone method (bevyengine#19432)

# Objective

bevyengine#19421 implemented `Ord` for `EntityGeneration` along the lines of [the
impl from
slotmap](https://docs.rs/slotmap/latest/src/slotmap/util.rs.html#8):
```rs
/// Returns if a is an older version than b, taking into account wrapping of
/// versions.
pub fn is_older_version(a: u32, b: u32) -> bool {
    let diff = a.wrapping_sub(b);
    diff >= (1 << 31)
}
```

But that PR and the slotmap impl are different:

**slotmap impl**
- if `(1u32 << 31)` is greater than `a.wrapping_sub(b)`, then `a` is
older than `b`
- if `(1u32 << 31)` is equal to `a.wrapping_sub(b)`, then `a` is older
than `b`
- if `(1u32 << 31)` is less than `a.wrapping_sub(b)`, then `a` is equal
or newer than `b`

**previous PR impl**
- if `(1u32 << 31)` is greater than `a.wrapping_sub(b)`, then `a` is
older than `b`
- if `(1u32 << 31)` is equal to `a.wrapping_sub(b)`, then `a` is equal
to `b` ⚠️
- if `(1u32 << 31)` is less than `a.wrapping_sub(b)`, then `a` is newer
than `b` ⚠️

This ordering is also not transitive, therefore it should not implement
`PartialOrd`.

## Solution

Fix the impl in a standalone method, remove the `Partialord`/`Ord`
implementation.

## Testing

Given the first impl was wrong and got past reviews, I think a new unit
test is justified.
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-Usability A targeted quality-of-life change that makes Bevy easier to use D-Trivial Nice and easy! A great choice to get started with Bevy S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants