Skip to content

Entity ranges#24102

Open
Trashtalk217 wants to merge 21 commits into
bevyengine:mainfrom
Trashtalk217:entity-ranges
Open

Entity ranges#24102
Trashtalk217 wants to merge 21 commits into
bevyengine:mainfrom
Trashtalk217:entity-ranges

Conversation

@Trashtalk217
Copy link
Copy Markdown
Contributor

Objective

For 'components as entities' (#23988), we move away from a seperate ComponentId allocator to the EntityAllocator. This has some advantages, but one major downside. ComponentIds are no longer 'dense'. Take the following code:

let _ in 0..100 {
    world::spawn(ComponentA);
}
world::spawn(ComponentB);

Assuming that we've never initialized ComponentA and ComponentB before, this code does four things:

  1. Initialize ComponentA with the World, this results in ComponentId(0)
  2. Spawn a hundred entities with ComponentA, entity indices 1 through 100 are allocated here.
  3. Initialize ComponentB with the World, this results in ComponentId(101)
  4. Spawn the entity with ComponentB.

If you now have a SparseArray or FixedIndexSet that relies on the indices of ComponentIds to stay relatively small, this becomes a problem, because 99% of the space is wasted on entities that are never ComponentIds.

Solution

We introduce ranges to EntityAllocator, such that you can use different allocators for different purposes. Say compont_id_allocator = EntityAllocator::new(0..1024), while entity_allocator = EntityAllocator::new(1024..), then the first 1024 components are allocated sequentially, after that, you can fallback on the entity allocator.

This does require the use of hybrid data structures, that use an array for the first n elements and a hashmap for when the index becomes larger than n.

fn get(&self, index: Entity) -> K {
    if index < n {
        self.array[index]
    } else {
        self.hashmap.get(index)
    }
}

Prior Art

Flecs uses entity ranges, reserving the low entity indexes for components. Read more about 'Id Partitioning'.

Benchmarking

I've ran some micro benchmarks and I'll be honest: They do not look good.

Details
entity_allocator_allocate_fresh/1_entities
                        time:   [9.4784 ns 10.257 ns 10.995 ns]
                        change: [−1.7068% +7.1307% +17.029%] (p = 0.14 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_fresh/100_entities
                        time:   [358.36 ns 360.97 ns 364.31 ns]
                        change: [+1.0677% +1.8075% +2.8304%] (p = 0.00 < 0.05)
                        Performance has regressed.

entity_allocator_allocate_fresh/10000_entities
                        time:   [34.556 µs 34.652 µs 34.756 µs]
                        change: [−0.1292% +0.4663% +1.2522%] (p = 0.22 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_fresh_bulk/1_entities
                        time:   [16.236 ns 17.799 ns 19.392 ns]
                        change: [−11.898% −2.8675% +7.0857%] (p = 0.57 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_fresh_bulk/100_entities
                        time:   [144.16 ns 145.59 ns 147.05 ns]
                        change: [−1.5016% +0.8472% +2.7315%] (p = 0.48 > 0.05)
                        No change in performance detected.
Found 7 outliers among 100 measurements (7.00%)

entity_allocator_allocate_fresh_bulk/10000_entities
                        time:   [13.420 µs 13.512 µs 13.628 µs]
                        change: [+2.3796% +4.8428% +7.2368%] (p = 0.00 < 0.05)
                        Performance has regressed.

entity_allocator_free/1_entities
                        time:   [13.469 ns 18.264 ns 23.583 ns]
                        change: [−8.3581% +12.721% +39.418%] (p = 0.29 > 0.05)
                        No change in performance detected.

entity_allocator_free/100_entities
                        time:   [240.65 ns 247.77 ns 255.28 ns]
                        change: [−4.5899% −1.3696% +1.6595%] (p = 0.41 > 0.05)
                        No change in performance detected.

entity_allocator_free/10000_entities
                        time:   [34.423 µs 34.947 µs 35.470 µs]
                        change: [−3.6801% −1.6275% +0.3801%] (p = 0.10 > 0.05)
                        No change in performance detected.

entity_allocator_free_bulk/1_entities
                        time:   [11.820 ns 13.322 ns 14.818 ns]
                        change: [−17.375% −4.1710% +11.461%] (p = 0.59 > 0.05)
                        No change in performance detected.

entity_allocator_free_bulk/100_entities
                        time:   [49.154 ns 55.053 ns 60.585 ns]
                        change: [−18.432% −6.7790% +6.4653%] (p = 0.31 > 0.05)
                        No change in performance detected.

entity_allocator_free_bulk/10000_entities
                        time:   [15.494 µs 16.020 µs 16.498 µs]
                        change: [−6.9686% −1.9433% +3.1998%] (p = 0.45 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_reused/1_entities
                        time:   [9.1057 ns 9.8838 ns 10.642 ns]
                        change: [−5.0685% +3.9422% +13.296%] (p = 0.39 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_reused/100_entities
                        time:   [355.66 ns 356.93 ns 358.39 ns]
                        change: [−0.7052% −0.1037% +0.4546%] (p = 0.73 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_reused/10000_entities
                        time:   [36.192 µs 36.374 µs 36.589 µs]
                        change: [+72.795% +74.654% +76.949%] (p = 0.00 < 0.05)
                        Performance has regressed.

entity_allocator_allocate_reused_bulk/1_entities
                        time:   [15.635 ns 16.993 ns 18.445 ns]
                        change: [−9.0411% −0.9649% +7.6055%] (p = 0.82 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_reused_bulk/100_entities
                        time:   [162.66 ns 166.06 ns 171.57 ns]
                        change: [+13.006% +14.883% +17.661%] (p = 0.00 < 0.05)
                        Performance has regressed.

entity_allocator_allocate_reused_bulk/10000_entities
                        time:   [15.970 µs 16.102 µs 16.243 µs]
                        change: [−1.2967% +0.1922% +1.4182%] (p = 0.80 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_fresh_remote/1_entities
                        time:   [3.8683 ns 4.0522 ns 4.2433 ns]
                        change: [−4.5889% +1.1340% +6.9951%] (p = 0.70 > 0.05)
                        No change in performance detected.

entity_allocator_allocate_fresh_remote/100_entities
                        time:   [197.11 ns 197.96 ns 198.88 ns]
                        change: [+12.090% +12.481% +12.877%] (p = 0.00 < 0.05)
                        Performance has regressed.

entity_allocator_allocate_fresh_remote/10000_entities
                        time:   [19.922 µs 19.981 µs 20.046 µs]
                        change: [+14.445% +15.355% +15.989%] (p = 0.00 < 0.05)
                        Performance has regressed.

entity_allocator_allocate_reused_remote/1_entities
                        time:   [4.3412 ns 4.6948 ns 5.0906 ns]
                        change: [+2.1866% +9.8798% +18.167%] (p = 0.02 < 0.05)
                        Performance has regressed.

entity_allocator_allocate_reused_remote/100_entities
                        time:   [198.90 ns 199.53 ns 200.37 ns]
                        change: [+10.114% +12.529% +14.446%] (p = 0.00 < 0.05)
                        Performance has regressed.

entity_allocator_allocate_reused_remote/10000_entities
                        time:   [36.950 µs 37.074 µs 37.224 µs]
                        change: [+23.683% +24.839% +26.918%] (p = 0.00 < 0.05)
                        Performance has regressed.

@Trashtalk217 Trashtalk217 added the A-ECS Entities, components, systems, and events label May 3, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS May 3, 2026
@alice-i-cecile alice-i-cecile added D-Complex Quite challenging from either a design or technical perspective. Ask for help! X-Needs-SME This type of work requires an SME to approve it. S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 3, 2026
Copy link
Copy Markdown
Contributor

@ElliottjPierce ElliottjPierce left a comment

Choose a reason for hiding this comment

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

I like the direction of this. I've got a few suggestions to improve performance, and there's somewhat of a problem in the new FreshEntityAllocator::alloc (see my comment there), but nothing too major. Looking forward to components as entities!

Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs
Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs Outdated
Comment thread crates/bevy_ecs/src/entity/mod.rs Outdated
Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs
Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs Outdated
let new = match start_new
.checked_add(count)
.filter(|new| *new < Self::MAX_ENTITIES)
.filter(|new| *new < self.max_index)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be nice if we could instead say, "Here's the id range you could allocate," so it never panics; it just sometimes doesn't contain all count entities. But that's definitely not for this PR. I think this is the correct implementation for now.

JMS55 and others added 6 commits May 4, 2026 03:43
# Objective

- Provide a higher quality texture compressor
- Automatically generate mipmaps

Closes bevyengine#14671.

## Solution

- Use the ctt crate

## Testing

- New compressed_image_saver example (for now I have merged the textures
into this branch, but before merging we should place them in the bevy
asset repo)

---

## Showcase
<img width="3206" height="1875" alt="image"
src="https://github.com/user-attachments/assets/4b236f00-3f5d-4618-a53a-efcc74e9d32b"
/>
…yengine#24065)

# Objective

Alternative to bevyengine#24004.

bevyengine#23288 adds ltc luts for rect
light support which implicitly requires `bevy_image/ktx2` and
`bevy_image/zstd` otherwise loading ltc luts will panic.

We either accept to always enable area light supoort (bevyengine#24004), or add a
feature to opt out it (this PR).

## Solution

Gate ltc luts behind a feature and merge them to a texture array.

## Testing

`rect_light` example works.

---------

Co-authored-by: Kevin Chen <chen.kevin.f@gmail.com>
…f `T` type (bevyengine#24048)

# Objective

Fixes issue where handles generate no problem regardless of T type via
`from_reflect` due to strong handle's being fully opaque and simply
cloned.


## Solution

Adds a small type id check to the `FromReflect` implemenation which
fails conversion if the type id is not what we expect:

Reference automatically derived implementation:

```rust
impl<A: Asset> bevy_reflect::FromReflect for Handle<A>
where
    Handle<A>: ::core::any::Any + ::core::marker::Send + ::core::marker::Sync,
    A: bevy_reflect::TypePath,
{
    fn from_reflect(__param0: &dyn bevy_reflect::PartialReflect) -> ::core::option::Option<Self> {
        if let bevy_reflect::ReflectRef::Enum(__param0) =
            bevy_reflect::PartialReflect::reflect_ref(__param0)
        {
            match bevy_reflect::enums::Enum::variant_name(__param0) {
                "Strong" => ::core::option::Option::Some(Handle::Strong {
                    0: {
                        let __0 = __param0.field_at(0usize);
                        let __0 = __0?;
                        <Arc<StrongHandle> as bevy_reflect::FromReflect>::from_reflect(__0)?
                    },
                }),
                "Uuid" => ::core::option::Option::Some(Handle::Uuid {
                    0: {
                        let __0 = __param0.field_at(0usize);
                        let __0 = __0?;
                        <Uuid as bevy_reflect::FromReflect>::from_reflect(__0)?
                    },
                    1: ::core::default::Default::default(),
                }),
                name => panic!(
                    "variant with name `{}` does not exist on enum `{}`",
                    name,
                    <Self as bevy_reflect::TypePath>::type_path()
                ),
            }
        } else {
            ::core::option::Option::None
        }
    }
}
```

## Testing

added basic tests

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Greeble <166992735+greeble-dev@users.noreply.github.com>
…e#24089)

# Objective

- Alongside bevyengine#24086, helps with bevyengine#24084, although I think we should double
check any other added conditionals for bind group entries to make sure
they are accurate.

## Solution
So, originally the `SCREEN_SPACE_TRANSMISSION` was enabled with
`key.intersects(MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS)`.
However, a low quality transmission would make this false, since low’s
MeshPipelineKey is configured like this: `const
SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW = 0 <<
Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS;`. So, a
ScreenSpaceTransmission with Low Quality would break rendering (since
another if-block merely checks that the `ScreenSpaceTransmission`
component exists)

Making it so that the low transmission truly does *not* include the
view_transmission_textures makes the transmission render not properly -
the spheres disappear!

So, I think the proper fix here is to remove the gating around
transmission textures.
Edit: Another potential fix is to change the condition of the
`intersects` but I’m not sure how to encode what we want unless we want
to add another bit for `ScreenSpaceTransmission` component exists
essentially? Happy to close this PR if that is an acceptable direction.

## Testing

`cargo run --example transmission` works for all quality levels.
…evyengine#24046)

I was initially using `MessageReader<WindowResized>` in my system for my
app but once my system grew to use more and more window events, I
refactored to using `MessageReader<WindowEvent>` and matching on its
variants. This is where I ran into the issue of the
`WindowEvent::WindowResized` case never matching.

When making this PR, I noticed
`WindowEvent::WindowBackendScaleFactorChanged` and
`WindowEvent::WindowScaleFactorChanged` had the same issue.

# Objective

Fixes bevyengine#15268

## Solution

Instead of writing into `MessageWriter<WindowResized>`,
`MessageWriter<WindowBackendScaleFactorChanged>` and
`MessageWriter<WindowScaleFactorChanged>`, push into
`bevy_window_events` where it gets sent to the `forward_bevy_events`
function for syncing the messages.

## Testing

I made local modifications to the `window_resizing` example to use a
`MessageReader<WindowEvent>` instead of `MessageReader<WindowResized>`
like so:
```rs
fn on_resize_system(
    mut text: Single<&mut Text, With<ResolutionText>>,
    mut resize_reader: MessageReader<WindowEvent>,
) {
    for e in resize_reader.read() {
        if let WindowEvent::WindowResized(e) = e {
            // When resolution is being changed
            text.0 = format!("{:.1} x {:.1}", e.width, e.height);
        }
    }
}
```
I ran this example on linux wayland.
# Objective

Make the possibility of 1-to-1 `Relationship` clearer in the docs, so
that people know it's an option.
(There's already a passing mention of it at the top, but that doesn't
show that it's supported in code.)

## Solution

Added an example to the doc comment to show how it's done, and explained
what happens if you try to add another entity anyway.

## Testing

Ran `cargo doc` and looked at the new docs to see if they're ok.
Copy link
Copy Markdown
Contributor

@chescock chescock left a comment

Choose a reason for hiding this comment

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

This looks good, but I agree with @ElliottjPierce that we need to handle the case where fetch_add overflows, so I'll hold off on clicking Approve until that's resolved.

I've ran some micro benchmarks and I'll be honest: They do not look good.

I'm surprised that this would cause a big difference! It might be worth checking the assembly for calls to alloc. The only difference I would expect is the overflow check changing from comparing to a constant to comparing to a variable. Maybe that's already enough in code this hot? Or maybe something is failing to inline where it should.

@Trashtalk217
Copy link
Copy Markdown
Contributor Author

New Perf numbers! I've also tried adding core::hint::cold_path(), but that actually made it worse. This is about parity with the current implementation.

group                                                     after                                  before
-----                                                     -----                                  ------
entity_allocator_allocate_fresh/10000_entities            1.00     34.0±0.35µs        ? ?/sec    1.01     34.5±0.44µs        ? ?/sec
entity_allocator_allocate_fresh/100_entities              1.00   351.9±24.89ns        ? ?/sec    1.01   354.4±10.59ns        ? ?/sec
entity_allocator_allocate_fresh/1_entities                1.03      7.7±2.66ns        ? ?/sec    1.00      7.5±1.90ns        ? ?/sec
entity_allocator_allocate_fresh_bulk/10000_entities       1.00     13.1±0.59µs        ? ?/sec    1.00     13.2±0.13µs        ? ?/sec
entity_allocator_allocate_fresh_bulk/100_entities         1.00    139.1±4.90ns        ? ?/sec    1.01    141.1±5.90ns        ? ?/sec
entity_allocator_allocate_fresh_bulk/1_entities           1.00     14.0±3.97ns        ? ?/sec    1.10     15.5±6.69ns        ? ?/sec
entity_allocator_allocate_fresh_remote/10000_entities     1.00     17.1±0.47µs        ? ?/sec    1.00     17.2±0.23µs        ? ?/sec
entity_allocator_allocate_fresh_remote/100_entities       1.00    174.9±6.04ns        ? ?/sec    1.01    176.0±7.80ns        ? ?/sec
entity_allocator_allocate_fresh_remote/1_entities         1.00      3.7±0.72ns        ? ?/sec    1.20      4.4±2.09ns        ? ?/sec
entity_allocator_allocate_reused/10000_entities           1.00     20.6±1.58µs        ? ?/sec    1.01     20.8±0.61µs        ? ?/sec
entity_allocator_allocate_reused/100_entities             1.00    352.1±8.20ns        ? ?/sec    1.01   355.3±22.63ns        ? ?/sec
entity_allocator_allocate_reused/1_entities               1.00      7.5±1.97ns        ? ?/sec    1.02      7.7±2.31ns        ? ?/sec
entity_allocator_allocate_reused_bulk/10000_entities      1.00     15.8±0.84µs        ? ?/sec    1.00     15.8±0.57µs        ? ?/sec
entity_allocator_allocate_reused_bulk/100_entities        1.00    142.5±7.77ns        ? ?/sec    1.00    142.1±7.23ns        ? ?/sec
entity_allocator_allocate_reused_bulk/1_entities          1.00     13.7±4.16ns        ? ?/sec    1.00     13.7±3.58ns        ? ?/sec
entity_allocator_allocate_reused_remote/10000_entities    1.00     29.0±1.13µs        ? ?/sec    1.02     29.6±1.03µs        ? ?/sec
entity_allocator_allocate_reused_remote/100_entities      1.00    174.8±5.08ns        ? ?/sec    1.00    175.1±1.82ns        ? ?/sec
entity_allocator_allocate_reused_remote/1_entities        1.00      3.6±0.77ns        ? ?/sec    1.02      3.7±0.68ns        ? ?/sec
entity_allocator_free/10000_entities                      1.00     33.0±2.56µs        ? ?/sec    1.01     33.4±2.14µs        ? ?/sec
entity_allocator_free/100_entities                        1.01   235.7±38.10ns        ? ?/sec    1.00   233.5±22.25ns        ? ?/sec
entity_allocator_free/1_entities                          1.00      8.4±3.18ns        ? ?/sec    1.13      9.5±4.17ns        ? ?/sec
entity_allocator_free_bulk/10000_entities                 1.05     13.9±2.76µs        ? ?/sec    1.00     13.2±2.08µs        ? ?/sec
entity_allocator_free_bulk/100_entities                   1.00    38.8±19.72ns        ? ?/sec    1.02    39.6±19.80ns        ? ?/sec
entity_allocator_free_bulk/1_entities                     1.04      9.7±3.02ns        ? ?/sec    1.00      9.3±3.59ns        ? ?/sec

Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs Outdated
Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs Outdated
Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs
@cart cart closed this May 5, 2026
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in ECS May 5, 2026
@cart cart reopened this May 5, 2026
@github-project-automation github-project-automation Bot moved this from Done to Needs SME Triage in ECS May 5, 2026
@alice-i-cecile
Copy link
Copy Markdown
Member

Broken by the force push noted in #24130; you'll need to clean up the Git history per @cart's message.

@Trashtalk217
Copy link
Copy Markdown
Contributor Author

I reran the numbers. I think it's good enough now. Not perfect, but good enough to proceed.

group                                                     after                                  before
-----                                                     -----                                  ------
entity_allocator_allocate_fresh/10000_entities            1.01     36.7±1.48µs        ? ?/sec    1.00     36.4±1.28µs        ? ?/sec
entity_allocator_allocate_fresh/100_entities              1.01   372.6±26.92ns        ? ?/sec    1.00    368.7±5.59ns        ? ?/sec
entity_allocator_allocate_fresh/1_entities                1.00      7.6±1.43ns        ? ?/sec    1.00      7.5±1.99ns        ? ?/sec
entity_allocator_allocate_fresh_bulk/10000_entities       1.17     16.1±0.45µs        ? ?/sec    1.00     13.7±0.24µs        ? ?/sec
entity_allocator_allocate_fresh_bulk/100_entities         1.18   172.1±15.57ns        ? ?/sec    1.00    146.1±4.64ns        ? ?/sec
entity_allocator_allocate_fresh_bulk/1_entities           1.03     12.3±2.64ns        ? ?/sec    1.00     11.9±2.56ns        ? ?/sec
entity_allocator_allocate_fresh_remote/10000_entities     1.01     18.1±0.42µs        ? ?/sec    1.00     17.9±0.32µs        ? ?/sec
entity_allocator_allocate_fresh_remote/100_entities       1.00   184.0±11.53ns        ? ?/sec    1.00   183.6±16.29ns        ? ?/sec
entity_allocator_allocate_fresh_remote/1_entities         1.00      4.4±1.88ns        ? ?/sec    1.20      5.3±2.82ns        ? ?/sec
entity_allocator_allocate_reused/10000_entities           1.00     21.8±2.20µs        ? ?/sec    1.00     21.8±1.35µs        ? ?/sec
entity_allocator_allocate_reused/100_entities             1.01   375.5±27.38ns        ? ?/sec    1.00   371.1±26.16ns        ? ?/sec
entity_allocator_allocate_reused/1_entities               1.01      8.0±2.09ns        ? ?/sec    1.00      7.9±2.64ns        ? ?/sec
entity_allocator_allocate_reused_bulk/10000_entities      1.00     14.4±1.30µs        ? ?/sec    1.16     16.7±1.53µs        ? ?/sec
entity_allocator_allocate_reused_bulk/100_entities        1.17   172.7±13.76ns        ? ?/sec    1.00   148.1±15.36ns        ? ?/sec
entity_allocator_allocate_reused_bulk/1_entities          1.03     14.3±4.52ns        ? ?/sec    1.00     13.8±4.23ns        ? ?/sec
entity_allocator_allocate_reused_remote/10000_entities    1.00     31.4±2.22µs        ? ?/sec    1.00     31.3±1.52µs        ? ?/sec
entity_allocator_allocate_reused_remote/100_entities      1.01   184.8±15.30ns        ? ?/sec    1.00   182.7±14.15ns        ? ?/sec
entity_allocator_allocate_reused_remote/1_entities        1.00      4.6±2.09ns        ? ?/sec    1.06      4.9±2.17ns        ? ?/sec
entity_allocator_free/10000_entities                      1.01     34.8±3.32µs        ? ?/sec    1.00     34.4±2.91µs        ? ?/sec
entity_allocator_free/100_entities                        1.00   242.1±29.47ns        ? ?/sec    1.00   242.9±27.73ns        ? ?/sec
entity_allocator_free/1_entities                          1.00      6.6±2.37ns        ? ?/sec    1.03      6.9±2.13ns        ? ?/sec
entity_allocator_free_bulk/10000_entities                 1.00     14.6±3.28µs        ? ?/sec    1.02     14.9±3.53µs        ? ?/sec
entity_allocator_free_bulk/100_entities                   1.02    39.4±19.00ns        ? ?/sec    1.00    38.6±21.04ns        ? ?/sec
entity_allocator_free_bulk/1_entities                     1.00      8.9±4.18ns        ? ?/sec    1.10      9.7±5.29ns        ? ?/sec

Copy link
Copy Markdown
Contributor

@ElliottjPierce ElliottjPierce left a comment

Choose a reason for hiding this comment

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

I'm satisfied with the implementation at this point, just a few nitpicks and docs stuff for the api, and I think this is ready.

Also, heads up that this brought back in the large assets, so there's probably some git cleanup to do, but I haven't been following that, so IDK.

Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs Outdated
Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs Outdated
Comment thread crates/bevy_ecs/src/entity/mod.rs Outdated
Comment thread crates/bevy_ecs/src/entity/remote_allocator.rs
Comment thread _release-content/migration-guides/compressed_image_saver.md Outdated
Copy link
Copy Markdown
Member

@mockersf mockersf left a comment

Choose a reason for hiding this comment

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

PR contains the reverted compressed_image_saver changes, it shouldn't

///
/// Conceptually, this is a collection of [`Entity`] ids who's [`EntityIndex`] is despawned and who's [`EntityGeneration`] is the most recent.
/// See the module docs for how these ids and this allocator participate in the life cycle of an entity.
#[derive(Default, Debug)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Removing Default here is technically a breaking change, right? And it's no longer possible to create these at all? You might need a migration guide.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shoot, you're right. Maybe it should stay Default after all. But definitely the world and stuff should make it with the range instead of the default IMO, but that change could be saved for later too, so up to you.

Copy link
Copy Markdown
Contributor

@ElliottjPierce ElliottjPierce left a comment

Choose a reason for hiding this comment

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

I'm happy with this! Components as entities here we come!

@Trashtalk217
Copy link
Copy Markdown
Contributor Author

I'll remove the Default later, no breaking changes in this PR.

@Trashtalk217 Trashtalk217 added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 23, 2026
@Victoronz
Copy link
Copy Markdown
Contributor

Two things:
First, IIUC, ComponentIds not being dense is itself not a blocker to #23988, it means that the data structure we use for them will be less performant.
But how much of a performance difference is that actually?
Entity ranges have been a controversial topic in the past, so for what can be considered a purely performance-motivated change, it'd be nice to see how much the intended benefit actually is!

Second, if they need only be contiguous, why do we need to be able to give exact ranges to the allocator? A more focused change would be to say "Give me N contiguous entities", instead of "Give me entities ranging from X to Y".
This way we still get dense ranges, but do not commit to defining any manual ranges anywhere.

@ElliottjPierce
Copy link
Copy Markdown
Contributor

@Victoronz

To your first point, I'd say an index op is much faster than a hashmap lookup, so I think the perf diff would be pretty big. Even if not, it still makes sense for an ecs to pursue the maximum performance wherever possible. (But I agree that it would be nice to see the benchmarks out of curiosity.)

To your second point, we need the range of entities to start at 0 ideally for faster indexing, so starting at 0 and with a length of N incidentally forces the use of specific ranges rather than just asking for N contiguous entities. Also, an interface that just asks for N contiguous entities would be hard to depend on because of fragmentation. I don't think there's a solution to that that's dependable enough to be included in the core engine.

Weighing in on the entity allocator, specifying ranges like this is extremely useful for lots of other stuff. Ex: syncing an entity id space between two worlds is really useful for multiplayer, and this makes it easy. This isn't worth making public though until we have entity paging to make large ranges performant.

You're right though that this could be done in a more focused way. Just allocate N entities during World::bootstrap, assert that they are contiguous (which they should be), and stick them in Components to be used before falling back to the global allocator. Is that a better approach than what we have here? Maybe?? But this PR would allow using a pure FreshEntityAllocator in Components rather than a Mutex<Vec> or whatever. (Component ids need to be generated concurrently IIRC.) It's just a matter of where you want to stick the extra complexity, and IMO, it makes sense to do this here because it can be reused for public-facing stuff if entity pages become a thing.

@Victoronz
Copy link
Copy Markdown
Contributor

Victoronz commented May 24, 2026

@Victoronz

To your first point, I'd say an index op is much faster than a hashmap lookup, so I think the perf diff would be pretty big. Even if not, it still makes sense for an ecs to pursue the maximum performance wherever possible. (But I agree that it would be nice to see the benchmarks out of curiosity.)

While we do aim for great performance, it does get more complicated once performance infrastructure changes behavior.

To your second point, we need the range of entities to start at 0 ideally for faster indexing, so starting at 0 and with a length of N incidentally forces the use of specific ranges rather than just asking for N contiguous entities. Also, an interface that just asks for N contiguous entities would be hard to depend on because of fragmentation. I don't think there's a solution to that that's dependable enough to be included in the core engine.

The thing is, Components are not the only engine entities we'd want to have a contiguous range for. If we decide to go with this pattern, we'll want to do it in-engine for other "X-As-Entities" as well. Only one of these can start at 0.
It then becomes a question of what is placed where, and have to remain synced. We can order them by priority sure enough, but that can done without specific ranges.

I'm not sure whether fragmentation is relevant for the use case of this PR. Once we spill over the first range, the rest will be fragmented. When you say it would be hard to depend on, what are you referring to?

Weighing in on the entity allocator, specifying ranges like this is extremely useful for lots of other stuff. Ex: syncing an entity id space between two worlds is really useful for multiplayer, and this makes it easy. This isn't worth making public though until we have entity paging to make large ranges performant.

I agree that there is a lot of potential use that this could see, but does that discussion belong paired to the motivation of this PR?

You're right though that this could be done in a more focused way. Just allocate N entities during World::bootstrap, assert that they are contiguous (which they should be), and stick them in Components to be used before falling back to the global allocator. Is that a better approach than what we have here? Maybe?? But this PR would allow using a pure FreshEntityAllocator in Components rather than a Mutex<Vec> or whatever. (Component ids need to be generated concurrently IIRC.) It's just a matter of where you want to stick the extra complexity, and IMO, it makes sense to do this here because it can be reused for public-facing stuff if entity pages become a thing.

I think there is still more inbetween space here. If we allow Allocators to be divided into sizes of N without defined range, would we not reach the same functionality needed for this PR?

I'm not necessarily against the more complex interface, but I think it is worthy of more detailed discussion of its own.
We have not defined any particular order for entities to be in, and that is still subject to discussion/decision.
This behavior becomes relevant if we want to expose entity order and/or allow reordering f.e.!
Once we allow specific ranges to be used, multiple use cases will competing for it.

ECS SME capacity is already sparsely allocated, so punting on these questions and/or giving them proper space may make it easier for them and other contributors to weigh in.

@Trashtalk217
Copy link
Copy Markdown
Contributor Author

Trashtalk217 commented May 24, 2026

With regards to performance. I opened a branch where the entity domain isn't split and we use normal hashmaps and hashsets: https://github.com/Trashtalk217/bevy/tree/alternate-comps-as-ents

I got the following result:

group                                                     after                                  before
-----                                                     -----                                  ------
ecs::resources::get                                       1.07      7.0±0.31ns        ? ?/sec    1.00      6.5±0.11ns        ? ?/sec
ecs::resources::get_mut                                   1.00      7.6±0.23ns        ? ?/sec    1.02      7.8±0.15ns        ? ?/sec
ecs::resources::insert_remove                             1.00    92.8±12.60ns        ? ?/sec    1.02    95.1±14.35ns        ? ?/sec

I've also ran

cargo run --release --example bevymark -- --waves 60 --per-wave 500 --benchmark --mode mesh2d

There was no material decrease in frame time. I've also ran

cargo run --release --example many_components

And had a minor increase in fps (~708 fps -> ~730fps).

This was the motivation behind entity ranges. Given that flecs also uses this, I feel confident that this will help.

To roughly respond to (some of) your comments:

A more focused change would be to say "Give me N contiguous entities"

Sure, if I were able to break off an EntityAllocator<N> from the main allocator (remember that components have to be able to be registered concurrently), that would work too. But this was by far the simplest thing to implement. If I allocated N entities beforehand and handed them out again through some sort of ComponentIdAllocator, I'd unnecessarily be recreating another EntityAllocator. I don't see the point in doing that, to be honest.

Only one of these can start at 0. It then becomes a question of what is placed where, and have to remain synced.

Components should get the 0 index, because they are components. They are easily the most important entities in the ECS.

@ElliottjPierce
Copy link
Copy Markdown
Contributor

I'm not sure whether fragmentation is relevant for the use case of this PR. Once we spill over the first range, the rest will be fragmented. When you say it would be hard to depend on, what are you referring to?

I just mean that I'd be wary of putting code in the allocator that assumes there is no fragmentation. It could easily be misused.

I think there is still more inbetween space here. If we allow Allocators to be divided into sizes of N without defined range, would we not reach the same functionality needed for this PR?

You mean like a private EntityAllocator::alloc_range(n: u32) -> Range<u32>? And we pinky promise to only call it on startup? Yeah, that could work, but then we need to build a new entity allocator to hand out that range, which isn't an issue, but I don't think that's any less complex than this. I'd still be open to that though; there's nothing wrong with that approach.

Only one of these can start at 0. It then becomes a question of what is placed where, and have to remain synced. We can order them by priority sure enough, but that can done without specific ranges.

This is true, but I think components are the only thing that would benefit from having a dense id like this. Systems, assets, etc could have too many entities to reliably fit in a pre-allocated range anyway. Plus, most of their data will be stored as components on their entities. Components are the one and only exception to this. That is, "components as entities" is the only case where the actual data can't be stored only as components because that would be circular. If we let components as entities start at 0, literately ever other X-as-entities becomes faster.


As it stands, the only thing I would flag with this approach as being wastefully over complicated is that the components entity id allocator doesn't need to have free/reuse functionality. But that's a simple fix if we let the FeshEntityAllocator be pub(crate), and I don't see any reason to not do that when this is used for components as entities in a future PR.

I'm open to other designs, but unless I'm misunderstanding (which is entirely possible), I don't think there's an objectively simpler approach here, and the reusability with entity pages (maybe) is certainly a cherry on top.

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 D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Needs-SME This type of work requires an SME to approve it.

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.