Skip to content

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
@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.

@james7132 james7132 self-requested a review August 18, 2025 20:07
Copy link
Member

@james7132 james7132 left a comment

Choose a reason for hiding this comment

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

This is only a cursory clean through the high level details. In particular, I haven't looked at the entities/mod.rs changes yet.

Conceptually, I like the idea and construction, especially since it gets rid of the pesky flushes that we were doing earlier.. However, this definitely comes with a lot more user-facing complexity that was otherwise siloed away, which, to me, violates Bevy's general policy of progressive disclosure. We may need to massage the API to either help check for some of errors statically, or in absence of that, move some of the new APIs into harder to reach for spaces to ensure users opt-in to the additional complexity..

I'll give this a more complete review before the end of October (will be unavailable for the majority of the next two weeks).

TODO on my end:

  • Review the entities/mod..rs changes.
  • Locally benchmark to validate the perf improvements seen in the PR description
  • Run a quick check against bevy_asm_tests to check the codegen difference.

/// All the entities to reuse.
/// This is a buffer, which contains an array of [`Entity`] ids to hand out.
/// The next id to hand out is tracked by `free_len`.
free: Vec<Entity>,
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to store the generation too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically, no. This could just store row information. But then, Entities and EntitiesAllocator would need to be merged back together, which is not ideal. Separating them is necessary for remote entity reservation. You need to be able to hold the allocator without holding any reference to the world. But I think separating them is the job of this PR. It makes it very clear that the allocation stage is the job of EntitiesAllocator, and the construction stage is the job of Entities (and storage, etc).

pub fn new_from_entities(queue: &'s mut CommandQueue, entities: &'w Entities) -> Self {
pub fn new_from_entities(
queue: &'s mut CommandQueue,
allocator: &'w EntitiesAllocator,
Copy link
Member

Choose a reason for hiding this comment

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

Is there any requirement that these two come from the same World?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In theory, no. Though IIRC, it could cause a panic.

///
/// # Safety
///
/// * Caller ensures that `queue` must outlive `'w`
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to be updated?

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's been a while since I looked at this, but I don't see anything wrong about this as it is...

#[inline(never)]
#[cold]
#[track_caller]
fn panic_no_entity(world: &World, entity: Entity) -> ! {
Copy link
Member

Choose a reason for hiding this comment

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

Was this removal necessary? This makes the error path no longer cold.

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 guess we could keep this, but panic should be cold anyway I think, so I don't think it would make a big difference. I think the old one had to be marked as cold because otherwise it would include entity_does_not_exist_error_details in the asm, but the new one doesn't have that problem. Still, its hard to predict how it compiles, so could be worth keeping anyway, Idk.

/// // treat it as a normal entity
/// entity_access.despawn();
/// ```
pub fn spawn_null(&self) -> Entity {
Copy link
Member

Choose a reason for hiding this comment

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

Not a fan of this naming, but I can't really come up with an alternative off the top of my head.

/// however, doing so is currently a bad idea as the allocator may hand out this entity row in the future, assuming it to be not constructed.
/// This would cause a panic.
///
/// Manual construction is a powerful tool, but must be used carefully.
Copy link
Member

Choose a reason for hiding this comment

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

Like Alice's earlier comments, I'm a bit conflicted. This is rather advanced lifecycle manipulation that the overwhelming majority of Bevy devs should never need to touch. Moving these functions to somewhere that is intentionally harder to reach for seems like something we should do. Perhaps not in this PR, but definitely before this lands in a public release.

I'm also not sure if we should have a separate type for this, also wrapping UnsafeEntityCell, to encode this as a type-state instead of relying on runtime checks.

Co-authored-by: James Liu <contact@jamessliu.com>
@ElliottjPierce
Copy link
Contributor Author

Lots more good review from @james7132 !

From what I can tell, there are 3 outstanding concerns:

First, does the performance and asm still hold up? Yes this needs more testing just because its been so long since this pr opened.

Second, is this too complex for users? No. It doesn't increase complexity at all. It just changes alloc -> flush -> insert to alloc -> construct. If anything, that's simpler, albeit lower-level. The "api surface area" doesn't really change here IMO.

Third, how should we name this and stop users from using this directly? IIRC (its been a few months) there has been discussion of putting "risky" or even unsafe in these functions. James mentioned moving the functions to a different location even. IMO, we should keep this more or less where it is. I think the "null" in these names is enough to turn off new users from using it before they know what they're doing. (I agree that there may be better names, and I'm always open to them, but I don't see anything "wrong" with where it is now.) I also liked how this was summed up as maybe "violates Bevy's general policy of progressive disclosure." That's well put. Maybe that's true; Idk. That's not for me to decide.

What I can say is that I would oppose hiding the construct function family too well. I think it will be used more than you might guess. For example, with entity paging, you could know that entity ids from over the network are never used in the allocator. (Ex: maybe you allocate and leak them as part of the handshake with the server.) Now for your multiplayer game, you don't need to map between client and server entity ids; they are the same! But you can't spawn an entity on the client to mirror the server, since that would allocate a new entity id. Instead, you would need to construct them! In a heavily multiplayer game, user's calls to construct may even outnumber their calls to spawn.

What I mean to say is that I don't see allocation and construction as a low-level primitive for the engine that just happens to be cleaner than before. I think these tools are just as useful to users as they are to the engine.

As a small rant: Bevy markets itself as "simple," which sounds nice. But when you use a simple interface to do something fundamentally complex, it becomes really annoying! Want to load an asset? One line of code; easy. Want to do so with a tiny, simple loading screen? Time to bust out state machines, events, and all manner of chaos. I would much rather expose 100% of complexity to users and give them the tools to name and handle that complexity, than hide all that complexity behind simple but limiting interfaces. (I know this is not my decision to make, and for good reason, but I can't help but bring it up, as it's by far my biggest problem with Bevy and is what moved me from a Bevy user to a Bevy contributor.)

On a completely different note, this pr is months old at this point, has a huge diff, and has so many comments and commits that it's starting to lag github (at least for me). Even if I could resolve all these merge conflicts, the result would be so different in implementation (same in concept of course) that all previous code review would probably need to be redone. When Bevy is ready for this, just let me know, and I will revive this I think on a new branch. And this time, I'll do my very best to keep the diff small, though that is not easy with a change so big. That said, I think getting the concept fully reviewed and agreed upon is still worth the work now, so I appreciate that very much. I feel like this pr has almost become more of an RFC than an implementation lol.

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.