Skip to content

Components as entities (v2) - #24728

Open
Trashtalk217 wants to merge 53 commits into
bevyengine:mainfrom
Trashtalk217:components-as-entities-alt
Open

Trashtalk217 wants to merge 53 commits into
bevyengine:mainfrom
Trashtalk217:components-as-entities-alt

Conversation

@Trashtalk217

Copy link
Copy Markdown
Contributor

See #23988.

Objective

There are two primary problems we currently have with resources-as-components (#19731) .

Performance regression #23039

In order to get access to a resource entity, you usually need to go through two lookups: TypeId -> ComponentId -> Entity. The ComponentId -> Entity lookup can potentially be removed, speeding up resource lookup.

World Asset Serialization (previously in bevy_scene) #22968

Currently, one big downside of resources-as-components is that any components on resource entities aren't serialized. This forms a barrier for implementing required components for resources. Implementing this correctly is tricky. One of the problems we run into is that IsResource(ComponentId) cannot be directly copied over because ComponentIds are not consistent between worlds.

Solution

We change ComponentId(usize) into ComponentId(Entity) and use the EntityAllocator to create ComponentIds. Next, we store all resources on the ComponentId entity. This makes the ComponentId -> Entity lookup a no-op. And we can also use the MapEntities machinery to serialize and deserialize worlds.

Future Work

In the future, we can look at adding parts of ComponentInfo as a component to the component entities. That sounds confusing, because it is. This would improve the introspection ability of the ECS (being able to query for metadata about the components). This, however, requires quite a bit of weird bootstrapping, which I don't know how to do and is not necessary for this PR.

Testing

This is principally a performance PR, and while I will be doing some benchmarking myself, I don't have the hardware. So I'll be asking someone to give it a once-over when it's ready.

Change

The primary difference between this and #23988, is that this is not reliant on Entity Ranges (#24102). Because of this, there is a possibility for a performance regression with regards to Components.components going from a SparseArray to HashMap. There is an additional hash operation. Additionally, the fields on Access have changed from a FixedBitSet to a HashSet, so the set operations are likely slower.

@Trashtalk217 Trashtalk217 changed the title Components as Entities (V2) Components as Entities (v2) Jun 23, 2026
@Trashtalk217 Trashtalk217 added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times C-Code-Quality A section of code that is hard to understand or change X-Contentious There are nontrivial implications that should be thought through labels Jun 23, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS Jun 23, 2026
@Trashtalk217 Trashtalk217 added S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jun 23, 2026
@Trashtalk217 Trashtalk217 changed the title Components as Entities (v2) Components as entities (V2) Jun 23, 2026
@Trashtalk217 Trashtalk217 changed the title Components as entities (V2) Components as entities (v2) Jun 23, 2026
@Trashtalk217
Trashtalk217 requested a review from chescock June 23, 2026 23:44
@Trashtalk217

Trashtalk217 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Performance Testing

I did some reasonably extensive performance testing. I ran both the entire ecs benchmark and several of our stress test examples. I've got the following data:

And the three examples I ran through were

  • cargo run --release --example bevymark -- --waves 60 --per-wave 500 --benchmark --mode mesh2d
  • cargo run --release --example many_components 100000 1000 1000
  • running bevy_city with mangohud.
name frametime before (main) frametime after (pr)
bevymark 37.5 ms 37.8 ms
many_components 6.6 ms 6.5 ms
bevy_city 90.5 ms 90.5 ms

Conclusion

From the measurements I took I've come to the conclusion that this PR mostly keeps performance the same. At worst it degrades slightly (mostly based on the microbenchmarks). The best solution to fix the resource performance issue is likely still going to be #24058 or something like it.

However!

I still think this PR is worth merging, because

  1. It's a step to storing a meta-component on the component entity, allowing for more introspection in the ecs.
  2. Users can tag component entities with their own data ComponentInfo extensions #24338.
  3. It removes an indirection in the code via ResourceEntities and with it some unsafe code.
  4. The (in my opinion minor) loss of performance can in the future be offset by introducing entity ranges (Entity ranges #24102) and hybrid datastructures. For more information checkout the entity ranges PR (or ask!).
  5. It makes working with resources more consistent. Before, world.spawn((Res1, Res2)) would store Res2 on the resource entity of Res1, so Res2 would be stored on the wrong location. In this PR, this would throw a warning and remove both Res1 and Res2 because neither are on the appropriate entity.

Important Note

When review and discussing this PR, I prefer if you mostly kept it anchored to some line of code (even if only vaguely related). This makes it easier to follow particular threads of argument.

@Trashtalk217 Trashtalk217 removed the S-Needs-Benchmarking This set of changes needs performance benchmarking to double-check that they help label Jun 24, 2026

@Victoronz Victoronz left a comment

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.

Looks good! I don't believe the #24102 is necessary for this, and can be left for later.

With this change, it becomes possible to express uniqueness regarding ComponentId collections/iterators as well. There are now likely several sections of code where it would make sense to convert the previous ComponentId collection/iterator types to their EntitySet version. That would make for a good follow-up PR!

One place for which this is especially relevant is DynamicComponentFetch, the current dynamic way to access the Components of an entity disjointly.
This functionality should fall under similar design considerations as #18234, though that PR has gotten stalled.

Comment thread _release-content/migration-guides/components-as-entities.md Outdated
Comment thread crates/bevy_ecs/src/world/mod.rs Outdated
@JaySpruce JaySpruce removed the S-Merge-Conflicts Merge conflicts :( Add this label on top of other S- labels. label Aug 25, 2026
@Zeophlite Zeophlite added the S-Merge-Conflicts Merge conflicts :( Add this label on top of other S- labels. label Sep 1, 2026

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.

This still uses a SparseSet over ComponentIds.

Note however that this is accessed once per table when iterating queries (that is, NOT when the QueryState/Query is constructed, but rather every time it's iterated, and multiple times per iteration), so changing it to a EntityHashMap might regress performance for query iteration.

@Trashtalk217

Trashtalk217 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

I want to place a brief note with regards to this commit: fb25d44

Here I formally replace a couple of ImmutableSparseArray<ComponentId, ...>s. I did this because sparse arrays take up space relative to the highest id stored, instead of the number of ids. Since ComponentIds can get pretty high due to this PR, this is potentially a memory issue.

I've replaced them with ComponentIdMaps (basically HashMaps). This solves the space issue, but takes more time. As per @SkiFire13, the following operations are likely slower: Query iteration, hook access, and archetype creation.

To figure out how much slower (if at all), I (or someone with a better PC) will have to rerun the benchmarks.

EDIT: To re-iterate, this is a solvable problem. Namely, #24102 could make it so the first N components are dense and from that point onwards it's some pretty basic data structure work.

pub struct ComponentsQueuedRegistrator<'w> {
components: &'w Components,
ids: &'w ComponentIds,
allocator: RemoteAllocator,

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.

Couldn't this be &'w EntityAllocator since EntityAllocator::alloc takes &self?

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.

Should be able to downgrade &'w mut EntityAllocator to a shared reference in ComponentsRegistrator as well.

@SkiFire13 SkiFire13 left a comment

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.

This is still a couple of type still using SparseSet<ComponentId>:

  • RemovedComponentMessages
  • NonSends
  • SparseSets

This could however be fine since these are all "one-instance" types, so we would not end up paying the cost for every table or archetype.

Comment thread crates/bevy_ecs/src/archetype.rs Outdated
entities: Vec<ArchetypeEntity>,
components: ImmutableSparseSet<ComponentId, ArchetypeComponentInfo>,
component_ids: Vec<ComponentId>,
archetype_components: ComponentIdMap<ArchetypeComponentInfo>,

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.

We should be able to remove this field completely, because all it stores is the StorageType for the given ComponentId, which we can fetch from Components as needed.

Comment thread crates/bevy_ecs/src/archetype.rs Outdated
edges: Edges,
entities: Vec<ArchetypeEntity>,
components: ImmutableSparseSet<ComponentId, ArchetypeComponentInfo>,
component_ids: Vec<ComponentId>,

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.

Box<[ComponentId]>, the component list is static for a given archetype.

@Zeophlite Zeophlite removed the S-Merge-Conflicts Merge conflicts :( Add this label on top of other S- labels. label Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change C-Performance A change motivated by improving speed, memory usage or compile times S-Waiting-on-SME This is currently waiting for an SME to resolve something controversial 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.

9 participants