Skip to content

Basic inherited components/entity prefabs #18767

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

eugineerd
Copy link
Contributor

Objective

This PR adds an implementation of inherited components and entity prefabs. This is useful for creating dynamic entity templates and allows component sharing, which reduces memory pressure when there are many entities with similar components. Some of the prime example of where this is useful are entity-per-tile tilemaps and ecs-based physics implementations.

Solution

Any entity can add an InheritFrom component to opt-into inheriting components from the target entity.

let base = world.spawn(ComponentA).id();
let inherited = world.spawn(InheritFrom(base)).id();

let base_entity = world.entity(base);
let inherited_entity = world.entity(inherited);

let base_component_a = base_entity.get::<ComponentA>();
assert_eq!(inherited_entity.get::<ComponentA>(), base_component_a);

Any changes to base entity will affect all entities inheriting from it as well. Inherited components are stored only on the base entity, so all entities inheriting a component from the base entity share the same component data.

Getting a shared component is supported only for non-mutable references for now, trying to get a mutable reference to a shared component will result in the same behavior as if requested component doesn't exist on the entity.

Implementation

The implementation is split into several commits, each containing a logical chunk to make reviewing it easier.

There's a new field on world, InheritedComponents, that has all the data needed to resolve inherited components for archetypes/tables. If archetype/table has an InheritFrom component, it is opted-into resolving inherited components during iteration.

Most of the functionality is active only on tables/archetypes that have inherited components, so this shouldn't impact performance of normal queries too much. However, iteration performance benchmarks seem to suffer anyway and need more investigation.

One other problem is with systems that have queries working with both inherited components and base components - this system should conflict, but it doesn't right now:

fn query_system(
    base_q: Query<&mut CompA, With<Inherited>>,
    inherited_q: Query<&CompA, Without<Inherited>>,
) {
}

If anyone has any ideas about how to fix this problem and the iteration performance, it'd solve most of the blockers for this PR (from my perspective).

Testing

Added tests for all the basic functionality.
Not all test pass yet, and there might be more cases that need testing.

Future work

This implementation implements only the basic parts of component inheritance, some important stuff that's still missing:

  • Make required components' requirement fulfilled if inherited entity contains the component
  • Support mutable references for inherited components
  • Support filtering which components get inherited on the base entity
  • Support filtering which components get inherited on the inherited entity
  • Allow marking components as not inheritable

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events X-Controversial There is active debate or serious implications around merging this PR M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 8, 2025
Copy link
Contributor

github-actions bot commented Apr 8, 2025

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

@alice-i-cecile
Copy link
Member

I did some complimentary work over in #17769, working on the "create a library of entities to draw from" pattern using disabling components.

@@ -5,6 +5,7 @@

Copy link
Member

@cart cart Apr 8, 2025

Choose a reason for hiding this comment

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

I haven't done a review yet, but this PR seems similar in shape to the Flecs approach, which is cool and worthy of consideration, but I've always been hesitant about it. My biggest concerns about that approach:

  1. Lookup overhead: data accesses need to do more work to check if a component is inherited or not, and to reach into the correct table. I'd want comprehensive benchmarks of every code path this touches.
  2. Data access issues: inherited components are "shared data". Our whole access model assumes that if we lock down to the scope of an entity, its accesses will not intersect with other entities. Providing mutable access would violate this naively.
  3. User interface concerns: especially in combination with (2), this would almost certainly introduce user-facing interface weirdness. How would we write a system that modifies the position of all Transforms, when some of those transforms are inherited? Foisting this complexity on every system author isn't viable, so we would need to internalize it. Would we make Mut "smart" and have it check on every access whether it can write directly to the pointer or whether it needs to queue up a command to "clone on write"? That would introduce massive overhead as Mut is very "hot". Do we make only immutable components inheritable? If so then this system becomes effectively useless for prefabs, as we need a prefab system to support the whole range of bevy components.

There are certainly benefits to this model (memory usage and "inherited spawn speed" being big ones). But before taking a single step on this road (in terms of merged features), I'd want satisfying / holistic answers to all of those questions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've though about these concerns when working on this implementation, I think it should be possible?

  1. All entities that inherit components are stored in a separate archetype. This means that looking up which component is actually used needs to be done only once per archetype when iterating. It theoretically should be fast enough if there are many instances of a single entity prefab (and maybe even faster since there is less data that needs to be cached?).
  2. For this, I think that as long as inherited entities' components are not accessible mutably in the same system as entities which inherit these components, this should still probably work.
  3. Copy-on-write behavior should be a fairly rare occasion most of the time (it would happen only once) and I hope that we can optimize for the branch predictor to consider this branch cold.

Copy link
Member

@cart cart Apr 8, 2025

Choose a reason for hiding this comment

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

All entities that inherit components are stored in a separate archetype. This means that looking up which component is actually used needs to be done only once per archetype when iterating

This will also add a fixed cost to Query::get and EntityWorldMut::get ops right?

For this, I think that as long as inherited entities' components are not accessible mutably in the same system as entities which inherit these components, this should still probably work

This was referring to two entities that both inherit from a third entity both trying to mutably access the same component.

Copy-on-write behavior should be a fairly rare occasion most of the time (it would happen only once) and I hope that we can optimize for the branch predictor to consider this branch cold.

I'm curious to see what Mut COW looks like in practice. There are also corner cases to consider like two queries in a query set that both access the same (inherited) component mutably. How would they coordinate to ensure that both changes are applied / they don't step on each other? Note that structural changes cannot be applied within a normal system.

Copy link
Member

Choose a reason for hiding this comment

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

I'm dubious of the overhead and complexity this will introduce, but I'm also very excited and curious to see where this lands!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will also add a fixed cost to Query::get and EntityWorldMut::get ops right?

Yes, there's a flag for the archetype and the lookup is done only if the archetype contains any inherited components, which is what flecs does.

There are also corner cases to consider like two queries in a query set that both access the same (inherited) component mutably. How would they coordinate to ensure that both changes are applied / they don't step on each other? Note that structural changes cannot be applied within a normal system.

We can make a staging hashmap for inherited components that have been mutated during system runtime and return them if they're requested multiple times in the same system, if I understood your question.

@eugineerd
Copy link
Contributor Author

eugineerd commented Apr 14, 2025

Been stuck in optimization hell staring at assembly code and llvm ir while implementing shared mutable components, but I've finally managed to implement the basic version of this. Here are the results of my struggles:

Iteration benchmarks
Benchmark Main (ns/µs) PR (ns/µs) Change
iter_fragmented/base 314.00-318.83 ns 327.05-332.22 ns +2.99% to +4.60% (regressed)
iter_fragmented/wide 4.8318-4.8808 µs 4.8904-4.9244 µs +0.82% to +2.56% (noise)
iter_fragmented/foreach 150.64-159.99 ns 141.75-150.94 ns -10.58% to -3.87% (improved)
iter_fragmented/foreach_wide 5.9477-6.0151 µs 18.166-18.228 µs +204.39% to +207.85% (regressed)
iter_fragmented_sparse/base 7.3423-7.8950 ns 7.8041-7.9021 ns +0.13% to +6.02% (noise)
iter_fragmented_sparse/wide 48.128-48.368 ns 80.809-81.130 ns +67.26% to +68.52% (regressed)
iter_fragmented_sparse/foreach 7.3979-8.3397 ns 7.3064-7.4126 ns -9.68% to -1.02% (improved)
iter_fragmented_sparse/foreach_wide 75.540-75.974 ns 212.62-213.90 ns +179.89% to +183.44% (regressed)
iter_simple/base 6.8433-6.8954 µs 6.8330-6.8513 µs -0.44% to +0.95% (no change)
iter_simple/wide 41.296-41.581 µs 65.385-65.533 µs +57.34% to +58.83% (regressed)
iter_simple/system 6.8414-6.8629 µs 6.8550-6.8718 µs -0.09% to +0.36% (no change)
iter_simple/sparse_set 21.107-21.186 µs 21.164-21.315 µs +0.26% to +0.87% (noise)
iter_simple/wide_sparse_set 89.770-90.504 µs 102.47-102.87 µs +13.92% to +14.81% (regressed)
iter_simple/foreach 6.8061-6.8267 µs 6.7805-6.8012 µs -0.52% to -0.01% (no change)
iter_simple/foreach_wide 31.290-31.402 µs 46.171-46.293 µs +47.20% to +48.19% (regressed)
iter_simple/foreach_sparse_set 20.834-20.938 µs 20.650-20.726 µs -1.55% to -1.03% (improved)
iter_simple/foreach_wide_sparse_set 96.508-96.770 µs 116.87-117.19 µs +20.51% to +21.13% (regressed)
iter_simple/foreach_hybrid 8.8300-8.8746 µs 9.0273-9.0633 µs +1.91% to +2.43% (regressed)
par_iter_simple/with_0_fragment 36.391-36.702 µs 36.334-36.761 µs -0.42% to +2.29% (no change)
par_iter_simple/with_10_fragment 36.630-36.877 µs 36.759-37.115 µs -0.54% to +2.10% (no change)
par_iter_simple/with_100_fragment 37.188-37.656 µs 37.434-38.180 µs -1.07% to +1.94% (no change)
par_iter_simple/with_1000_fragment 46.469-47.187 µs 47.155-47.807 µs -0.43% to +2.81% (no change)
par_iter_simple/hybrid 74.036-74.795 µs 74.586-75.909 µs -1.08% to +2.62% (no change)
iter_fragmented(4096)_empty/foreach_table 3.2693-3.3137 µs 2.8646-2.8891 µs -12.61% to -11.11% (improved)
iter_fragmented(4096)_empty/foreach_sparse 10.008-10.078 µs 11.040-11.087 µs +9.76% to +10.70% (regressed)

The results are... not too bad, I think. There is a clear regression for wide queries, but smaller queries seem to perform just fine. It might be possible to optimize wide queries, but I doubt it. Looking at the generated assembly, there is a branch on each access (test followed by jnz), so I believe this is just hitting the limitations of the branch predictor at this point. All of the benchmarks are really tight loops, so I suspect the impact on real-world code that has much more going on will be somewhat different.
Some of the benchmarks that seem to have improved are probably due to the additional #[inline]'s I had to sprinkle throughout the fetch implementation.

This implementation uses component_id + table_id/archetype_id as a key for the staged mutated components hashmap that stores the cloned components on deref_mut, so in QuerySet the conflicting queries with the same shared mutable components will see the changes introduced by each other. Then, on world.flush, the staged components are inserted on the target entity.

I need to clean up the implementation a bit before I push the changes here, but overall I think there might be a path forward for this feature (if we're ok with taking the hit for wide queries).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants