Skip to content

Construct and deconstruct entities to improve entity allocation #19451

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 71 commits into
base: main
Choose a base branch
from

Conversation

ElliottjPierce
Copy link
Contributor

@ElliottjPierce ElliottjPierce commented May 31, 2025

Objective

This is the next step for #19430 and is also convinient for #18670.

For context, the way entities work on main is as a "allocate and use" system. Entity ids are allocated, and given a location. The location can then be changed, etc. Entities that are free have an invalid location. To allocate an entity, one must also set its location. This introduced the need for pending entities, where an entity would be reserved, pending, and at some point flushed. Pending and free entities have an invalid location, and others are assumed to have a valid one.

This paradigm has a number of downsides: First, the entities metadata table is inseparable from the allocator, which makes remote reservation challenging. Second, the World must be flushed, even to do simple things, like allocate a temporary entity id. Third, users have little control over entity ids, only interacting with conceptual entities. This made things like Entities::alloc_at clunky and slow, leading to its removal, despite some users still having valid need of it.

So the goal of this PR is to:

  • Decouple Entities from entity allocation to make room for other allocators and resolve alloc_at issues.
  • Decouple entity allocation from spawning to make reservation a moot point.
  • Introduce constructing and destructing entities, in addition to spawn/despawn.
  • Change reserve and flush patterns to alloc and construct patterns.

It is possible to break this up into multiple prs, as I originally intended, but doing so would require lots of temporary scaffolding that would both hurt performance and make things harder to review.

Solution

This solution builds on #19433, which changed the representation of invalid entity locations from a constant to None.

There's quite a few steps to this, each somewhat controversial:

Entities with no location

This pr introduces the idea of entity rows both with and without locations. This corresponds to entities that are constructed (the row has a location) and not constructed (the row has no location). When a row is free or pending, it is not constructed. When a row is outside the range of the meta list, it still exists; it's just not constructed.

This extends to conceptual entities; conceptual entities may now be in one of 3 states: empty (constructed; no components), normal (constructed; 1 or more components), or null (not constructed). This extends to entity pointers (EntityWorldMut, etc): These now can point to "null"/not constructed entities. Depending on the privilege of the pointer, these can also construct or destruct the entity.

This also changes how Entity ids relate to conceptual entities. An Entity now exists if its generation matches that of its row. An Entity that has the right generation for its row will claim to exist, even if it is not constructed. This means, for example, an Entity manually constructed with a large index and generation of 0 will exist if it has not been allocated yet.

Entities is separate from the allocator

This pr separates entity allocation from Entities. Entities is now only focused on tracking entity metadata, etc. The new EntitiesAllocator on World manages all allocations. This forces Entities to not rely on allocator state to determine if entities exist, etc, which is convinient for remote reservation and needed for custom allocators. It also paves the way for allocators not housed within the World, makes some unsafe code easier since the allocator and metadata live under different pointers, etc.

This separation requires thinking about interactions with Entities in a new way. Previously, the Entities set the rules for what entities are valid and what entities are not. Now, it has no way of knowing. Instead, interaction with Entities are more like declaring some information for it to track than changing some information it was already tracking. To reflect this, set has been split up into declare and update.

Constructing and destructing

As mentioned, entities that have no location (not constructed) can be constructed at any time. This takes on exactly the same meaning as the previous spawn_non_existent. It creates/declares a location instead of updating an old one. As an example, this makes spawning an entity now literately just allocate a new id and construct it immediately.

Conversely, entities that are constructed may be destructed. This removes all components and despawns related entities, just like despawn. The only difference is that destructing does not free the entity id for reuse. Between constructing and destructing, all needs for alloc_at are resolved. If you want to keep the id for custom reuse, just destruct instead of despawn! Despawn, now just destructs the entity and frees it.

Destructing a not constructed entity will do nothing. Constructing an already constructed entity will panic. This is to guard against users constructing a manually formed Entity that the allocator could later hand out. However, public construction methods have proper error handling for this. Despawning a not constructed entity just frees its id.

No more flushing

All places that once needed to reserve and flush entity ids now allocate and construct them instead. This improves performance and simplifies things.

Flow chart

entity row lifecycle

(Thanks @ItsDoot)

Testing

  • CI
  • Some new tests
  • A few deleted (no longer applicable) tests
  • If you see something you think should have a test case, I'll gladly add it.

Showcase

Here's an example of constructing and destructing

let e4 = world.spawn_null();
world
    .entity_mut(e4)
    .construct((TableStored("junk"), A(0)))
    .unwrap()
    .destruct()
    .construct((TableStored("def"), A(456)))
    .unwrap();

Future Work

  • More expansive docs. This should definitely should be done, but I'd rather do that in a future pr to separate writing review from code review. If you have more ideas for how to introduce users to these concepts, I'd like to see them. As it is, we don't do a very good job of explaining entities to users. Ex: Entity doesn't always correspond to a conceptual entity.
  • Try to remove panics from EntityWorldMut. There is (and was) a lot of assuming the entity is constructed there (was assuming it was not despawned).
  • A lot of names are still centered around spawn/despawn, which is more user-friendly than construct/destruct but less precise. Might be worth changing these over.
  • Making a centralized bundle despawner would make sense now.
  • Of course, build on this for remote reservation and, potentially, for paged entities.

Performance

Benchmarks
critcmp main pr19451 -t 1
group                                                                                                     main                                     pr19451
-----                                                                                                     ----                                     -------
add_remove/sparse_set                                                                                     1.13    594.7±6.80µs        ? ?/sec      1.00    527.4±8.01µs        ? ?/sec
add_remove/table                                                                                          1.08   799.6±15.53µs        ? ?/sec      1.00   739.7±15.10µs        ? ?/sec
add_remove_big/sparse_set                                                                                 1.10    614.6±6.50µs        ? ?/sec      1.00   557.0±19.04µs        ? ?/sec
add_remove_big/table                                                                                      1.03      2.8±0.01ms        ? ?/sec      1.00      2.7±0.02ms        ? ?/sec
added_archetypes/archetype_count/100                                                                      1.01     30.9±0.50µs        ? ?/sec      1.00     30.5±0.44µs        ? ?/sec
added_archetypes/archetype_count/1000                                                                     1.00   638.0±19.77µs        ? ?/sec      1.03   657.0±73.61µs        ? ?/sec
added_archetypes/archetype_count/10000                                                                    1.02      5.5±0.14ms        ? ?/sec      1.00      5.4±0.09ms        ? ?/sec
all_added_detection/50000_entities_ecs::change_detection::Sparse                                          1.02     47.9±1.22µs        ? ?/sec      1.00     46.8±0.40µs        ? ?/sec
all_added_detection/50000_entities_ecs::change_detection::Table                                           1.02     45.4±1.89µs        ? ?/sec      1.00     44.6±0.78µs        ? ?/sec
build_schedule/1000_schedule                                                                              1.02   942.6±11.53ms        ? ?/sec      1.00   925.2±10.35ms        ? ?/sec
build_schedule/100_schedule                                                                               1.01      5.8±0.12ms        ? ?/sec      1.00      5.7±0.12ms        ? ?/sec
build_schedule/100_schedule_no_constraints                                                                1.03   803.1±28.93µs        ? ?/sec      1.00   781.1±50.11µs        ? ?/sec
build_schedule/500_schedule_no_constraints                                                                1.00      5.6±0.31ms        ? ?/sec      1.08      6.0±0.27ms        ? ?/sec
busy_systems/01x_entities_03_systems                                                                      1.00     24.4±1.35µs        ? ?/sec      1.01     24.7±1.35µs        ? ?/sec
busy_systems/03x_entities_03_systems                                                                      1.00     38.1±1.70µs        ? ?/sec      1.04     39.7±1.49µs        ? ?/sec
busy_systems/03x_entities_09_systems                                                                      1.01    111.4±2.27µs        ? ?/sec      1.00    109.9±2.46µs        ? ?/sec
busy_systems/03x_entities_15_systems                                                                      1.00    174.8±2.56µs        ? ?/sec      1.01    176.6±4.22µs        ? ?/sec
contrived/03x_entities_09_systems                                                                         1.00     59.0±2.92µs        ? ?/sec      1.01     59.8±3.03µs        ? ?/sec
contrived/03x_entities_15_systems                                                                         1.00     97.5±4.87µs        ? ?/sec      1.01     98.8±4.69µs        ? ?/sec
contrived/05x_entities_09_systems                                                                         1.00     75.3±3.76µs        ? ?/sec      1.01     76.4±4.11µs        ? ?/sec
despawn_world/10000_entities                                                                              1.32    344.8±4.47µs        ? ?/sec      1.00    261.4±4.91µs        ? ?/sec
despawn_world/100_entities                                                                                1.22      4.3±0.04µs        ? ?/sec      1.00      3.5±0.54µs        ? ?/sec
despawn_world/1_entities                                                                                  1.01    169.6±7.88ns        ? ?/sec      1.00   167.8±11.45ns        ? ?/sec
despawn_world_recursive/10000_entities                                                                    1.20  1723.0±53.82µs        ? ?/sec      1.00  1437.0±26.11µs        ? ?/sec
despawn_world_recursive/100_entities                                                                      1.16     17.9±0.10µs        ? ?/sec      1.00     15.5±0.16µs        ? ?/sec
despawn_world_recursive/1_entities                                                                        1.01   372.8±15.68ns        ? ?/sec      1.00   367.7±16.90ns        ? ?/sec
ecs::entity_cloning::hierarchy_many/clone                                                                 1.03   227.9±24.67µs 1559.9 KElem/sec    1.00   221.1±29.74µs 1607.8 KElem/sec
ecs::entity_cloning::hierarchy_many/reflect                                                               1.00   406.2±23.46µs 875.2 KElem/sec     1.02   413.9±22.45µs 858.9 KElem/sec
ecs::entity_cloning::hierarchy_tall/clone                                                                 1.01     12.2±0.34µs  4.0 MElem/sec      1.00     12.0±1.41µs  4.1 MElem/sec
ecs::entity_cloning::hierarchy_tall/reflect                                                               1.02     15.3±0.39µs  3.2 MElem/sec      1.00     15.0±2.14µs  3.2 MElem/sec
ecs::entity_cloning::single/clone                                                                         1.02  659.0±100.01ns 1481.8 KElem/sec    1.00  643.3±101.49ns 1517.9 KElem/sec
ecs::entity_cloning::single/reflect                                                                       1.03  1135.2±72.17ns 860.2 KElem/sec     1.00  1098.3±65.99ns 889.1 KElem/sec
empty_archetypes/for_each/10                                                                              1.02      8.1±0.57µs        ? ?/sec      1.00      8.0±0.37µs        ? ?/sec
empty_archetypes/for_each/100                                                                             1.01      8.1±0.34µs        ? ?/sec      1.00      8.1±0.28µs        ? ?/sec
empty_archetypes/for_each/1000                                                                            1.03      8.4±0.25µs        ? ?/sec      1.00      8.2±0.29µs        ? ?/sec
empty_archetypes/iter/100                                                                                 1.01      8.1±0.29µs        ? ?/sec      1.00      8.0±0.34µs        ? ?/sec
empty_archetypes/iter/1000                                                                                1.02      8.5±0.31µs        ? ?/sec      1.00      8.4±0.62µs        ? ?/sec
empty_archetypes/iter/10000                                                                               1.01     10.6±1.22µs        ? ?/sec      1.00     10.5±0.49µs        ? ?/sec
empty_archetypes/par_for_each/10                                                                          1.01      8.8±0.49µs        ? ?/sec      1.00      8.7±0.31µs        ? ?/sec
empty_archetypes/par_for_each/100                                                                         1.00      8.7±0.48µs        ? ?/sec      1.04      9.0±0.34µs        ? ?/sec
empty_archetypes/par_for_each/10000                                                                       1.01     21.2±0.41µs        ? ?/sec      1.00     20.9±0.44µs        ? ?/sec
empty_commands/0_entities                                                                                 1.72      3.7±0.01ns        ? ?/sec      1.00      2.1±0.02ns        ? ?/sec
empty_systems/100_systems                                                                                 1.00     82.9±3.29µs        ? ?/sec      1.07     88.3±3.77µs        ? ?/sec
empty_systems/2_systems                                                                                   1.01      8.2±0.71µs        ? ?/sec      1.00      8.2±0.38µs        ? ?/sec
empty_systems/4_systems                                                                                   1.00      8.2±0.72µs        ? ?/sec      1.03      8.4±0.71µs        ? ?/sec
entity_hash/entity_set_build/10000                                                                        1.10     45.9±1.60µs 207.7 MElem/sec     1.00     41.6±0.39µs 229.0 MElem/sec
entity_hash/entity_set_build/3162                                                                         1.06     12.7±0.77µs 236.7 MElem/sec     1.00     12.0±0.75µs 250.6 MElem/sec
entity_hash/entity_set_lookup_hit/10000                                                                   1.02     14.5±0.30µs 658.3 MElem/sec     1.00     14.2±0.07µs 672.6 MElem/sec
entity_hash/entity_set_lookup_hit/3162                                                                    1.01      4.4±0.03µs 682.7 MElem/sec     1.00      4.4±0.01µs 691.3 MElem/sec
entity_hash/entity_set_lookup_miss_gen/10000                                                              1.01     61.3±4.12µs 155.6 MElem/sec     1.00     60.6±1.47µs 157.3 MElem/sec
entity_hash/entity_set_lookup_miss_gen/3162                                                               1.00      9.5±0.02µs 316.3 MElem/sec     1.01      9.7±0.88µs 311.7 MElem/sec
entity_hash/entity_set_lookup_miss_id/100                                                                 1.00    145.5±1.49ns 655.4 MElem/sec     1.03    149.8±1.59ns 636.7 MElem/sec
entity_hash/entity_set_lookup_miss_id/10000                                                               1.85     63.9±3.57µs 149.3 MElem/sec     1.00     34.6±3.81µs 275.8 MElem/sec
entity_hash/entity_set_lookup_miss_id/316                                                                 1.00    562.0±9.58ns 536.2 MElem/sec     1.02    573.9±1.27ns 525.1 MElem/sec
entity_hash/entity_set_lookup_miss_id/3162                                                                1.03      9.1±0.10µs 330.7 MElem/sec     1.00      8.9±0.24µs 339.0 MElem/sec
event_propagation/four_event_types                                                                        1.12    541.5±3.84µs        ? ?/sec      1.00    482.7±4.64µs        ? ?/sec
event_propagation/single_event_type                                                                       1.07   769.5±10.21µs        ? ?/sec      1.00   715.9±15.16µs        ? ?/sec
event_propagation/single_event_type_no_listeners                                                          1.56    393.4±2.89µs        ? ?/sec      1.00    251.4±3.68µs        ? ?/sec
events_iter/size_16_events_100                                                                            1.01     64.0±0.18ns        ? ?/sec      1.00     63.4±0.23ns        ? ?/sec
events_iter/size_4_events_100                                                                             1.02     64.8±0.90ns        ? ?/sec      1.00     63.4±0.24ns        ? ?/sec
events_iter/size_4_events_1000                                                                            1.01    586.5±8.00ns        ? ?/sec      1.00    579.1±4.93ns        ? ?/sec
events_send/size_16_events_100                                                                            1.00   142.7±24.34ns        ? ?/sec      1.03   147.1±28.36ns        ? ?/sec
events_send/size_16_events_10000                                                                          1.01     12.2±0.13µs        ? ?/sec      1.00     12.1±0.12µs        ? ?/sec
fake_commands/10000_commands                                                                              1.43     63.3±8.21µs        ? ?/sec      1.00     44.1±0.16µs        ? ?/sec
fake_commands/1000_commands                                                                               1.40      6.2±0.01µs        ? ?/sec      1.00      4.4±0.02µs        ? ?/sec
fake_commands/100_commands                                                                                1.38    629.4±1.69ns        ? ?/sec      1.00    457.1±0.84ns        ? ?/sec
few_changed_detection/50000_entities_ecs::change_detection::Table                                         1.00     57.7±0.86µs        ? ?/sec      1.07     61.6±1.19µs        ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Sparse                                         1.05      5.4±0.53µs        ? ?/sec      1.00      5.1±0.56µs        ? ?/sec
few_changed_detection/5000_entities_ecs::change_detection::Table                                          1.00      4.3±0.30µs        ? ?/sec      1.18      5.1±0.35µs        ? ?/sec
insert_commands/insert                                                                                    1.11   402.5±10.75µs        ? ?/sec      1.00    363.6±8.07µs        ? ?/sec
insert_commands/insert_batch                                                                              1.00    174.9±3.03µs        ? ?/sec      1.02    177.9±5.74µs        ? ?/sec
insert_simple/base                                                                                        1.04   564.1±23.01µs        ? ?/sec      1.00   544.3±60.70µs        ? ?/sec
insert_simple/unbatched                                                                                   1.32  929.3±180.10µs        ? ?/sec      1.00  704.1±132.88µs        ? ?/sec
iter_fragmented/base                                                                                      1.02    280.0±2.86ns        ? ?/sec      1.00    274.0±4.85ns        ? ?/sec
iter_fragmented/foreach                                                                                   1.00     97.3±0.42ns        ? ?/sec      1.03    100.6±3.44ns        ? ?/sec
iter_fragmented/foreach_wide                                                                              1.04      2.7±0.04µs        ? ?/sec      1.00      2.6±0.03µs        ? ?/sec
iter_fragmented_sparse/base                                                                               1.00      5.6±0.05ns        ? ?/sec      1.04      5.8±0.06ns        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Sparse    1.00   737.7±27.38µs        ? ?/sec      1.01   747.5±30.01µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Table     1.02   678.3±25.13µs        ? ?/sec      1.00   662.1±19.63µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Sparse     1.09     76.0±9.35µs        ? ?/sec      1.00     70.0±3.29µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Table      1.03     64.7±3.40µs        ? ?/sec      1.00     62.8±1.80µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_100_entities_ecs::change_detection::Table       1.02      7.6±0.12µs        ? ?/sec      1.00      7.5±0.16µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10_entities_ecs::change_detection::Sparse       1.00  1003.5±12.38ns        ? ?/sec      1.01  1013.7±32.64ns        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10_entities_ecs::change_detection::Sparse        1.03   187.1±21.18ns        ? ?/sec      1.00   181.9±22.86ns        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10_entities_ecs::change_detection::Sparse         1.00     52.8±8.19ns        ? ?/sec      1.03     54.3±8.06ns        ? ?/sec
multiple_archetypes_none_changed_detection/5_archetypes_10_entities_ecs::change_detection::Table          1.00     46.8±2.23ns        ? ?/sec      1.03     48.0±2.48ns        ? ?/sec
no_archetypes/system_count/0                                                                              1.00     16.3±0.17ns        ? ?/sec      1.02     16.6±0.16ns        ? ?/sec
no_archetypes/system_count/100                                                                            1.02    851.5±9.32ns        ? ?/sec      1.00    832.9±7.93ns        ? ?/sec
none_changed_detection/5000_entities_ecs::change_detection::Sparse                                        1.00      3.4±0.04µs        ? ?/sec      1.02      3.5±0.05µs        ? ?/sec
nonempty_spawn_commands/10000_entities                                                                    1.89    261.1±6.99µs        ? ?/sec      1.00    137.8±8.47µs        ? ?/sec
nonempty_spawn_commands/1000_entities                                                                     1.90     26.4±3.18µs        ? ?/sec      1.00     13.9±2.38µs        ? ?/sec
nonempty_spawn_commands/100_entities                                                                      1.87      2.6±0.07µs        ? ?/sec      1.00  1388.8±97.31ns        ? ?/sec
observe/trigger_simple                                                                                    1.09    347.5±1.51µs        ? ?/sec      1.00    317.7±2.62µs        ? ?/sec
observe/trigger_targets_simple/10000_entity                                                               1.04   696.5±15.50µs        ? ?/sec      1.00   672.0±13.88µs        ? ?/sec
par_iter_simple/with_0_fragment                                                                           1.01     34.4±0.51µs        ? ?/sec      1.00     33.9±0.53µs        ? ?/sec
par_iter_simple/with_1000_fragment                                                                        1.04     45.5±0.93µs        ? ?/sec      1.00     43.9±1.85µs        ? ?/sec
par_iter_simple/with_100_fragment                                                                         1.03     36.2±0.50µs        ? ?/sec      1.00     35.1±0.44µs        ? ?/sec
par_iter_simple/with_10_fragment                                                                          1.03     37.5±0.97µs        ? ?/sec      1.00     36.5±0.74µs        ? ?/sec
param/combinator_system/8_dyn_params_system                                                               1.00     10.4±0.73µs        ? ?/sec      1.01     10.5±0.79µs        ? ?/sec
param/combinator_system/8_piped_systems                                                                   1.05      8.0±0.65µs        ? ?/sec      1.00      7.6±0.57µs        ? ?/sec
query_get/50000_entities_sparse                                                                           1.06    136.7±0.35µs        ? ?/sec      1.00    128.6±0.44µs        ? ?/sec
query_get_many_10/50000_calls_sparse                                                                      1.02  1649.4±77.80µs        ? ?/sec      1.00  1614.4±78.91µs        ? ?/sec
query_get_many_2/50000_calls_sparse                                                                       1.00    191.3±3.66µs        ? ?/sec      1.01    193.3±0.75µs        ? ?/sec
query_get_many_2/50000_calls_table                                                                        1.00    243.9±0.55µs        ? ?/sec      1.05    257.2±8.62µs        ? ?/sec
query_get_many_5/50000_calls_sparse                                                                       1.00    585.9±7.70µs        ? ?/sec      1.03    600.6±5.99µs        ? ?/sec
query_get_many_5/50000_calls_table                                                                        1.00    673.7±7.44µs        ? ?/sec      1.07   722.3±10.77µs        ? ?/sec
run_condition/no/1000_systems                                                                             1.00     23.7±0.06µs        ? ?/sec      1.06     25.1±0.07µs        ? ?/sec
run_condition/no/100_systems                                                                              1.00   1460.5±4.28ns        ? ?/sec      1.03   1510.1±3.69ns        ? ?/sec
run_condition/no/10_systems                                                                               1.00    201.5±0.53ns        ? ?/sec      1.04    209.1±2.37ns        ? ?/sec
run_condition/yes/1000_systems                                                                            1.00  1225.7±22.58µs        ? ?/sec      1.02  1253.7±24.90µs        ? ?/sec
run_condition/yes/100_systems                                                                             1.02     89.4±3.43µs        ? ?/sec      1.00     88.0±3.96µs        ? ?/sec
run_condition/yes_using_query/1000_systems                                                                1.00  1288.3±26.57µs        ? ?/sec      1.03  1323.0±24.73µs        ? ?/sec
run_condition/yes_using_query/100_systems                                                                 1.00    108.8±2.51µs        ? ?/sec      1.03    112.3±3.09µs        ? ?/sec
run_condition/yes_using_resource/100_systems                                                              1.03     99.0±3.37µs        ? ?/sec      1.00     96.2±4.80µs        ? ?/sec
run_empty_schedule/MultiThreaded                                                                          1.03     15.3±0.10ns        ? ?/sec      1.00     14.9±0.03ns        ? ?/sec
run_empty_schedule/Simple                                                                                 1.01     15.2±0.15ns        ? ?/sec      1.00     15.0±0.25ns        ? ?/sec
sized_commands_0_bytes/10000_commands                                                                     1.57     52.6±0.41µs        ? ?/sec      1.00     33.5±0.10µs        ? ?/sec
sized_commands_0_bytes/1000_commands                                                                      1.57      5.3±0.01µs        ? ?/sec      1.00      3.4±0.00µs        ? ?/sec
sized_commands_0_bytes/100_commands                                                                       1.56    536.5±4.83ns        ? ?/sec      1.00    343.6±1.12ns        ? ?/sec
sized_commands_12_bytes/10000_commands                                                                    1.22     63.0±0.53µs        ? ?/sec      1.00     51.5±6.06µs        ? ?/sec
sized_commands_12_bytes/1000_commands                                                                     1.25      5.7±0.01µs        ? ?/sec      1.00      4.6±0.05µs        ? ?/sec
sized_commands_12_bytes/100_commands                                                                      1.27    579.3±1.28ns        ? ?/sec      1.00    455.4±0.85ns        ? ?/sec
sized_commands_512_bytes/10000_commands                                                                   1.11   248.4±85.81µs        ? ?/sec      1.00   224.3±52.11µs        ? ?/sec
sized_commands_512_bytes/1000_commands                                                                    1.09     22.8±0.18µs        ? ?/sec      1.00     21.0±0.17µs        ? ?/sec
sized_commands_512_bytes/100_commands                                                                     1.13  1852.2±11.21ns        ? ?/sec      1.00   1635.3±4.91ns        ? ?/sec
spawn_commands/10000_entities                                                                             1.04   844.2±11.96µs        ? ?/sec      1.00   811.5±13.25µs        ? ?/sec
spawn_commands/1000_entities                                                                              1.05     84.9±3.66µs        ? ?/sec      1.00     80.5±4.13µs        ? ?/sec
spawn_commands/100_entities                                                                               1.06      8.6±0.12µs        ? ?/sec      1.00      8.1±0.12µs        ? ?/sec
spawn_world/10000_entities                                                                                1.03   413.2±25.20µs        ? ?/sec      1.00   400.9±49.97µs        ? ?/sec
spawn_world/100_entities                                                                                  1.02      4.1±0.62µs        ? ?/sec      1.00      4.1±0.69µs        ? ?/sec
spawn_world/1_entities                                                                                    1.04     42.2±3.23ns        ? ?/sec      1.00     40.6±6.81ns        ? ?/sec
world_entity/50000_entities                                                                               1.18     88.3±0.42µs        ? ?/sec      1.00     74.7±0.16µs        ? ?/sec
world_get/50000_entities_sparse                                                                           1.02    182.2±0.32µs        ? ?/sec      1.00    179.5±0.84µs        ? ?/sec
world_get/50000_entities_table                                                                            1.01    198.3±0.46µs        ? ?/sec      1.00    196.2±0.63µs        ? ?/sec
world_query_for_each/50000_entities_sparse                                                                1.00     32.7±0.12µs        ? ?/sec      1.01     33.1±0.46µs        ? ?/sec

This roughly doubles command spawning speed! Despawning also sees a 20-30% improvement. Dummy commands improve by 10-50% (due to not needing an entity flush). Other benchmarks seem to be noise and are negligible. It looks to me like a massive performance win!

@ElliottjPierce ElliottjPierce added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events D-Complex Quite challenging from either a design or technical perspective. Ask for help! X-Controversial There is active debate or serious implications around merging this PR labels May 31, 2025
@ElliottjPierce ElliottjPierce marked this pull request as draft May 31, 2025 19:54
/// This can error if the [`EntityGeneration`] of this id has passed or if the [`EntityRow`] is not constructed.
/// See the module docs for a full explanation of these ids, entity life cycles, and the meaning of this result.
#[inline]
pub fn get_constructed(
Copy link
Contributor

Choose a reason for hiding this comment

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

A lot of the churn above seems to be replacing calls to get with get_constructed. Should get_constructed be the default?

Or, should this be the only method (and renamed to get)? If I'm reading these branches correctly, they return the same data in a different format. That is, I think get could be

pub fn get(&self, entity: Entity) -> Result<EntityIdLocation, EntityDoesNotExistError> {
    match self.get_constructed(entity) {
        Ok(location) => Ok(Some(location)),
        Err(ConstructedEntityDoesNotExistError::WasNotConstructed { .. }) => Ok(None),
        Err(ConstructedEntityDoesNotExistError::DidNotExist(err)) => Err(err),
    }
}

and I think that match could then be inlined cleanly into most callers, especially ones that do things like if let Ok(None) = self.world.entities.get(self.entity) {.

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 thoughts. For now I just did the match. I'd like to keep both for utility purposes, but I'm open to ideas and renames.

// SAFETY: Caller guarantees that `index` a valid entity index
let meta = unsafe { self.meta.get_unchecked_mut(index as usize) };
meta.spawned_or_despawned = SpawnedOrDespawned { by, at };
pub(crate) unsafe fn declare(
Copy link
Contributor

Choose a reason for hiding this comment

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

The name declare is a little confusing without context. And then it's implemented in terms of update, which seems to be another word for the same thing.

I think this this is setting the location, and it seems to be the equivalent of the old set method. Could this be set or set_location, and fn update be set_unchecked or set_location_unchecked?

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 don't have a preference here. In my mind, declare means "this is location is true; make it happen" and update means "this is a revision, just change the value". But that doesn't mean there aren't better names. There's a lot of naming questions here in general. I'm open to suggestions, but I also don't want to make this pr blocked on name discussions. I'd rather merge with a bad less-good name and update it later.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't have a preference here. In my mind, declare means "this is location is true; make it happen" and update means "this is a revision, just change the value". But that doesn't mean there aren't better names. There's a lot of naming questions here in general. I'm open to suggestions, but I also don't want to make this pr blocked on name discussions. I'd rather merge with a bad less-good name and update it later.

Yeah, I don't want to block on naming questions! I was just confused while starting to read the PR why entities.set(entity.index(), Some(location)); changed to let was_at = entities.declare(entity.row(), Some(location));. It would have been easier to read if the name hadn't changed, but that doesn't really matter now.

pub unsafe fn flush(
/// - `row` must have been constructed at least once, ensuring its row is valid.
#[inline]
pub(crate) unsafe fn mark_construct_or_destruct(
Copy link
Contributor

Choose a reason for hiding this comment

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

You changed the name of this method from mark_spawn_despawn to mark_construct_or_destruct, but it's still updating a field called spawned_or_despawned. I'd vote for reverting the method name change to reduce churn, but if we really want to change the method name then shouldn't we also change the field name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are good questions. I mentioned this early in the PR discussion (now hidden it was so long ago).

Basically, we need to find a balance between intuitive and easy to migrate (spawn/despawn) and precisely correct (construct/destruct). Using both in different places is one way I tried to strike the balance. Higher-level stuff is named spawn/despawn and lower level stuff is named construct/destruct. (In general.) But other ideas for how to handle this are welcome.

Copy link
Contributor

Choose a reason for hiding this comment

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

These are good questions. I mentioned this early in the PR discussion (now hidden it was so long ago).

Oh, I see, sorry!

Basically, we need to find a balance between intuitive and easy to migrate (spawn/despawn) and precisely correct (construct/destruct). Using both in different places is one way I tried to strike the balance. Higher-level stuff is named spawn/despawn and lower level stuff is named construct/destruct. (In general.) But other ideas for how to handle this are welcome.

Yeah, that seems like a good approach! It just seems odd for a field and the method that sets it to be on opposite sides of the high-level/low-level boundary :). Not a blocker, though!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It just seems odd for a field and the method that sets it to be on opposite sides of the high-level/low-level boundary.

100% agree. I just don't want to rename everything in one pr.

let index = row.index() as usize;
if self.meta.len() <= index {
// TODO: hint unlikely once stable.
expand(&mut self.meta, index + 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just call meta.resize(len, EntityMeta::FRESH); directly? I would expect Vec::resize to already implement optimizations for making actual resizing be cold, since it's called during things like push.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Two reasons: First, this lets us also init the whole capacity, which reduces these calls, and second, turns out resize is not marked as cold. At least, I didn't see it. IDK why not.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess I'm asking whether you benchmarked this part or are just theorizing. If there's a measured performance benefit to the complexity here then we should absolutely do it! But otherwise I'd prefer the code be smaller and simpler.

I think the #[cold] in Vec::reserve is buried inside RawVec: https://github.com/rust-lang/rust/blob/048b8790916ee2b1baf4dea62c2c8367aedb0d0c/library/alloc/src/raw_vec/mod.rs#L546

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that resize can also shrink the Vec. Not sure if this can actually happen here though.

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 the #[cold] in Vec::reserve is buried inside RawVec: https://github.com/rust-lang/rust/blob/048b8790916ee2b1baf4dea62c2c8367aedb0d0c/library/alloc/src/raw_vec/mod.rs#L546

Oh interesting.

I guess I'm asking whether you benchmarked this part or are just theorizing. If there's a measured performance benefit to the complexity here then we should absolutely do it! But otherwise I'd prefer the code be smaller and simpler.

I am just theorizing performance, but I think the theory is right. Prior to this PR, Entities::set was a get_unchecked. Now update is get_unchecked, and declare needs bounds checking. I think skipping those checks for things like inserting components (which only needs update) is worth it. And initing the whole capacity with the default value instead of doing that incrementally means this is called 32 times max instead of 2 ^ 32 times. So I think it's worth it. And I don't think I can get precise enough benches to be meaningful here anyway; I'm not sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that resize can also shrink the Vec. Not sure if this can actually happen here though.

Yeah, and extend_with is private. I'm not aware of an alternative.

{
let archetype = &mut world.archetypes[location.archetype_id];
let archetype = &mut self.world.archetypes[location.archetype_id];
Copy link
Contributor

Choose a reason for hiding this comment

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

Removing the let world = self.world; added a lot of churn to this method. It's a good change, but I'd prefer it be done as a separate PR to make this one smaller.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The old one method took self, so it could be easily moved out. The new method takes &mut self (because it could be constructed again later), so reverting this would be challenging with lifetimes I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

The old one method took self, so it could be easily moved out. The new method takes &mut self (because it could be constructed again later), so reverting this would be challenging with lifetimes I think.

Oh, I didn't see that! I think it still works if you reborrow with let world = &mut self.world;, though.

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 it still works if you reborrow with let world = &mut self.world;, though.

Lol. Forgot I could do that. That would be double indirection though (&mut &mut World). The compiler would probably unroll that, but I'm not sure. If you think it would be a purely aesthetic change, I can revert it. It does cause a lot of noise in the diff for that function.

Copy link
Contributor

Choose a reason for hiding this comment

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

That would be double indirection though (&mut &mut World).

Oh, right, it would need to be let world = &mut *self.world; to reborrow.

If you think it would be a purely aesthetic change, I can revert it. It does cause a lot of noise in the diff for that function.

I would personally revert it to reduce the diff, but I don't actually feel strongly. I only keep responding because you keep pointing out the flaws in my plan and so I am compelled to fix them :).

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 would personally revert it to reduce the diff, but I don't actually feel strongly. I only keep responding because you keep pointing out the flaws in my plan and so I am compelled to fix them :).

Lol. I'll just keep it as is then. I definitely should have tried to reduce the diff, but after 7 reviews, I think it's too little too late to revet now.

Ok(fetched) => fetched,
Err(error) => panic_no_entity(self, error.entity),
Ok(res) => res,
Err(err) => panic!("{err}"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this method now be shortened to self.get_entity(entities).unwrap()?

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 would tag along the "calling unwrap on a None value" message. Which it didn't before, but in principal, yeah.

/// Returns the [`EntityCommands`] for the requested [`Entity`] if it exists in the world *now*.
/// Note that for entities that have not been constructed, like ones from [`spawn`](Self::spawn), this will error.
/// If that is not desired, try [`get_entity`](Self::get_entity).
/// This should be used over [`get_entity`](Self::get_entity) when you expect the entity to already exist constructed in the world.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean get_constructed_entity should be the one that users should usually use? If that's the case, then it might be good to give that behavior the short name of get_entity and rename get_entity to something longer. (Do we have a good term for a constructed-or-allocated entity?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You've mentioned this too with Entities::get vs get_constructed. They are both useful for different things. I would use get_constructed_entity when doing commands for an entity in a query. But for almost everything else, I would use get_entity, so it includes those that are not yet constructed.

I don't have a strong preference for where we settle with the naming. But from my (loose) experience updating internal usages of this, they are roughly equally useful. For example, consider an entity being piped through an event: Even tough those will almost always be constructed, we don't want bevy_systems to break down when they aren't.

These command methods are meant to catch "definitely an issue" cases, and even if get_constructed_entity fails, it is very possible that by the time the command would have been applied, the entity would be constructed. I mostly just added it for parity.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would use get_constructed_entity when doing commands for an entity in a query. But for almost everything else, I would use get_entity, so it includes those that are not yet constructed.

I don't have a strong preference for where we settle with the naming. But from my (loose) experience updating internal usages of this, they are roughly equally useful. For example, consider an entity being piped through an event: Even tough those will almost always be constructed, we don't want bevy_systems to break down when they aren't.

What sort of user code needs to interact with entities that are allocated but not constructed? I think doing commands for an entity in a query is the common case. Apart from commands.spawn(), of course, but that already gives you an EntityCommands.

Or, wait, didn't we rework these now that we have error handling in commands? ... Ah, I guess commands.entity() is the common case. It doesn't do any eager checks, since the entity might get despawned before the commands run. That makes get_entity feel even more niche. If I care enough to check eagerly, then it's probably not because I expect an allocated-but-not-constructed entity! So I'd vote for getting rid of the get_entity behavior and renaming get_constructed_entity to get_entity.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I don't think commands.get_entity is a very useful API in general.

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'm broadly on board with these ideas. What do we want to do here? A few options

  1. Keep it as is and sort out renaming latter. (Most options)
  2. Remove get_entity and rename get_constructed_entity to get_entity. (Simplest)
  3. Do option 2 but keep get_entity under a new name: get_maybe_constructed_entity? (Good, but starts naming battles with parity, etc.)
  4. Do another pass later to move this error handling into Commands::entity itself since get_entity isn't that helpful. (It just catches stuff slightly earlier.) (Probably best long-term IMO)

Copy link
Member

Choose a reason for hiding this comment

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

I would prefer 4. This PR is already too complex.

/// The next id to hand out is tracked by `free_len`.
free: Vec<Entity>,
/// This is continually subtracted from.
/// If it wraps to a very large number, it will be outside the bounds of `free`,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is really subtle. The old code used signed numbers here so that it could use a sign check before casting, which made things a little more clear. Why switch to unsigned?

Do we need to worry about overflow if the free list is very large? The old code used 64-bit counters when available to prevent that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's really three reasons for this:

  1. We know that the free list doesn't contain duplicates of an entity row, so the total length can now fit in 32 bits.
  2. I didn't like how the whole allocator cursor situation changed based on pointer width.
  3. I would guess not needing to cast signed to unsigned with the check is maybe part of the perf improvements.

If you would prefer, I could switch this to do the same trick but with an AtomicUsize to make reason 1 a moot point. Open to suggestions.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you would prefer, I could switch this to do the same trick but with an AtomicUsize to make reason 1 a moot point. Open to suggestions.

That sounds reasonable to me! I didn't like the cfgs there, either. usize gets us protection on common 64-bit platforms, and it's going to be hard to actually overflow on 32-bit platforms without running out of memory.

But it might be worth looking at the history of why that was done originally to make sure we aren't missing something!

  1. I would guess not needing to cast signed to unsigned with the check is maybe part of the perf improvements.

This would really surprise me. Signed and unsigned values usually have the same representation, and the high-level types just affect what instruction the arithmetic operations get compiled to. But I'm often surprised by the results of profilers :).

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 sounds reasonable to me! I didn't like the cfgs there, either. usize gets us protection on common 64-bit platforms, and it's going to be hard to actually overflow on 32-bit platforms without running out of memory.

I'll do that then!

But it might be worth looking at the history of why that was done originally to make sure we aren't missing something!

I've looked at this for a while, and I don't think there's anything spooky going on here. Could be wrong though.

This would really surprise me. Signed and unsigned values usually have the same representation, and the high-level types just affect what instruction the arithmetic operations get compiled to. But I'm often surprised by the results of profilers :).

Honestly, me too. I'm not sure if the x > 0 is any slower than the x < len. But I do just generally prefer unsigned. IDK why.

Copy link
Contributor

Choose a reason for hiding this comment

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

But it might be worth looking at the history of why that was done originally to make sure we aren't missing something!

Found some context from three years ago: #4452 (comment)

Is there a reason to not use isize/AtomicIsize everywhere?

We want the smallest signed integer big enough to contain u32::MAX.

That is i64. It just so happens that some platforms don't support 64 bit atomics, so for those rare cases, it's better to fall back on isize, which (since we use at least 4 bytes of overhead per entity) is definitely big enough to contain as many entities as can be spawned (u32::MAX, but capped at usize::MAX/4).

The reason not to use isize everywhere is that it's wasteful on 128 bit platforms. Admittedly, I don't know of any 128 bit platforms, but I'd rather keep it as the condition explained in my first paragraph.

And I think the consensus is that we don't guarantee it works. We just don't intentionally stand in the way of it not working (without good reason, anyway). In rustc parlance, these are tier 3 targets, i.e. it might be supported, and you can bring PRs to fix the support, but we don't try and ensure it is.

@alice-i-cecile alice-i-cecile changed the title Remove entity reserving/pending/flushing system Construct and deconstruct entities to improve entity allocation Jul 9, 2025
@urben1680
Copy link
Contributor

Does this fix #19012? I probably cannot test before the weekend.

let source_archetype = source_entity.archetype();
let source_archetype = source_entity
.archetype()
.expect("Source entity must exist constructed");
Copy link
Member

Choose a reason for hiding this comment

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

Are these expects actually more useful than an unwrap with a comment above?

,expect() wastes binary space with strings, often gives worse error messages, and don't get automatically updated as the error type changes.

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 ended up collapsing them together. Now, instead of "expect the entity exists" and "expect it is constructed" it is just one "expect the entity exists and is constructed". I think that strikes a good balance.

pub(crate) unsafe fn declare(
&mut self,
row: EntityRow,
location: EntityIdLocation,
Copy link
Member

Choose a reason for hiding this comment

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

Changing this to new_location would help me understand how this function works a lot faster :)

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 one of those times when I go "why didn't I think of that?". Now it's new_location and update_location.

Copy link
Contributor

Choose a reason for hiding this comment

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

Judging that marking one line in github shows the three previous lines here, I think the suggestion was renaming the parameter, not the method. But it is still better 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.

lol. I didn't notice that. 😂 But yeah, I think this is much better than before.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Dramatically clearer than your initial attempts, and I'm sold on the functionality and core model. I'm also extremely pleased that we now have a number of ECS contributors who understand what's going on here. Thanks y'all!

I've left a few nits around docs and unwraps, but fundamentally nothing blocking. I'd also like to try and incorporate that lovely diagram into the entity module docs, but that's fine in follow-up.

The most important follow-up work for this is cleaning up commands.get_entity, but that can and should be done separately.

Copy link
Contributor

@Trashtalk217 Trashtalk217 left a comment

Choose a reason for hiding this comment

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

What an Odyssee this is.

I understand this PR and while I approve it, I do have two notes:

  • I think these names could use some bikeshedding, especially EntityRow is confusing to me. I also think the docs could be more clear in places.
  • Secondly, I want to take a closer look at the seperation of responsibillities between EntityAllocator and Entities. Do they need to be seperate structs? Can the former be nested in the latter?

I'm worried that the average user will find this mighty confusing, but I say this with the caveat that I've been staring af this code for a couple hours and the average user might never have to, so keep that in mind.

//! This column doesn't represents a component and is specific to the [`EntityRow`], not the [`Entity`].
//! For example, one thing Bevy stores in this metadata is the current [`EntityGeneration`] of the row.
//! It also stores more information like the [`Tick`] a row was last constructed or destructed, and the [`EntityIdLocation`] itself.
//! For more information about what's stored here, see [`Entities`], Bevy's implementation of this special column.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this documentation is a good start, but I think a more direct route can be taken, along the lines of

"Entity spawning is done in two stages: First we create an entity id (alloc step), and then we construct it (add components and other metadata). The reason for this is that we need to be able to assign entity ids concurrently, while for construction we need exclusive (non-concurrent) access to the world. This leads to an entity having three states: It doesn't exist (unallocated), a id has been assigned but is not known to the rest of the world (null), and an entity that has been fully created (constructed)."

This, to me, seems like a much simpler mental model (even if it's partially incorrect). I think this documentation is fine for now, but it might be worth looking over again 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.

I like the clarity here. Yes, we need another docs pass, and book pass, etc. But for now, I adapted this into the storage docs. It was too good not to!

@ElliottjPierce
Copy link
Contributor Author

Does this fix #19012? I probably cannot test before the weekend.

@urben1680: Yes and no.

No, it doesn't fix that. Technically, a freed entity does still exist. That is correct. The only requirement for an entity to exist is that the generation is up to date. This is intended behavior. For example, it could allow checking if the entity exists, and then constructing it. (In the particular case of a freed entity, this would cause errors when that entity is allocated, but resolve_from_row and contains are functioning correctly.)

Yes, it does add a contains_constructed which will return false when a freshly freed and re-resolved row is passed. Both contains and contains_constructed are useful in different ways. Docs and naming could maybe be improved here to make the uses more obvious.

@ElliottjPierce
Copy link
Contributor Author

What an Odyssee this is.

I know, right?

I understand this PR and while I approve it, I do have two notes:

  • I think these names could use some bikeshedding, especially EntityRow is confusing to me. I also think the docs could be more clear in places.

Agreed. The more I think about it, the more "row" -> "index" renames make sense to me. And definitely more docs are needed in future PRs.

  • Secondly, I want to take a closer look at the seperation of responsibillities between EntityAllocator and Entities. Do they need to be seperate structs? Can the former be nested in the latter?

Yes, they can be together. In fact, originally, they were. But ultimately, they do very different things. There is no overlap of responsibility between them, and, more importantly, for remote reservation, the allocator needs to operate completely independently of Entities information. Separating them enforces that requirement until remote entity allocation lands.

As a side note, I see EntitiesAllocator evolving a lot here. I would guess it will end up containing multiple allocator kinds, batched freeing, etc. But we'll see what happens.

I'm worried that the average user will find this mighty confusing, but I say this with the caveat that I've been staring af this code for a couple hours and the average user might never have to, so keep that in mind.

I hear what you're saying here, but I don't think this will be an issue. Before, we had, alloc and reserve/flush. Now, we have alloc/construct. If anything, it's kinda simpler. I think if users didn't know about the reserve/flush scheme, they won't need to know about the new one, and if they did know the old one, they will learn the new one pretty easily. At least, that's my guess.

@urben1680
Copy link
Contributor

urben1680 commented Jul 10, 2025

Yes, it does add a contains_constructed which will return false when a freshly freed and re-resolved row is passed. Both contains and contains_constructed are useful in different ways. Docs and naming could maybe be improved here to make the uses more obvious.

Hm with two separate methods that distinction becomes more obvious to me which makes me lean more to the "yes" part of your answer.

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-Feature A new feature, making something new possible C-Performance A change motivated by improving speed, memory usage or compile times D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-SME Decision or review from an SME is required X-Controversial There is active debate or serious implications around merging this PR
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

10 participants