Skip to content

Make entity::index non max #18704

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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e6fc08a
added EntityRow
ElliottjPierce Apr 3, 2025
6d67b52
fixed remaining entity file
ElliottjPierce Apr 3, 2025
ef11fb4
fixed more tests
ElliottjPierce Apr 3, 2025
88aae53
fixed remaining tests
ElliottjPierce Apr 3, 2025
d7e955b
fix wrong tests
ElliottjPierce Apr 3, 2025
28584d7
fixed doc tests
ElliottjPierce Apr 3, 2025
20d68df
fix doc
ElliottjPierce Apr 3, 2025
3fc8506
fix example
ElliottjPierce Apr 3, 2025
da0268d
fix benches
ElliottjPierce Apr 3, 2025
1f113a8
Merge branch 'main' into Make-`Entity`index-non-max
ElliottjPierce Apr 3, 2025
fbd9306
fix scene example
ElliottjPierce Apr 3, 2025
3164bf2
fixed should serialize
ElliottjPierce Apr 3, 2025
de0b7b7
fixed postcard
ElliottjPierce Apr 3, 2025
5930ada
message pack
ElliottjPierce Apr 3, 2025
bf28752
fixed bincode
ElliottjPierce Apr 3, 2025
10a39c2
fix should deserialize
ElliottjPierce Apr 3, 2025
afaea98
fix scene_child...
ElliottjPierce Apr 3, 2025
190f43b
fix extraction order
ElliottjPierce Apr 3, 2025
8ba2839
fix extraction query
ElliottjPierce Apr 3, 2025
2ba14d8
fix allowed_components
ElliottjPierce Apr 3, 2025
f91b96d
remove custom index constants
ElliottjPierce Apr 4, 2025
9b2dc56
add migration guide
ElliottjPierce Apr 4, 2025
73062cb
change name to not have "::"
ElliottjPierce Apr 4, 2025
74349dc
fix markdown lint
ElliottjPierce Apr 4, 2025
40e1b3d
fix typo
ElliottjPierce Apr 5, 2025
a855e13
make to_bits one transmute
ElliottjPierce Apr 5, 2025
fd628ed
fix safety comment
ElliottjPierce Apr 20, 2025
30ee589
use row everywhere
ElliottjPierce Apr 20, 2025
4364558
change migration guide name
ElliottjPierce Apr 20, 2025
a32912c
Fix safety comment
ElliottjPierce Apr 21, 2025
5bd4982
use row naming everywhere
ElliottjPierce Apr 21, 2025
f66efff
fix docs
ElliottjPierce Apr 21, 2025
3cf5c7d
Use better comment
ElliottjPierce Apr 22, 2025
8d482c8
Merge branch 'main' into Make-`Entity`index-non-max
ElliottjPierce May 6, 2025
79a297e
fix new test
ElliottjPierce May 6, 2025
28c08f0
Improve migration guide
ElliottjPierce May 7, 2025
2cbed0b
rename from_raw_u32
ElliottjPierce May 7, 2025
f373597
new line for migration guide
ElliottjPierce May 7, 2025
50fe2b2
fix docs
ElliottjPierce May 7, 2025
b2e4116
Merge branch 'main' into Make-`Entity`index-non-max
ElliottjPierce May 7, 2025
f360294
Include ord in migration guide
ElliottjPierce May 7, 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
6 changes: 3 additions & 3 deletions assets/scenes/load_scene_example.scn.ron
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
),
},
entities: {
4294967296: (
4294967297: (
components: {
"bevy_ecs::name::Name": "joe",
"bevy_transform::components::global_transform::GlobalTransform": ((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0)),
Expand All @@ -23,7 +23,7 @@
),
},
),
4294967297: (
4294967298: (
components: {
"scene::ComponentA": (
x: 3.0,
Expand All @@ -32,4 +32,4 @@
},
),
},
)
)
1 change: 1 addition & 0 deletions benches/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ bevy_platform = { path = "../crates/bevy_platform", default-features = false, fe
glam = "0.29"
rand = "0.8"
rand_chacha = "0.3"
nonmax = { version = "0.5", default-features = false }
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there value in using NonMax over NonZero (I can see the value of banning u32::MAX from being a valid Entity but that doesn't require that the missing value be there)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we do u32, we just need to document that u32::MAX is invalid. We can go that route, but that means there won't be a niche in things like TableRow or anything. (Or at least, it would be more work to convert types, etc.)

If we do u32::NonZero, we loose both index 0 and u32::MAX. We still need careful documentation, and we need to make sure we put a dummy value at index 0 anywhere we are using it as an index.

If we do u32::NonZero but map it to a non max somehow, then we're effectively just reinventing NonMax.

So out of those options I went with NonMax here. But that's not to say we can't do otherwise. If you have strong opinions otherwise, feel free to make your case.

Copy link
Contributor

@Guvante Guvante Apr 29, 2025

Choose a reason for hiding this comment

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

I only called it out since the PR focused solely on wanting a niche but didn't go into why this value was chosen.

IMHO I would lean towards banning 0 as a dummy (many similar setups just use 0 as explicitly none) and either fixing the places that can't handle Max or banning it. I only say that because I think the code here to use NonMax efficiently isn't worth the trade off of preserving 0 that isn't really used.

However I don't think this set of changes is bad and so wouldn't be against this.

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 fair. We just need to put a niche in the EntityRow, marking one value as invalid. Then the total number of ids possible is within u32::MAX. That's the goal.

So we could make 0 the niche, create dummy values for every relevant list, communicate that to users, and map 0..u32::MAX onto 1..=u32::MAX in the allocator. That would work. Pro: no op to access the index. Cons: need dummy values + extra allocation + extra math in entity allocator + invalidates some serialized data.

This pr makes the max the niche, keeping everything but the bit layout exactly the same. Pro: very simple. Cons: binary ! to access + invalidates a lot of serialized data.

It's just a trade off. If the consensus is different, I'm 100% fine with that. But at the end of the day, we just need to pick one.


# Make `bevy_render` compile on Linux with x11 windowing. x11 vs. Wayland does not matter here
# because the benches do not actually open any windows.
Expand Down
5 changes: 3 additions & 2 deletions benches/benches/bevy_ecs/world/entity_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ fn make_entity(rng: &mut impl Rng, size: usize) -> Entity {
let generation = 1.0 + -(1.0 - x).log2() * 2.0;

// this is not reliable, but we're internal so a hack is ok
let bits = ((generation as u64) << 32) | (id as u64);
let id = id as u64 + 1;
let bits = ((generation as u64) << 32) | id;
let e = Entity::from_bits(bits);
assert_eq!(e.index(), id as u32);
assert_eq!(e.index(), !(id as u32));
Copy link
Contributor

Choose a reason for hiding this comment

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

why is there a ! here now

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 question. NonMax just nots the inner NonZero. So if we want to construct one from bits here, we need to do the not ourselves.

assert_eq!(e.generation(), generation as u32);
e
}
Expand Down
32 changes: 24 additions & 8 deletions benches/benches/bevy_ecs/world/world_get.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use core::hint::black_box;
use nonmax::NonMaxU32;

use bevy_ecs::{
bundle::{Bundle, NoBundleEffect},
component::Component,
entity::Entity,
entity::{Entity, EntityRow},
system::{Query, SystemState},
world::World,
};
Expand Down Expand Up @@ -53,7 +54,9 @@ pub fn world_entity(criterion: &mut Criterion) {

bencher.iter(|| {
for i in 0..entity_count {
let entity = Entity::from_raw(i);
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
black_box(world.entity(entity));
}
});
Expand All @@ -74,7 +77,9 @@ pub fn world_get(criterion: &mut Criterion) {

bencher.iter(|| {
for i in 0..entity_count {
let entity = Entity::from_raw(i);
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
assert!(world.get::<Table>(entity).is_some());
}
});
Expand All @@ -84,7 +89,9 @@ pub fn world_get(criterion: &mut Criterion) {

bencher.iter(|| {
for i in 0..entity_count {
let entity = Entity::from_raw(i);
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
assert!(world.get::<Sparse>(entity).is_some());
}
});
Expand All @@ -106,7 +113,9 @@ pub fn world_query_get(criterion: &mut Criterion) {

bencher.iter(|| {
for i in 0..entity_count {
let entity = Entity::from_raw(i);
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
assert!(query.get(&world, entity).is_ok());
}
});
Expand All @@ -131,7 +140,9 @@ pub fn world_query_get(criterion: &mut Criterion) {

bencher.iter(|| {
for i in 0..entity_count {
let entity = Entity::from_raw(i);
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
assert!(query.get(&world, entity).is_ok());
}
});
Expand All @@ -142,7 +153,9 @@ pub fn world_query_get(criterion: &mut Criterion) {

bencher.iter(|| {
for i in 0..entity_count {
let entity = Entity::from_raw(i);
let entity =
// SAFETY: Range is exclusive.
Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
assert!(query.get(&world, entity).is_ok());
}
});
Expand All @@ -169,7 +182,10 @@ pub fn world_query_get(criterion: &mut Criterion) {

bencher.iter(|| {
for i in 0..entity_count {
let entity = Entity::from_raw(i);
// SAFETY: Range is exclusive.
let entity = Entity::from_raw(EntityRow::new(unsafe {
NonMaxU32::new_unchecked(i)
}));
assert!(query.get(&world, entity).is_ok());
}
});
Expand Down
14 changes: 6 additions & 8 deletions crates/bevy_ecs/src/entity/map_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ impl<T: MapEntities, A: smallvec::Array<Item = T>> MapEntities for SmallVec<A> {
/// fn get_mapped(&mut self, entity: Entity) -> Entity {
/// self.map.get(&entity).copied().unwrap_or(entity)
/// }
///
///
/// fn set_mapped(&mut self, source: Entity, target: Entity) {
/// self.map.insert(source, target);
/// }
Expand Down Expand Up @@ -228,7 +228,7 @@ impl EntityMapper for SceneEntityMapper<'_> {

// this new entity reference is specifically designed to never represent any living entity
let new = Entity::from_raw_and_generation(
self.dead_start.index(),
self.dead_start.row(),
IdentifierMask::inc_masked_high_by(self.dead_start.generation, self.generations),
);

Expand Down Expand Up @@ -331,21 +331,19 @@ impl<'m> SceneEntityMapper<'m> {

#[cfg(test)]
mod tests {

use crate::{
entity::{Entity, EntityHashMap, EntityMapper, SceneEntityMapper},
world::World,
};

#[test]
fn entity_mapper() {
const FIRST_IDX: u32 = 1;
const SECOND_IDX: u32 = 2;

let mut map = EntityHashMap::default();
let mut world = World::new();
let mut mapper = SceneEntityMapper::new(&mut map, &mut world);

let mapped_ent = Entity::from_raw(FIRST_IDX);
let mapped_ent = Entity::from_raw_u32(1).unwrap();
let dead_ref = mapper.get_mapped(mapped_ent);

assert_eq!(
Expand All @@ -354,7 +352,7 @@ mod tests {
"should persist the allocated mapping from the previous line"
);
assert_eq!(
mapper.get_mapped(Entity::from_raw(SECOND_IDX)).index(),
mapper.get_mapped(Entity::from_raw_u32(2).unwrap()).index(),
dead_ref.index(),
"should re-use the same index for further dead refs"
);
Expand All @@ -372,7 +370,7 @@ mod tests {
let mut world = World::new();

let dead_ref = SceneEntityMapper::world_scope(&mut map, &mut world, |_, mapper| {
mapper.get_mapped(Entity::from_raw(0))
mapper.get_mapped(Entity::from_raw_u32(0).unwrap())
});

// Next allocated entity should be a further generation on the same index
Expand Down
Loading