From 2ea86a797a80de65d522ef65491595f60e758063 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 17:49:26 -0700 Subject: [PATCH 01/19] First pass --- rfcs/minimal-fragmenting-relationships.md | 120 ++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 rfcs/minimal-fragmenting-relationships.md diff --git a/rfcs/minimal-fragmenting-relationships.md b/rfcs/minimal-fragmenting-relationships.md new file mode 100644 index 00000000..e387606a --- /dev/null +++ b/rfcs/minimal-fragmenting-relationships.md @@ -0,0 +1,120 @@ +# Feature Name: `minimal-fragmenting-relationships` + +## Summary + +The central idea behind fragmenting relationships is the ability to express a connection between two `Entity`s by adding a pair of `(ComponentId, Entity)` as if it were a component. For example if one had several entities representing different kinds of swords one could add the pair `(Equipped, sword_entity)` to the player to signify which is currently equipped. + +Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to fragmenting relationships is the pair is a unique component. This means we could also add `(Equipped, shield_entity)` to the same player and offer query APIs that express `(Equipped, *)`to find all equipped pairs. + +Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. +## Motivation + +Fragmenting relationships has been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of fragmenting relationships. +## User-facing explanation + +Relationship pairs can be inserted and removed like any other component. There are 3 ids associated with any relationship pair instance: +1. The source entity that the pair is being added to +2. The relationship component +3. The target entity +In the minimal implementation all relationships will take the form of `(ZST Component, entity)` pairs so the API surface will look quite familiar. All naming is subject to bikeshedding: +```rust +#[derive(Component)] +struct Relationship {} + +world.entity_mut(source_entity_a) + .insert_pair::(target_entity_b) + .remove_pair::(target_entity_b); + +command.entity(source_entity_a) + .insert_pair::(target_entity_b) + .remove_pair::(target_entity_b); +``` + +Since each pair is a unique component you can also add multiple entities with the same relationship type, as well as use APIs to access and query for them: +```rust +#[derive(Component)] +struct Eats {} + +let alice_mut = world.entity_mut(alice); + +alice.insert_pair::(apple) + .insert_pair::(banana); + +alice.has_pair(apple); // == true + +for target in alice.targets() { + // target == apple + // target == banana +} + +let query = world.query<(Entity, Targets)>(); +for entity, targets in query.iter(&world) { + // entity == alice + for target in targets { + // target == apple + // target == banana + } +} + +lt query = world.query>>(); +for entity in query.iter(&world) { + // entity == alice +} + +``` + +Dynamic queries would also be able to match on specific pairs: +```rust +let query = QueryBuilder::new::(&mut World) + .with_pair::(apple) + .build(); + +query.single(&world); // == alice +``` + +## Implementation strategy + +There are a number of technical blockers before even a minimal version of relationships could be implemented in bevy. I've broken them down below: +#### Archetype Cleanup +In order to fit each pair into a single id we lose the entity generation (see this [article](https://ajmmertens.medium.com/doing-a-lot-with-a-little-ecs-identifiers-25a72bd2647) for more details). As a consequence when an entity is despawned we need to ensure that all the archetypes that included that entity as a target are also destroyed so that a future entity re-using that id is not misinterpreted as being the old target. This is non-trivial. + +`bevy_ecs` currently operates under the assumption that archetype and component ids are dense and strictly increasing. In order to break that invariant we need to address several issues: +##### Query and System Caches +Query's caches of matching tables and archetypes are updated each time the query is accessed by iterating through the archetypes that have been created since it was last accessed. As the number of archetypes in bevy is expected to stabilize over the runtime of a program this isn't currently an issue. However with the increased archetype fragmentation caused by fragmenting relationships and the new wrinkle of archetype deletion the updating of the query caches is poised to become a performance bottleneck. + +Luckily observers [#10839](https://github.com/bevyengine/bevy/pull/10839) can help solve this problem. By sending ECS events each time an archetype is created and creating an observer for each query we can target the updates to query caches so that we no longer need to iterate every new archetype. Similarly we can fire archetype deletion events to cause queries to remove the archetype from their cache. + +In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache to optimize for removal. However this is largely blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). + +Similarly system's cache their `ArchetypeComponentId` accesses for the purposes of multithreading, we also need to update theses caches to remove deleted ids presenting many of the same problems. +##### Access Bitsets and Component SparseSets +The `FixedBitSet` implementation used by `Access` inside of systems, system params, queries etc. relies on dense component ids to avoid using a large amount of memory. If we were to start inserting ids with arbitrarily large entity targets as well as component ids shifted into the upper bits each `FixedBitSet` would allocate huge amounts of memory to have flags for all the intervening ids. In order to minimize the affects of this we could consider all `(R, *)` pairs as one id in accesses, reducing the amount we need to track at the expense of some theoretical ability to parallelise. + +Similarly the `SparseSet` implementation used in both table and sparse component storage also operate under the assumption that component ids will remain low and would allocate an even larger amount of memory storing indexes for all the intervening ids when storing relationship pairs on an entity. + +The most practical way to solve this is using paged versions of the appropriate data structures, for bitsets this can take the form of existing hierarchical or compressed bitsets like `roaring` or `tinyset`. +### Minimal Implementation +Implementing the above in order of dependance could look like: +- Resolve [#12144](https://github.com/bevyengine/bevy/issues/12144) so entities can be persisted in the render world. +- Merge observers [#10839](https://github.com/bevyengine/bevy/pull/10839) +- Refactor queries to become entities with caches updated via observers +- Implement archetype deletion and the associated clean-up for systems and queries +- Replace `FixedBitSet` and `SparseSet` implementations with ones that support sparse ids +After all the issues above are addressed the actual implementation of the feature can begin. This requires changes in 3 main places: +- Implementation of `WorldData` and `WorldFilter` implementations for accessing relationship targets +- Transition to new `Identifier` implemented in [#9797](https://github.com/bevyengine/bevy/pull/9797) instead of `ComponentId` in all places where pairs could be expected: tables, archetypes etc. +- New methods for `EntityCommands`, `EntityMut`/`Ref`/`WorldMut` and `QueryBuilder` +## Drawbacks +- Increased archetype fragmentation +- Increase in internal and API complexity +## Prior art +- Prior [RFC](https://github.com/BoxyUwU/rfcs/blob/min-relations/rfcs/min-relations.md) +- [flecs](https://github.com/SanderMertens/flecs/blob/master/docs/Relationships.md) +- Partial draft implementation: [#9123](https://github.com/bevyengine/bevy/pull/9123) +## Unresolved questions +- How to address [#12144](https://github.com/bevyengine/bevy/issues/12144) + +## Future possibilities +- More advanced traversal methods: up, BFS down, etc. +- More expressive query types: multi-target, grouping, sorted etc. +- With component as entities we unlock full `(entity, entity)` relationships \ No newline at end of file From d096ba936b499bf5f66f606f98222f92944f98b5 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 17:55:33 -0700 Subject: [PATCH 02/19] Formatting fixes --- ...hips.md => 79-minimal-fragmenting-relationships.md} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename rfcs/{minimal-fragmenting-relationships.md => 79-minimal-fragmenting-relationships.md} (92%) diff --git a/rfcs/minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md similarity index 92% rename from rfcs/minimal-fragmenting-relationships.md rename to rfcs/79-minimal-fragmenting-relationships.md index e387606a..c131baa8 100644 --- a/rfcs/minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -16,6 +16,7 @@ Relationship pairs can be inserted and removed like any other component. There a 1. The source entity that the pair is being added to 2. The relationship component 3. The target entity + In the minimal implementation all relationships will take the form of `(ZST Component, entity)` pairs so the API surface will look quite familiar. All naming is subject to bikeshedding: ```rust #[derive(Component)] @@ -30,7 +31,7 @@ command.entity(source_entity_a) .remove_pair::(target_entity_b); ``` -Since each pair is a unique component you can also add multiple entities with the same relationship type, as well as use APIs to access and query for them: +Since each pair is a unique component you can also add multiple target entities with the same relationship type, as well as use APIs to access and query for them: ```rust #[derive(Component)] struct Eats {} @@ -86,9 +87,9 @@ Luckily observers [#10839](https://github.com/bevyengine/bevy/pull/10839) can he In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache to optimize for removal. However this is largely blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). -Similarly system's cache their `ArchetypeComponentId` accesses for the purposes of multithreading, we also need to update theses caches to remove deleted ids presenting many of the same problems. +Similarly systems cache their `ArchetypeComponentId` accesses for the purposes of multithreading, we also need to update theses caches to remove deleted ids presenting many of the same problems. ##### Access Bitsets and Component SparseSets -The `FixedBitSet` implementation used by `Access` inside of systems, system params, queries etc. relies on dense component ids to avoid using a large amount of memory. If we were to start inserting ids with arbitrarily large entity targets as well as component ids shifted into the upper bits each `FixedBitSet` would allocate huge amounts of memory to have flags for all the intervening ids. In order to minimize the affects of this we could consider all `(R, *)` pairs as one id in accesses, reducing the amount we need to track at the expense of some theoretical ability to parallelise. +The `FixedBitSet` implementation used by `Access` inside of systems, system params, queries etc. relies on dense component ids to avoid using a large amount of memory. If we were to start inserting ids with arbitrarily large entity targets as well as component ids shifted into the upper bits each `FixedBitSet` would allocate non-trivial amounts of memory to have flags for all the intervening ids. In order to minimize the affects of this we could consider all `(R, *)` pairs as one id in accesses, reducing the amount we need to track at the expense of some theoretical ability to parallelise. Similarly the `SparseSet` implementation used in both table and sparse component storage also operate under the assumption that component ids will remain low and would allocate an even larger amount of memory storing indexes for all the intervening ids when storing relationship pairs on an entity. @@ -100,7 +101,7 @@ Implementing the above in order of dependance could look like: - Refactor queries to become entities with caches updated via observers - Implement archetype deletion and the associated clean-up for systems and queries - Replace `FixedBitSet` and `SparseSet` implementations with ones that support sparse ids -After all the issues above are addressed the actual implementation of the feature can begin. This requires changes in 3 main places: +After all the issues above are addressed the actual implementation of the feature can begin. This requires changes in 3 main areas: - Implementation of `WorldData` and `WorldFilter` implementations for accessing relationship targets - Transition to new `Identifier` implemented in [#9797](https://github.com/bevyengine/bevy/pull/9797) instead of `ComponentId` in all places where pairs could be expected: tables, archetypes etc. - New methods for `EntityCommands`, `EntityMut`/`Ref`/`WorldMut` and `QueryBuilder` @@ -113,7 +114,6 @@ After all the issues above are addressed the actual implementation of the featur - Partial draft implementation: [#9123](https://github.com/bevyengine/bevy/pull/9123) ## Unresolved questions - How to address [#12144](https://github.com/bevyengine/bevy/issues/12144) - ## Future possibilities - More advanced traversal methods: up, BFS down, etc. - More expressive query types: multi-target, grouping, sorted etc. From da7eccb7c3d1a92dc3d8190c27d2ad84b8f03914 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 17:58:27 -0700 Subject: [PATCH 03/19] Minor additions --- rfcs/79-minimal-fragmenting-relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index c131baa8..c1d4d616 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -6,7 +6,7 @@ The central idea behind fragmenting relationships is the ability to express a co Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to fragmenting relationships is the pair is a unique component. This means we could also add `(Equipped, shield_entity)` to the same player and offer query APIs that express `(Equipped, *)`to find all equipped pairs. -Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. +Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. ## Motivation Fragmenting relationships has been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of fragmenting relationships. From 7a5c1a0ea723c235b6005c3b1b28a73092b6fa07 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 18:06:24 -0700 Subject: [PATCH 04/19] Formatting --- rfcs/79-minimal-fragmenting-relationships.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index c1d4d616..e75073e3 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -39,7 +39,7 @@ struct Eats {} let alice_mut = world.entity_mut(alice); alice.insert_pair::(apple) - .insert_pair::(banana); + .insert_pair::(banana); alice.has_pair(apple); // == true @@ -101,6 +101,7 @@ Implementing the above in order of dependance could look like: - Refactor queries to become entities with caches updated via observers - Implement archetype deletion and the associated clean-up for systems and queries - Replace `FixedBitSet` and `SparseSet` implementations with ones that support sparse ids + After all the issues above are addressed the actual implementation of the feature can begin. This requires changes in 3 main areas: - Implementation of `WorldData` and `WorldFilter` implementations for accessing relationship targets - Transition to new `Identifier` implemented in [#9797](https://github.com/bevyengine/bevy/pull/9797) instead of `ComponentId` in all places where pairs could be expected: tables, archetypes etc. From 732dfff22f114d749701f244bf1ebf15478d2f7d Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 18:07:29 -0700 Subject: [PATCH 05/19] Fix example --- rfcs/79-minimal-fragmenting-relationships.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index e75073e3..86b6a6e5 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -38,12 +38,12 @@ struct Eats {} let alice_mut = world.entity_mut(alice); -alice.insert_pair::(apple) +alice_mut.insert_pair::(apple) .insert_pair::(banana); -alice.has_pair(apple); // == true +alice_mut.has_pair(apple); // == true -for target in alice.targets() { +for target in alice_mut.targets() { // target == apple // target == banana } From 2477cece10b03033432af2127fe9e701ffcccdd3 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 18:10:35 -0700 Subject: [PATCH 06/19] Fix crate references --- rfcs/79-minimal-fragmenting-relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 86b6a6e5..64b81e09 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -93,7 +93,7 @@ The `FixedBitSet` implementation used by `Access` inside of systems, system para Similarly the `SparseSet` implementation used in both table and sparse component storage also operate under the assumption that component ids will remain low and would allocate an even larger amount of memory storing indexes for all the intervening ids when storing relationship pairs on an entity. -The most practical way to solve this is using paged versions of the appropriate data structures, for bitsets this can take the form of existing hierarchical or compressed bitsets like `roaring` or `tinyset`. +The most practical way to solve this is using paged versions of the appropriate data structures, for bitsets this can take the form of existing hierarchical or compressed bitsets like `roaring`. ### Minimal Implementation Implementing the above in order of dependance could look like: - Resolve [#12144](https://github.com/bevyengine/bevy/issues/12144) so entities can be persisted in the render world. From 219ebfe77482bcb6c15f85086b59ae361f14ce66 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 18:15:12 -0700 Subject: [PATCH 07/19] Formatting --- rfcs/79-minimal-fragmenting-relationships.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 64b81e09..23f02c37 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -6,7 +6,9 @@ The central idea behind fragmenting relationships is the ability to express a co Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to fragmenting relationships is the pair is a unique component. This means we could also add `(Equipped, shield_entity)` to the same player and offer query APIs that express `(Equipped, *)`to find all equipped pairs. -Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. +Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below.  + +The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. ## Motivation Fragmenting relationships has been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of fragmenting relationships. From c4448a369e3202303144ac70763087b71b3d210f Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 19:54:59 -0700 Subject: [PATCH 08/19] Minor cleanup --- rfcs/79-minimal-fragmenting-relationships.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 23f02c37..db620aea 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -6,9 +6,7 @@ The central idea behind fragmenting relationships is the ability to express a co Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to fragmenting relationships is the pair is a unique component. This means we could also add `(Equipped, shield_entity)` to the same player and offer query APIs that express `(Equipped, *)`to find all equipped pairs. -Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below.  - -The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. +Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. ## Motivation Fragmenting relationships has been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of fragmenting relationships. @@ -22,7 +20,7 @@ Relationship pairs can be inserted and removed like any other component. There a In the minimal implementation all relationships will take the form of `(ZST Component, entity)` pairs so the API surface will look quite familiar. All naming is subject to bikeshedding: ```rust #[derive(Component)] -struct Relationship {} +struct Relationship; world.entity_mut(source_entity_a) .insert_pair::(target_entity_b) @@ -36,7 +34,7 @@ command.entity(source_entity_a) Since each pair is a unique component you can also add multiple target entities with the same relationship type, as well as use APIs to access and query for them: ```rust #[derive(Component)] -struct Eats {} +struct Eats; let alice_mut = world.entity_mut(alice); @@ -118,6 +116,10 @@ After all the issues above are addressed the actual implementation of the featur ## Unresolved questions - How to address [#12144](https://github.com/bevyengine/bevy/issues/12144) ## Future possibilities +- Data on relationship pairs is a trivial extension: + ```rust + world.entity_mut(source).insert_pair(Relationship {}, target); +``` - More advanced traversal methods: up, BFS down, etc. - More expressive query types: multi-target, grouping, sorted etc. - With component as entities we unlock full `(entity, entity)` relationships \ No newline at end of file From bf948f849010c64de888e26d28a07bb43100b94e Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 19:55:44 -0700 Subject: [PATCH 09/19] Formatting --- rfcs/79-minimal-fragmenting-relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index db620aea..622e884d 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -119,7 +119,7 @@ After all the issues above are addressed the actual implementation of the featur - Data on relationship pairs is a trivial extension: ```rust world.entity_mut(source).insert_pair(Relationship {}, target); -``` + ``` - More advanced traversal methods: up, BFS down, etc. - More expressive query types: multi-target, grouping, sorted etc. - With component as entities we unlock full `(entity, entity)` relationships \ No newline at end of file From f66bab1db4512879237a70b82a32837ee47b3ac8 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 31 Mar 2024 19:56:41 -0700 Subject: [PATCH 10/19] Typo --- rfcs/79-minimal-fragmenting-relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 622e884d..46a4729d 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -57,7 +57,7 @@ for entity, targets in query.iter(&world) { } } -lt query = world.query>>(); +let query = world.query>>(); for entity in query.iter(&world) { // entity == alice } From 9ace384e55dd13519334886b24d82fb7d874baf5 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Mon, 1 Apr 2024 08:06:30 -0700 Subject: [PATCH 11/19] Slightly more detail --- rfcs/79-minimal-fragmenting-relationships.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 46a4729d..e1974cb1 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -2,9 +2,9 @@ ## Summary -The central idea behind fragmenting relationships is the ability to express a connection between two `Entity`s by adding a pair of `(ComponentId, Entity)` as if it were a component. For example if one had several entities representing different kinds of swords one could add the pair `(Equipped, sword_entity)` to the player to signify which is currently equipped. +The central idea behind fragmenting relationships is the ability to express a connection between two `Entity`s by adding a pair of `(ComponentId, Entity)` as if it were a component. For example if one had several entities representing different kinds of swords one could add the pair `(Equipped, sword_entity)` to the player to signify which is currently equipped. -Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to fragmenting relationships is the pair is a unique component. This means we could also add `(Equipped, shield_entity)` to the same player and offer query APIs that express `(Equipped, *)`to find all equipped pairs. +Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to "fragmenting" relationships is each unique pair is treated as if it were a unique component id, forming their own archetypes. This means we could also add `(Equipped, shield_entity)` to the same player and offer performant query APIs that express `(Equipped, *)`to find all equipped pairs. Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. ## Motivation @@ -107,7 +107,7 @@ After all the issues above are addressed the actual implementation of the featur - Transition to new `Identifier` implemented in [#9797](https://github.com/bevyengine/bevy/pull/9797) instead of `ComponentId` in all places where pairs could be expected: tables, archetypes etc. - New methods for `EntityCommands`, `EntityMut`/`Ref`/`WorldMut` and `QueryBuilder` ## Drawbacks -- Increased archetype fragmentation +- Increased archetype fragmentation when in use - Increase in internal and API complexity ## Prior art - Prior [RFC](https://github.com/BoxyUwU/rfcs/blob/min-relations/rfcs/min-relations.md) @@ -120,6 +120,7 @@ After all the issues above are addressed the actual implementation of the featur ```rust world.entity_mut(source).insert_pair(Relationship {}, target); ``` +- Clean-up policies to allow specifying recursive despawning, this is important to allow porting `bevy_hierarchy` to use relationships - More advanced traversal methods: up, BFS down, etc. - More expressive query types: multi-target, grouping, sorted etc. - With component as entities we unlock full `(entity, entity)` relationships \ No newline at end of file From 5c5056798d5641fd630d55b6ab0d7851a8b7d0d2 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 5 May 2024 16:01:44 -0700 Subject: [PATCH 12/19] Address a portion of feedback --- rfcs/79-minimal-fragmenting-relationships.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index e1974cb1..6c055785 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -75,7 +75,7 @@ query.single(&world); // == alice ## Implementation strategy -There are a number of technical blockers before even a minimal version of relationships could be implemented in bevy. I've broken them down below: +There are a number of technical blockers before even a minimal version of relationships could be implemented in bevy. These have been broken them down below: #### Archetype Cleanup In order to fit each pair into a single id we lose the entity generation (see this [article](https://ajmmertens.medium.com/doing-a-lot-with-a-little-ecs-identifiers-25a72bd2647) for more details). As a consequence when an entity is despawned we need to ensure that all the archetypes that included that entity as a target are also destroyed so that a future entity re-using that id is not misinterpreted as being the old target. This is non-trivial. @@ -85,22 +85,22 @@ Query's caches of matching tables and archetypes are updated each time the query Luckily observers [#10839](https://github.com/bevyengine/bevy/pull/10839) can help solve this problem. By sending ECS events each time an archetype is created and creating an observer for each query we can target the updates to query caches so that we no longer need to iterate every new archetype. Similarly we can fire archetype deletion events to cause queries to remove the archetype from their cache. -In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache to optimize for removal. However this is largely blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). +In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache. For example currently the collection of tables and archetypes is a vector but that doesn't provide an efficent method for removal without scanning the entiriety of the vector so this data structure will have to replaced with one that performs reasonably for both iteration and insertion/removal. However moving ECS structures into entities is largely blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). Similarly systems cache their `ArchetypeComponentId` accesses for the purposes of multithreading, we also need to update theses caches to remove deleted ids presenting many of the same problems. ##### Access Bitsets and Component SparseSets The `FixedBitSet` implementation used by `Access` inside of systems, system params, queries etc. relies on dense component ids to avoid using a large amount of memory. If we were to start inserting ids with arbitrarily large entity targets as well as component ids shifted into the upper bits each `FixedBitSet` would allocate non-trivial amounts of memory to have flags for all the intervening ids. In order to minimize the affects of this we could consider all `(R, *)` pairs as one id in accesses, reducing the amount we need to track at the expense of some theoretical ability to parallelise. -Similarly the `SparseSet` implementation used in both table and sparse component storage also operate under the assumption that component ids will remain low and would allocate an even larger amount of memory storing indexes for all the intervening ids when storing relationship pairs on an entity. +Similarly the `SparseSet` implementation used in both table and sparse component storage also operate under the assumption that component ids will remain low and would allocate an even larger amount of memory storing indexes for all the intervening ids when storing relationship pairs on an entity. In order to circumvent this we can implement a "component index", this index would track for each component the tables and archetypes it belongs to as well as the column in that table the component inhabits. This removes the need for the sparse lookup as well as providing opportunities for other optimisations related to creation of observers and queries. -The most practical way to solve this is using paged versions of the appropriate data structures, for bitsets this can take the form of existing hierarchical or compressed bitsets like `roaring`. ### Minimal Implementation Implementing the above in order of dependance could look like: - Resolve [#12144](https://github.com/bevyengine/bevy/issues/12144) so entities can be persisted in the render world. - Merge observers [#10839](https://github.com/bevyengine/bevy/pull/10839) - Refactor queries to become entities with caches updated via observers -- Implement archetype deletion and the associated clean-up for systems and queries +- Implement a component index to speed-up creation of queries/observers and alleviate the need for `SparseSet` in tables - Replace `FixedBitSet` and `SparseSet` implementations with ones that support sparse ids +- Implement archetype deletion and the associated clean-up for systems and queries After all the issues above are addressed the actual implementation of the feature can begin. This requires changes in 3 main areas: - Implementation of `WorldData` and `WorldFilter` implementations for accessing relationship targets From 94bb65ccaacdb428dd539aebd3dc45fc6f15ce68 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 5 May 2024 16:16:33 -0700 Subject: [PATCH 13/19] Address more feedback --- rfcs/79-minimal-fragmenting-relationships.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 6c055785..15dd80ed 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -4,12 +4,12 @@ The central idea behind fragmenting relationships is the ability to express a connection between two `Entity`s by adding a pair of `(ComponentId, Entity)` as if it were a component. For example if one had several entities representing different kinds of swords one could add the pair `(Equipped, sword_entity)` to the player to signify which is currently equipped. -Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to "fragmenting" relationships is each unique pair is treated as if it were a unique component id, forming their own archetypes. This means we could also add `(Equipped, shield_entity)` to the same player and offer performant query APIs that express `(Equipped, *)`to find all equipped pairs. +Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to "fragmenting" relationships is each unique pair is treated as if it were a unique component id, therefore they can appear in queries, use the existing insert and get APIs as well as form thier own archetypes. For example we could add `(Equipped, shield_entity)` to the same player and query for `(Equipped, *)`to find all equipped pairs. Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. ## Motivation -Fragmenting relationships has been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of fragmenting relationships. +Relationships has been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of relationships as first class ECS primitive. ## User-facing explanation Relationship pairs can be inserted and removed like any other component. There are 3 ids associated with any relationship pair instance: @@ -72,12 +72,13 @@ let query = QueryBuilder::new::(&mut World) query.single(&world); // == alice ``` +As relationship pairs are effectively regular components this doesn't require any changes to the more complex query iteration APIs such as `par_iter`. ## Implementation strategy -There are a number of technical blockers before even a minimal version of relationships could be implemented in bevy. These have been broken them down below: +There are a number of technical blockers before even a minimal version of relationships could be implemented in bevy. They're individually broken down below: #### Archetype Cleanup -In order to fit each pair into a single id we lose the entity generation (see this [article](https://ajmmertens.medium.com/doing-a-lot-with-a-little-ecs-identifiers-25a72bd2647) for more details). As a consequence when an entity is despawned we need to ensure that all the archetypes that included that entity as a target are also destroyed so that a future entity re-using that id is not misinterpreted as being the old target. This is non-trivial. +In order to fit each pair into a single `Identifier` we lose the entity generation (see this [article](https://ajmmertens.medium.com/doing-a-lot-with-a-little-ecs-identifiers-25a72bd2647) for more details). As a consequence when an entity is despawned we need to ensure that all the archetypes that included that entity as a target are also destroyed so that a future entity re-using that id is not misinterpreted as being the old target. This is non-trivial. `bevy_ecs` currently operates under the assumption that archetype and component ids are dense and strictly increasing. In order to break that invariant we need to address several issues: ##### Query and System Caches @@ -85,7 +86,9 @@ Query's caches of matching tables and archetypes are updated each time the query Luckily observers [#10839](https://github.com/bevyengine/bevy/pull/10839) can help solve this problem. By sending ECS events each time an archetype is created and creating an observer for each query we can target the updates to query caches so that we no longer need to iterate every new archetype. Similarly we can fire archetype deletion events to cause queries to remove the archetype from their cache. -In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache. For example currently the collection of tables and archetypes is a vector but that doesn't provide an efficent method for removal without scanning the entiriety of the vector so this data structure will have to replaced with one that performs reasonably for both iteration and insertion/removal. However moving ECS structures into entities is largely blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). +In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache. For example currently the collection of tables and archetypes is a vector but that doesn't provide an efficent method for removal without scanning the entiriety of the vector so this data structure will have to replaced with one that performs reasonably for both iteration and insertion/removal. `QueryState` would then be taken private in order to prevent unsafe accesses and users would refer to queries either by their type signature or entity id. + +However moving ECS structures into entities is largely blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). Similarly systems cache their `ArchetypeComponentId` accesses for the purposes of multithreading, we also need to update theses caches to remove deleted ids presenting many of the same problems. ##### Access Bitsets and Component SparseSets @@ -95,19 +98,18 @@ Similarly the `SparseSet` implementation used in both table and sparse component ### Minimal Implementation Implementing the above in order of dependance could look like: -- Resolve [#12144](https://github.com/bevyengine/bevy/issues/12144) so entities can be persisted in the render world. - Merge observers [#10839](https://github.com/bevyengine/bevy/pull/10839) +- Resolve [#12144](https://github.com/bevyengine/bevy/issues/12144) so entities can be persisted in the render world. - Refactor queries to become entities with caches updated via observers - Implement a component index to speed-up creation of queries/observers and alleviate the need for `SparseSet` in tables -- Replace `FixedBitSet` and `SparseSet` implementations with ones that support sparse ids -- Implement archetype deletion and the associated clean-up for systems and queries +- Implement archetype deletion and the associated clean-up for systems and queries, this is the only change that pays a complexity cost for no user benefit until relationships are realised After all the issues above are addressed the actual implementation of the feature can begin. This requires changes in 3 main areas: - Implementation of `WorldData` and `WorldFilter` implementations for accessing relationship targets - Transition to new `Identifier` implemented in [#9797](https://github.com/bevyengine/bevy/pull/9797) instead of `ComponentId` in all places where pairs could be expected: tables, archetypes etc. - New methods for `EntityCommands`, `EntityMut`/`Ref`/`WorldMut` and `QueryBuilder` ## Drawbacks -- Increased archetype fragmentation when in use +- Increased archetype fragmentation when used, this can have impacts on memory fragmentation and query iteration performance, however the impact is non-trivial and varies from case to case - Increase in internal and API complexity ## Prior art - Prior [RFC](https://github.com/BoxyUwU/rfcs/blob/min-relations/rfcs/min-relations.md) From f4fc39b8dc80721f1f71db2153f3b9572057cec6 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Fri, 10 May 2024 14:40:03 -0700 Subject: [PATCH 14/19] Fix typos Co-authored-by: Trashtalk217 --- rfcs/79-minimal-fragmenting-relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 15dd80ed..46c51646 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -86,7 +86,7 @@ Query's caches of matching tables and archetypes are updated each time the query Luckily observers [#10839](https://github.com/bevyengine/bevy/pull/10839) can help solve this problem. By sending ECS events each time an archetype is created and creating an observer for each query we can target the updates to query caches so that we no longer need to iterate every new archetype. Similarly we can fire archetype deletion events to cause queries to remove the archetype from their cache. -In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache. For example currently the collection of tables and archetypes is a vector but that doesn't provide an efficent method for removal without scanning the entiriety of the vector so this data structure will have to replaced with one that performs reasonably for both iteration and insertion/removal. `QueryState` would then be taken private in order to prevent unsafe accesses and users would refer to queries either by their type signature or entity id. +In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache. For example, currently the collection of tables and archetypes is a vector but that doesn't provide an efficient method for removal without scanning the entirety of the vector so this data structure will have to be replaced with one that performs reasonably for both iteration and insertion/removal. `QueryState` would then be taken private in order to prevent unsafe accesses and users would refer to queries either by their type signature or entity id. However moving ECS structures into entities is largely blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). From ab88b28b327d5d4befb64acc76aeb090b5079394 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Fri, 10 May 2024 14:40:14 -0700 Subject: [PATCH 15/19] More typos Co-authored-by: Trashtalk217 --- rfcs/79-minimal-fragmenting-relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 46c51646..91aa75bd 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -9,7 +9,7 @@ Of course you could also just have created an `Equipped` component with an `Enti Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. ## Motivation -Relationships has been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of relationships as first class ECS primitive. +Relationships have been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of relationships as first class ECS primitive. ## User-facing explanation Relationship pairs can be inserted and removed like any other component. There are 3 ids associated with any relationship pair instance: From 24ef42533de25d3bd031ffecf0e429a4c592fa74 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Fri, 10 May 2024 14:49:41 -0700 Subject: [PATCH 16/19] Re-arrange depencencies --- rfcs/79-minimal-fragmenting-relationships.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 91aa75bd..6e166f7e 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -97,11 +97,11 @@ The `FixedBitSet` implementation used by `Access` inside of systems, system para Similarly the `SparseSet` implementation used in both table and sparse component storage also operate under the assumption that component ids will remain low and would allocate an even larger amount of memory storing indexes for all the intervening ids when storing relationship pairs on an entity. In order to circumvent this we can implement a "component index", this index would track for each component the tables and archetypes it belongs to as well as the column in that table the component inhabits. This removes the need for the sparse lookup as well as providing opportunities for other optimisations related to creation of observers and queries. ### Minimal Implementation -Implementing the above in order of dependance could look like: +The component index could be implemented at any time, however the other pieces require some ordering to satisfy dependencies or prevent half implemented features. +Implementing the remainder in an appropriate order of dependance would work as follows: - Merge observers [#10839](https://github.com/bevyengine/bevy/pull/10839) - Resolve [#12144](https://github.com/bevyengine/bevy/issues/12144) so entities can be persisted in the render world. - Refactor queries to become entities with caches updated via observers -- Implement a component index to speed-up creation of queries/observers and alleviate the need for `SparseSet` in tables - Implement archetype deletion and the associated clean-up for systems and queries, this is the only change that pays a complexity cost for no user benefit until relationships are realised After all the issues above are addressed the actual implementation of the feature can begin. This requires changes in 3 main areas: @@ -125,4 +125,4 @@ After all the issues above are addressed the actual implementation of the featur - Clean-up policies to allow specifying recursive despawning, this is important to allow porting `bevy_hierarchy` to use relationships - More advanced traversal methods: up, BFS down, etc. - More expressive query types: multi-target, grouping, sorted etc. -- With component as entities we unlock full `(entity, entity)` relationships \ No newline at end of file +- With component as entities we unlock full `(entity, entity)` relationships From 62bed5df7b9db258a9630e607cebb6e900e19b45 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Fri, 10 May 2024 14:50:43 -0700 Subject: [PATCH 17/19] Typo --- rfcs/79-minimal-fragmenting-relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 6e166f7e..5bd84db4 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -9,7 +9,7 @@ Of course you could also just have created an `Equipped` component with an `Enti Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. ## Motivation -Relationships have been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of relationships as first class ECS primitive. +Relationships have been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of relationships as a first class ECS primitive. ## User-facing explanation Relationship pairs can be inserted and removed like any other component. There are 3 ids associated with any relationship pair instance: From 1a59055581235a6a13059198901051e58a4f53d4 Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Mon, 13 May 2024 08:19:54 -0700 Subject: [PATCH 18/19] Apply trivial suggestions Co-authored-by: Alice Cecile --- rfcs/79-minimal-fragmenting-relationships.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 5bd84db4..3e7a5dce 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -76,7 +76,7 @@ As relationship pairs are effectively regular components this doesn't require an ## Implementation strategy -There are a number of technical blockers before even a minimal version of relationships could be implemented in bevy. They're individually broken down below: +There are a number of technical blockers before even a minimal version of relationships could be implemented in Bevy. They're individually broken down below: #### Archetype Cleanup In order to fit each pair into a single `Identifier` we lose the entity generation (see this [article](https://ajmmertens.medium.com/doing-a-lot-with-a-little-ecs-identifiers-25a72bd2647) for more details). As a consequence when an entity is despawned we need to ensure that all the archetypes that included that entity as a target are also destroyed so that a future entity re-using that id is not misinterpreted as being the old target. This is non-trivial. @@ -116,7 +116,7 @@ After all the issues above are addressed the actual implementation of the featur - [flecs](https://github.com/SanderMertens/flecs/blob/master/docs/Relationships.md) - Partial draft implementation: [#9123](https://github.com/bevyengine/bevy/pull/9123) ## Unresolved questions -- How to address [#12144](https://github.com/bevyengine/bevy/issues/12144) +- How to address [the render world blocking ECS developments](https://github.com/bevyengine/bevy/issues/12144) ## Future possibilities - Data on relationship pairs is a trivial extension: ```rust From f8c2497c75e2ccba8cb6476828f97c2f5dc6ec4f Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Tue, 4 Jun 2024 14:22:05 -0700 Subject: [PATCH 19/19] Address feedback, add terminology. --- rfcs/79-minimal-fragmenting-relationships.md | 59 +++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/rfcs/79-minimal-fragmenting-relationships.md b/rfcs/79-minimal-fragmenting-relationships.md index 3e7a5dce..8ef9201d 100644 --- a/rfcs/79-minimal-fragmenting-relationships.md +++ b/rfcs/79-minimal-fragmenting-relationships.md @@ -1,23 +1,36 @@ # Feature Name: `minimal-fragmenting-relationships` ## Summary +Users often run into difficulty while trying to model entity hierarchies in Bevy. The tools we currently have are not expressive enough to meet their needs. See this issue for more discussion [#3742](https://github.com/bevyengine/bevy/issues/3742). -The central idea behind fragmenting relationships is the ability to express a connection between two `Entity`s by adding a pair of `(ComponentId, Entity)` as if it were a component. For example if one had several entities representing different kinds of swords one could add the pair `(Equipped, sword_entity)` to the player to signify which is currently equipped. +This RFC proposes a minimal design that would unblock a large amount of users while also leaving avenues for the engine to further expand these features. In short: +- Define a new variant of `Identifier` that combines a `ComponentId` (the relationship type) and an `Entity` (the target) that can be added to an entity (the source) to model a relationship of that type between the source and the target. +- Implement command, world and query APIs to support adding, removing and reading these new relationships. -Of course you could also just have created an `Equipped` component with an `Entity` field member to represent the same relationship. The advantage to "fragmenting" relationships is each unique pair is treated as if it were a unique component id, therefore they can appear in queries, use the existing insert and get APIs as well as form thier own archetypes. For example we could add `(Equipped, shield_entity)` to the same player and query for `(Equipped, *)`to find all equipped pairs. +A large amount of internal refactoring has to be done in order to support this API so a significant focus will be on eliminating these technical blockers. For example: +- Replacing internal `SparseSet` usage where `Identifier` would be used as keys as they are no longer dense. +- Allowing archetypes to be deleted when an entity that was used as part of a pair in that archetype is despawned. This involves updating several internal caches that are used for correctness and/or safety. -Another analogy when considering the ECS as an in memory database is that relationships act as foreign keys. This unlocks a potential future where we can express more complex queries matching across multiple entities and relationship pairs. There are a myriad of additional features that can be built on top of the initial implementation described below. The main focus in this RFC will be the blocking issues that must be addressed before the initial implementation is achievable. ## Motivation - -Relationships have been a long desired bevy feature. The ability to express and traverse hierarchies and graphs within the ECS unlocks a huge amount of potential. Features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries etc. all rely on or are simplified by the existence of relationships as a first class ECS primitive. +Creating, traversing and maintaining hierarchies in Bevy has been a long standing UX problem, it can result in large amounts of code with poor performance characteristics that users will often bend their designs in order to avoid. Users should be able to many types of relationships between two entities and we should offer the tools to manage those relationships in an ergonomic and performant way. + +There are many possible implementations of relationships in an ECS, some of which have been proposed for bevy before. The main factor separating them is how deeply integrated they are into the ECS itself. The central idea behind fragmenting relationships is the ability to express a connection between two `Entity`s by adding a combined id of `(ComponentId, Entity)` as if it were a single component id. This is advantageous compared to other designs as it makes it trivial to query for these relationships and thus expose advanced features such as breadth-first query traversal, immediate hierarchy clean-up, component inheritance, multi-target queries and more. + +## Terminology + +- **[Identifier](https://docs.rs/bevy_ecs/latest/bevy_ecs/identifier/struct.Identifier.html)**: A globally unique ECS id represented as a `u64`. +- **[Entity](https://docs.rs/bevy_ecs/latest/bevy_ecs/entity/struct.Entity.html)**: A variant of `Identifier` representing an entity. +- **Entity Id**: The lower 32 bits of an `Entity`, no two living entities should share the same id. +- **Generation**: The upper 32 bits of an `Entity`, represents how many times the id has been re-used, important to prevent interpreting a recycled `Entity` as the original. +- **Relationship**: A `Component` type used to model a relationship between two entities i.e. `Eats`, takes up the upper 32 bits of a `Pair` +- **Target**: An entity used as a target of a relationship, it's id takes up the lower 32 bits of a `Pair` +- **Pair**: A variant of `Identifier` representing a relationship to a target entity i.e. `(Eats, apple)` where `apple` is the target entity +- **Source**: The entity a `Pair` is added to as a component i.e. if `(Eats, apple)` is added to entity `alice`, `alice` would be the source of that relationship. +- **ZST**: Zero-sized type, these are types with `std::mem::size_of::() == 0` and hence carry no data. +- **Wildcard**: A special id used to match all any id, similar to a glob i.e. `(Eats, *)` would match all ids where the relationship type was `Eats` ## User-facing explanation -Relationship pairs can be inserted and removed like any other component. There are 3 ids associated with any relationship pair instance: -1. The source entity that the pair is being added to -2. The relationship component -3. The target entity - -In the minimal implementation all relationships will take the form of `(ZST Component, entity)` pairs so the API surface will look quite familiar. All naming is subject to bikeshedding: +Pairs can be inserted and removed like any other component. For simplicity the initial implementation will have all relationships take the form of `(ZST Component, entity)` pairs. All naming is subject to bikeshedding: ```rust #[derive(Component)] struct Relationship; @@ -31,7 +44,7 @@ command.entity(source_entity_a) .remove_pair::(target_entity_b); ``` -Since each pair is a unique component you can also add multiple target entities with the same relationship type, as well as use APIs to access and query for them: +Since each pair is a unique id you can also add multiple target entities with the same relationship type, as well as use APIs to access and query for them: ```rust #[derive(Component)] struct Eats; @@ -43,11 +56,13 @@ alice_mut.insert_pair::(apple) alice_mut.has_pair(apple); // == true +// Finds all (Eats, *) pairs and iterates the targets for target in alice_mut.targets() { // target == apple // target == banana } +// Matches all entities with at least one (Eats, *) pair and exposes the targets let query = world.query<(Entity, Targets)>(); for entity, targets in query.iter(&world) { // entity == alice @@ -66,14 +81,14 @@ for entity in query.iter(&world) { Dynamic queries would also be able to match on specific pairs: ```rust -let query = QueryBuilder::new::(&mut World) +let query = QueryBuilder::::new(&mut World) .with_pair::(apple) .build(); query.single(&world); // == alice ``` -As relationship pairs are effectively regular components this doesn't require any changes to the more complex query iteration APIs such as `par_iter`. +As relationship pairs are treated internally as regular components this doesn't require any changes to the more complex query iteration APIs such as `par_iter`. ## Implementation strategy There are a number of technical blockers before even a minimal version of relationships could be implemented in Bevy. They're individually broken down below: @@ -84,17 +99,17 @@ In order to fit each pair into a single `Identifier` we lose the entity generati ##### Query and System Caches Query's caches of matching tables and archetypes are updated each time the query is accessed by iterating through the archetypes that have been created since it was last accessed. As the number of archetypes in bevy is expected to stabilize over the runtime of a program this isn't currently an issue. However with the increased archetype fragmentation caused by fragmenting relationships and the new wrinkle of archetype deletion the updating of the query caches is poised to become a performance bottleneck. -Luckily observers [#10839](https://github.com/bevyengine/bevy/pull/10839) can help solve this problem. By sending ECS events each time an archetype is created and creating an observer for each query we can target the updates to query caches so that we no longer need to iterate every new archetype. Similarly we can fire archetype deletion events to cause queries to remove the archetype from their cache. +An appropriate solution would allow us to expose new/deleted archetypes to only the queries that may be interested in iterating them. Luckily observers [#10839](https://github.com/bevyengine/bevy/pull/10839) could cleanly model this pattern. By sending ECS triggers each time an archetype is created and creating an observer for each query we can target the updates so that we no longer need to iterate every new archetype. Similarly we can fire archetype deletion events to cause queries to remove the archetype from their cache. -In order to implement such a mechanism we need some way for the observer to have access to the query state in order to update it. This can be done by moving the query state into a component on the associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) and will require further refactors to the cache. For example, currently the collection of tables and archetypes is a vector but that doesn't provide an efficient method for removal without scanning the entirety of the vector so this data structure will have to be replaced with one that performs reasonably for both iteration and insertion/removal. `QueryState` would then be taken private in order to prevent unsafe accesses and users would refer to queries either by their type signature or entity id. +In order to implement such a mechanism we need some way for the observer to have access to the query state. Currently `QueryState` is stored in the system state, so in order to update it we would need to expose several additional APIs, even then it's quite difficult to reach into the system state tuple to access the correct query without some macros to handle tuple indexing. An alternative would be to move the query state into a component on an associated entity (WIP branch here: [queries-as-entities](https://github.com/james-j-obrien/bevy/tree/queries-as-entities)) this provides additional benefits in making it easier manage dynamic queries, but they are largely tangential to this RFC. -However moving ECS structures into entities is largely blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). +Regardless this will also require further refactors to the query cache implementation. For example, currently the collection of tables and archetypes is a vector but that doesn't provide an efficient method for removal without an O(n) scan, so this data structure will have to be replaced with one that performs reasonably for both iteration and insertion/removal. Similarly systems cache their `ArchetypeComponentId` accesses for the purposes of multithreading, we also need to update theses caches to remove deleted ids presenting many of the same problems. -Similarly systems cache their `ArchetypeComponentId` accesses for the purposes of multithreading, we also need to update theses caches to remove deleted ids presenting many of the same problems. +Using observers globally or moving queries to entities are both blocked by the deletion of entities in the render world tracked here: [#12144](https://github.com/bevyengine/bevy/issues/12144). ##### Access Bitsets and Component SparseSets -The `FixedBitSet` implementation used by `Access` inside of systems, system params, queries etc. relies on dense component ids to avoid using a large amount of memory. If we were to start inserting ids with arbitrarily large entity targets as well as component ids shifted into the upper bits each `FixedBitSet` would allocate non-trivial amounts of memory to have flags for all the intervening ids. In order to minimize the affects of this we could consider all `(R, *)` pairs as one id in accesses, reducing the amount we need to track at the expense of some theoretical ability to parallelise. +The `FixedBitSet` implementation used by `Access` inside of systems, system params, queries etc. relies on dense ids to avoid allocating a large amount of memory. Since pairs have component ids in the upper bits the `u64` they are represented as is extremely large, each `FixedBitSet` would allocate non-trivial amounts of memory to have flags for all the intervening ids. In order to minimize the affects of this we could consider all `(R, *)` pairs as one id in accesses, reducing the amount we need to track at the expense of some theoretical ability to parallelise. -Similarly the `SparseSet` implementation used in both table and sparse component storage also operate under the assumption that component ids will remain low and would allocate an even larger amount of memory storing indexes for all the intervening ids when storing relationship pairs on an entity. In order to circumvent this we can implement a "component index", this index would track for each component the tables and archetypes it belongs to as well as the column in that table the component inhabits. This removes the need for the sparse lookup as well as providing opportunities for other optimisations related to creation of observers and queries. +Similarly the `SparseSet` implementation used in both table and sparse component storage also operate under the assumption that ids will remain low and would allocate an even larger amount of memory storing pairs on an entity. In order to circumvent this we can implement a "component index", this index would track for each `Identifier` the tables and archetypes it belongs to as well as the column in that table the id inhabits (if any). This removes the need for the sparse lookup as well as providing opportunities for other optimizations related to creation of observers and queries. ### Minimal Implementation The component index could be implemented at any time, however the other pieces require some ordering to satisfy dependencies or prevent half implemented features. @@ -125,4 +140,6 @@ After all the issues above are addressed the actual implementation of the featur - Clean-up policies to allow specifying recursive despawning, this is important to allow porting `bevy_hierarchy` to use relationships - More advanced traversal methods: up, BFS down, etc. - More expressive query types: multi-target, grouping, sorted etc. -- With component as entities we unlock full `(entity, entity)` relationships +- With component as entities we unlock the ability `(entity, entity)` pairs, where either entity could also be a component. This makes it much easier to express dynamic relationships where you don't necessarily want to define your relationship type statically or go through the trouble of creating a dynamic component, as well as cases where you want a component as the target. + +There is also a hackmd that acts as a living design document and covers the implementation in more detail: https://hackmd.io/iNAekW8qRtqEW_BkcHDtgQ