Skip to content

Entity ranges#24102

Open
Trashtalk217 wants to merge 13 commits intobevyengine:mainfrom
Trashtalk217:entity-ranges
Open

Entity ranges#24102
Trashtalk217 wants to merge 13 commits intobevyengine: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

self.next_entity_index
.store(self.max_index, Ordering::Relaxed);
return None;
} else if index == Self::MAX_ENTITIES {
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.

I think this if branch needs to come first because max_index might incidentally be u32::MAX.

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.

Actually, this might be being compiled out right now, since u32::MAX >= N for any N. Might affect performance.

pub(crate) fn new(range: &Range<u32>) -> Self {
/// This exists because it may possibly change depending on platform.
/// Ex: We may want this to be smaller on 32 bit platforms at some point.
const MAX_ENTITIES: u32 = u32::MAX;
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.

I'm not against adding a constant like this, but if we do, it shouldn't live here. There are a lot of other places that use u32::MAX throughout the whole crate.

Comment on lines +775 to 781
if index >= self.max_index {
self.next_entity_index
.store(self.max_index, Ordering::Relaxed);
return None;
} else if index == Self::MAX_ENTITIES {
Self::on_overflow();
}
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.

Actually, this might be being compiled out right now, since u32::MAX >= N for any N. Might affect performance.

Yeah, if we hit MAX_ENTITIES then we've already hit max_index. One option is to put the MAX_ENTITIES check inside the max_index check, which should avoid the second check in the common case of no overflow:

Suggested change
if index >= self.max_index {
self.next_entity_index
.store(self.max_index, Ordering::Relaxed);
return None;
} else if index == Self::MAX_ENTITIES {
Self::on_overflow();
}
if index >= self.max_index {
self.next_entity_index
.store(self.max_index, Ordering::Relaxed);
if index >= Self::MAX_ENTITIES {
Self::on_overflow();
}
return None;
}

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

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-Needs-Review Needs reviewer attention (from anyone!) to move forward 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.