Skip to content

Commit 279a867

Browse files
ElliottjPiercealice-i-cecileItsDooturben1680chescock
authored
Improved Entity Lifecycle: remove flushing, support manual spawning and despawning (#19451)
# 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](https://github.com/user-attachments/assets/3e5397ab-4ec6-4477-91de-3d002d0cf92e) (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 ```rust let e4 = world.spawn_null(); world .entity_mut(e4) .construct((TableStored("junk"), A(0))) .unwrap() .destruct() .construct((TableStored("def"), A(456))) .unwrap(); ``` ## Future Work - [x] 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 <details> <summary>Benchmarks</summary> ```txt 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 ``` </details> 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! --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Christian Hughes <9044780+ItsDoot@users.noreply.github.com> Co-authored-by: urben1680 <55257931+urben1680@users.noreply.github.com> Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com> Co-authored-by: Trashtalk217 <24552941+Trashtalk217@users.noreply.github.com> Co-authored-by: James Liu <contact@jamessliu.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
1 parent 985aa40 commit 279a867

File tree

42 files changed

+1503
-1281
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1503
-1281
lines changed

benches/benches/bevy_ecs/world/entity_hash.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ fn make_entity(rng: &mut impl Rng, size: usize) -> Entity {
2020
let id = id as u64 + 1;
2121
let bits = ((generation as u64) << 32) | id;
2222
let e = Entity::from_bits(bits);
23-
assert_eq!(e.index(), !(id as u32));
23+
assert_eq!(e.index_u32(), !(id as u32));
2424
assert_eq!(
2525
e.generation(),
2626
EntityGeneration::FIRST.after_versions(generation as u32)

crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ impl EntityCountDiagnosticsPlugin {
4343

4444
/// Updates entity count measurement.
4545
pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) {
46-
diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.len() as f64);
46+
diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.count_spawned() as f64);
4747
}
4848
}

crates/bevy_ecs/src/bundle/insert.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ impl<'w> BundleInserter<'w> {
231231
if let Some(swapped_entity) = result.swapped_entity {
232232
let swapped_location =
233233
// SAFETY: If the swap was successful, swapped_entity must be valid.
234-
unsafe { entities.get(swapped_entity).debug_checked_unwrap() };
235-
entities.set(
234+
unsafe { entities.get_spawned(swapped_entity).debug_checked_unwrap() };
235+
entities.update_existing_location(
236236
swapped_entity.index(),
237237
Some(EntityLocation {
238238
archetype_id: swapped_location.archetype_id,
@@ -243,7 +243,7 @@ impl<'w> BundleInserter<'w> {
243243
);
244244
}
245245
let new_location = new_archetype.allocate(entity, result.table_row);
246-
entities.set(entity.index(), Some(new_location));
246+
entities.update_existing_location(entity.index(), Some(new_location));
247247
bundle_info.write_components(
248248
table,
249249
sparse_sets,
@@ -280,8 +280,8 @@ impl<'w> BundleInserter<'w> {
280280
if let Some(swapped_entity) = result.swapped_entity {
281281
let swapped_location =
282282
// SAFETY: If the swap was successful, swapped_entity must be valid.
283-
unsafe { entities.get(swapped_entity).debug_checked_unwrap() };
284-
entities.set(
283+
unsafe { entities.get_spawned(swapped_entity).debug_checked_unwrap() };
284+
entities.update_existing_location(
285285
swapped_entity.index(),
286286
Some(EntityLocation {
287287
archetype_id: swapped_location.archetype_id,
@@ -295,15 +295,15 @@ impl<'w> BundleInserter<'w> {
295295
// redundant copies
296296
let move_result = table.move_to_superset_unchecked(result.table_row, new_table);
297297
let new_location = new_archetype.allocate(entity, move_result.new_row);
298-
entities.set(entity.index(), Some(new_location));
298+
entities.update_existing_location(entity.index(), Some(new_location));
299299

300300
// If an entity was moved into this entity's table spot, update its table row.
301301
if let Some(swapped_entity) = move_result.swapped_entity {
302302
let swapped_location =
303303
// SAFETY: If the swap was successful, swapped_entity must be valid.
304-
unsafe { entities.get(swapped_entity).debug_checked_unwrap() };
304+
unsafe { entities.get_spawned(swapped_entity).debug_checked_unwrap() };
305305

306-
entities.set(
306+
entities.update_existing_location(
307307
swapped_entity.index(),
308308
Some(EntityLocation {
309309
archetype_id: swapped_location.archetype_id,

crates/bevy_ecs/src/bundle/remove.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,9 @@ impl<'w> BundleRemover<'w> {
228228
.swap_remove(location.archetype_row);
229229
// if an entity was moved into this entity's archetype row, update its archetype row
230230
if let Some(swapped_entity) = remove_result.swapped_entity {
231-
let swapped_location = world.entities.get(swapped_entity).unwrap();
231+
let swapped_location = world.entities.get_spawned(swapped_entity).unwrap();
232232

233-
world.entities.set(
233+
world.entities.update_existing_location(
234234
swapped_entity.index(),
235235
Some(EntityLocation {
236236
archetype_id: swapped_location.archetype_id,
@@ -269,9 +269,9 @@ impl<'w> BundleRemover<'w> {
269269

270270
// if an entity was moved into this entity's table row, update its table row
271271
if let Some(swapped_entity) = move_result.swapped_entity {
272-
let swapped_location = world.entities.get(swapped_entity).unwrap();
272+
let swapped_location = world.entities.get_spawned(swapped_entity).unwrap();
273273

274-
world.entities.set(
274+
world.entities.update_existing_location(
275275
swapped_entity.index(),
276276
Some(EntityLocation {
277277
archetype_id: swapped_location.archetype_id,
@@ -294,7 +294,9 @@ impl<'w> BundleRemover<'w> {
294294

295295
// SAFETY: The entity is valid and has been moved to the new location already.
296296
unsafe {
297-
world.entities.set(entity.index(), Some(new_location));
297+
world
298+
.entities
299+
.update_existing_location(entity.index(), Some(new_location));
298300
}
299301

300302
(new_location, pre_remove_result)

crates/bevy_ecs/src/bundle/spawner.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ use bevy_ptr::{ConstNonNull, MovingPtr};
55
use crate::{
66
archetype::{Archetype, ArchetypeCreated, ArchetypeId, SpawnBundleStatus},
77
bundle::{Bundle, BundleId, BundleInfo, DynamicBundle, InsertMode},
8-
change_detection::MaybeLocation,
9-
change_detection::Tick,
10-
entity::{Entities, Entity, EntityLocation},
8+
change_detection::{MaybeLocation, Tick},
9+
entity::{Entity, EntityAllocator, EntityLocation},
1110
event::EntityComponentsTrigger,
1211
lifecycle::{Add, Insert, ADD, INSERT},
1312
relationship::RelationshipHookMode,
@@ -89,7 +88,7 @@ impl<'w> BundleSpawner<'w> {
8988
/// [`apply_effect`]: crate::bundle::DynamicBundle::apply_effect
9089
#[inline]
9190
#[track_caller]
92-
pub unsafe fn spawn_non_existent<T: DynamicBundle>(
91+
pub unsafe fn spawn_at<T: DynamicBundle>(
9392
&mut self,
9493
entity: Entity,
9594
bundle: MovingPtr<'_, T>,
@@ -120,8 +119,8 @@ impl<'w> BundleSpawner<'w> {
120119
InsertMode::Replace,
121120
caller,
122121
);
123-
entities.set(entity.index(), Some(location));
124-
entities.mark_spawn_despawn(entity.index(), caller, self.change_tick);
122+
entities.set_location(entity.index(), Some(location));
123+
entities.mark_spawned_or_despawned(entity.index(), caller, self.change_tick);
125124
location
126125
};
127126

@@ -186,22 +185,16 @@ impl<'w> BundleSpawner<'w> {
186185
bundle: MovingPtr<'_, T>,
187186
caller: MaybeLocation,
188187
) -> Entity {
189-
let entity = self.entities().alloc();
190-
// SAFETY:
191-
// - `entity` is allocated above
192-
// - The caller ensures that `T` matches this `BundleSpawner`'s type.
193-
// - The caller ensures that if `T::Effect: !NoBundleEffect.`, then [`apply_effect`] must be called exactly once on `bundle`
194-
// after this function returns before returning to safe code.
195-
// - The caller ensures that the value pointed to by `bundle` must not be accessed for anything other than [`apply_effect`]
196-
// or dropped.
197-
unsafe { self.spawn_non_existent::<T>(entity, bundle, caller) };
188+
let entity = self.allocator().alloc();
189+
// SAFETY: entity is allocated (but non-existent), `T` matches this BundleInfo's type
190+
let _ = unsafe { self.spawn_at(entity, bundle, caller) };
198191
entity
199192
}
200193

201194
#[inline]
202-
pub(crate) fn entities(&mut self) -> &mut Entities {
195+
pub(crate) fn allocator(&mut self) -> &'w mut EntityAllocator {
203196
// SAFETY: No outstanding references to self.world, changes to entities cannot invalidate our internal pointers
204-
unsafe { &mut self.world.world_mut().entities }
197+
unsafe { &mut self.world.world_mut().allocator }
205198
}
206199

207200
/// # Safety

crates/bevy_ecs/src/entity/clone_entities.rs

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
use alloc::{boxed::Box, collections::VecDeque, vec::Vec};
2-
use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet};
3-
use bevy_ptr::{Ptr, PtrMut};
4-
use bevy_utils::prelude::DebugName;
5-
use bumpalo::Bump;
6-
use core::{any::TypeId, cell::LazyCell, ops::Range};
7-
use derive_more::derive::From;
8-
91
use crate::{
102
archetype::Archetype,
113
bundle::{Bundle, BundleRemover, InsertMode},
124
change_detection::MaybeLocation,
135
component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo},
14-
entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper},
6+
entity::{hash_map::EntityHashMap, Entity, EntityAllocator, EntityMapper},
157
query::DebugCheckedUnwrap,
168
relationship::RelationshipHookMode,
179
world::World,
1810
};
11+
use alloc::{boxed::Box, collections::VecDeque, vec::Vec};
12+
use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet};
13+
use bevy_ptr::{Ptr, PtrMut};
14+
use bevy_utils::prelude::DebugName;
15+
use bumpalo::Bump;
16+
use core::{any::TypeId, cell::LazyCell, ops::Range};
17+
use derive_more::From;
1918

2019
/// Provides read access to the source component (the component being cloned) in a [`ComponentCloneFn`].
2120
pub struct SourceComponent<'a> {
@@ -80,7 +79,7 @@ pub struct ComponentCloneCtx<'a, 'b> {
8079
target_component_moved: bool,
8180
bundle_scratch: &'a mut BundleScratch<'b>,
8281
bundle_scratch_allocator: &'b Bump,
83-
entities: &'a Entities,
82+
allocator: &'a EntityAllocator,
8483
source: Entity,
8584
target: Entity,
8685
component_info: &'a ComponentInfo,
@@ -106,7 +105,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> {
106105
target: Entity,
107106
bundle_scratch_allocator: &'b Bump,
108107
bundle_scratch: &'a mut BundleScratch<'b>,
109-
entities: &'a Entities,
108+
allocator: &'a EntityAllocator,
110109
component_info: &'a ComponentInfo,
111110
entity_cloner: &'a mut EntityClonerState,
112111
mapper: &'a mut dyn EntityMapper,
@@ -121,7 +120,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> {
121120
target_component_written: false,
122121
target_component_moved: false,
123122
bundle_scratch_allocator,
124-
entities,
123+
allocator,
125124
mapper,
126125
component_info,
127126
state: entity_cloner,
@@ -279,7 +278,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> {
279278

280279
/// Queues the `entity` to be cloned by the current [`EntityCloner`]
281280
pub fn queue_entity_clone(&mut self, entity: Entity) {
282-
let target = self.entities.reserve_entity();
281+
let target = self.allocator.alloc();
283282
self.mapper.set_mapped(entity, target);
284283
self.state.clone_queue.push_back(entity);
285284
}
@@ -565,14 +564,21 @@ impl EntityCloner {
565564
relationship_hook_insert_mode: RelationshipHookMode,
566565
) -> Entity {
567566
let target = mapper.get_mapped(source);
567+
// The target may need to be constructed if it hasn't been already.
568+
// If this fails, it either didn't need to be constructed (ok) or doesn't exist (caught better later).
569+
let _ = world.spawn_empty_at(target);
570+
568571
// PERF: reusing allocated space across clones would be more efficient. Consider an allocation model similar to `Commands`.
569572
let bundle_scratch_allocator = Bump::new();
570573
let mut bundle_scratch: BundleScratch;
571574
let mut moved_components: Vec<ComponentId> = Vec::new();
572575
let mut deferred_cloned_component_ids: Vec<ComponentId> = Vec::new();
573576
{
574577
let world = world.as_unsafe_world_cell();
575-
let source_entity = world.get_entity(source).expect("Source entity must exist");
578+
let source_entity = world
579+
.get_entity(source)
580+
.expect("Source entity must be valid and spawned.");
581+
let source_archetype = source_entity.archetype();
576582

577583
#[cfg(feature = "bevy_reflect")]
578584
// SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone
@@ -585,13 +591,12 @@ impl EntityCloner {
585591
#[cfg(not(feature = "bevy_reflect"))]
586592
let app_registry = Option::<()>::None;
587593

588-
let source_archetype = source_entity.archetype();
589594
bundle_scratch = BundleScratch::with_capacity(source_archetype.component_count());
590595

591596
let target_archetype = LazyCell::new(|| {
592597
world
593598
.get_entity(target)
594-
.expect("Target entity must exist")
599+
.expect("Target entity must be valid and spawned.")
595600
.archetype()
596601
});
597602

@@ -641,7 +646,7 @@ impl EntityCloner {
641646
target,
642647
&bundle_scratch_allocator,
643648
&mut bundle_scratch,
644-
world.entities(),
649+
world.entities_allocator(),
645650
info,
646651
state,
647652
mapper,

crates/bevy_ecs/src/entity/map_entities.rs

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ use super::EntityIndexSet;
5656
pub trait MapEntities {
5757
/// Updates all [`Entity`] references stored inside using `entity_mapper`.
5858
///
59-
/// Implementors should look up any and all [`Entity`] values stored within `self` and
59+
/// Implementers should look up any and all [`Entity`] values stored within `self` and
6060
/// update them to the mapped values via `entity_mapper`.
6161
fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E);
6262
}
@@ -202,7 +202,7 @@ impl<T: MapEntities, A: smallvec::Array<Item = T>> MapEntities for SmallVec<A> {
202202
///
203203
/// More generally, this can be used to map [`Entity`] references between any two [`Worlds`](World).
204204
///
205-
/// This is used by [`MapEntities`] implementors.
205+
/// This is used by [`MapEntities`] implementers.
206206
///
207207
/// ## Example
208208
///
@@ -276,8 +276,8 @@ impl EntityMapper for SceneEntityMapper<'_> {
276276
}
277277

278278
// this new entity reference is specifically designed to never represent any living entity
279-
let new = Entity::from_row_and_generation(
280-
self.dead_start.row(),
279+
let new = Entity::from_index_and_generation(
280+
self.dead_start.index(),
281281
self.dead_start.generation.after_versions(self.generations),
282282
);
283283
self.generations = self.generations.wrapping_add(1);
@@ -336,14 +336,10 @@ impl<'m> SceneEntityMapper<'m> {
336336
}
337337

338338
/// Creates a new [`SceneEntityMapper`], spawning a temporary base [`Entity`] in the provided [`World`]
339-
pub fn new(map: &'m mut EntityHashMap<Entity>, world: &mut World) -> Self {
340-
// We're going to be calling methods on `Entities` that require advance
341-
// flushing, such as `alloc` and `free`.
342-
world.flush_entities();
339+
pub fn new(map: &'m mut EntityHashMap<Entity>, world: &World) -> Self {
343340
Self {
344341
map,
345-
// SAFETY: Entities data is kept in a valid state via `EntityMapper::world_scope`
346-
dead_start: unsafe { world.entities_mut().alloc() },
342+
dead_start: world.allocator.alloc(),
347343
generations: 0,
348344
}
349345
}
@@ -352,10 +348,13 @@ impl<'m> SceneEntityMapper<'m> {
352348
/// [`Entity`] while reserving extra generations. Because this makes the [`SceneEntityMapper`] unable to
353349
/// safely allocate any more references, this method takes ownership of `self` in order to render it unusable.
354350
pub fn finish(self, world: &mut World) {
355-
// SAFETY: Entities data is kept in a valid state via `EntityMap::world_scope`
356-
let entities = unsafe { world.entities_mut() };
357-
assert!(entities.free(self.dead_start).is_some());
358-
assert!(entities.reserve_generations(self.dead_start.index(), self.generations));
351+
// SAFETY: We never constructed the entity and never released it for something else to construct.
352+
let reuse_row = unsafe {
353+
world
354+
.entities
355+
.mark_free(self.dead_start.index(), self.generations)
356+
};
357+
world.allocator.free(reuse_row);
359358
}
360359

361360
/// Creates an [`SceneEntityMapper`] from a provided [`World`] and [`EntityHashMap<Entity>`], then calls the
@@ -388,7 +387,7 @@ mod tests {
388387
fn entity_mapper() {
389388
let mut map = EntityHashMap::default();
390389
let mut world = World::new();
391-
let mut mapper = SceneEntityMapper::new(&mut map, &mut world);
390+
let mut mapper = SceneEntityMapper::new(&mut map, &world);
392391

393392
let mapped_ent = Entity::from_raw_u32(1).unwrap();
394393
let dead_ref = mapper.get_mapped(mapped_ent);
@@ -431,21 +430,4 @@ mod tests {
431430
.cmp_approx(&dead_ref.generation())
432431
.is_gt());
433432
}
434-
435-
#[test]
436-
fn entity_mapper_no_panic() {
437-
let mut world = World::new();
438-
// "Dirty" the `Entities`, requiring a flush afterward.
439-
world.entities.reserve_entity();
440-
assert!(world.entities.needs_flush());
441-
442-
// Create and exercise a SceneEntityMapper - should not panic because it flushes
443-
// `Entities` first.
444-
SceneEntityMapper::world_scope(&mut Default::default(), &mut world, |_, m| {
445-
m.get_mapped(Entity::PLACEHOLDER);
446-
});
447-
448-
// The SceneEntityMapper should leave `Entities` in a flushed state.
449-
assert!(!world.entities.needs_flush());
450-
}
451433
}

0 commit comments

Comments
 (0)