-
Notifications
You must be signed in to change notification settings - Fork 136
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
Experimental refactor in legion_experiment crate #115
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sat down and read some of the changes at a high level and made minor comments about a couple of parts that tripped me up.
All the changes look good so far by the way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that the Or
macro matches the And
macro for how it coalesces while seeing if the new version made a fix for weird behavior when different levels of queries interact better.
It looks like there are some confusions around |
I have just pushed something I have been messing about with for some time. Packed ArchetypesThe largest change is the introduction of what I am calling "packed archetypes". Legion can now optimise the layout of its internal storage for specific access orders as defined by pre-defined component groupings. Component slices for each archetype may be packed in-order into a single memory allocation if the world determines it to be worthwhile. Packed archetype slices can shrink and grow in-place but, if they run into the next slice, they will be "unpacked" into their own vec. This is very similar to having to resize a vec as items are added. An archetype slice will only be considered a candidate for packing if the slice has remained stable (i.e. no entities added or removed) for a certain amount of time. I would expect this to include the vast majority of the entities in most typical game scenes. This decision is made per component type, not per archetype. Each component storage maintains a "fragmentation" heuristic, which is an estimate of the number of cache misses which would be avoided if the storage were re-packed, and this is used to determine if a component should be packed. Component groups are specified via a tupl;e of component types (e.g. The largest code change as a result of this feature is that all components of a specific type are now owned by a single component storage per world, rather than each archetype holding onto its own component storage. Archetypes are represented as slices of components whereby each index across all slices refer to the same entity, as they do now, but these slices are no longer divided into fixed size chunks. TagsI have not ported tags over to this architecture. It could be done, but I am not yet decided whether or not to drop tags as a feature. Legion#s internal design is very heavily inspired by Unity's ECS, and I initially found the example use cases Unity gave for their "shared components" to be quite compelling (and, indeed, those examples are still compelling). However, in using both legion and the Unity ECS I have found myself and my colleages very rarely using the feature. More importantly, one of the most commonly asked questions I get from users of legion is "should I use a component or a tag?", and my answer so have has, every time, been "use a component". I would like to hear more feedback on people's experience with legions tags before deciding on how to proceed here. PerformanceI have not done a lot of benchmarking of this branch so far, but here are the preliminary results:
This benchmark creates entities with 4 different component layouts, The experimental branch is 3 times as fast. However, there are a few things to note. The first is that the "ordered" benchmark (which defines an The second issues is that, upon further investigation, I do not believe that the performance difference between the two branches is actually caused by any of the major changes in this branch.... At its core, a legion query produces an iterator of chunks. Those chunks can then be iterated through to pull out each entity's components. The most "manual" way of doing this is via a nested loop:
On the experimental branch, this takes approximately 150ns in one of my benchmarks. However, users usually don't want to interact with chunks directly, and so we provide wrapper functions which will flatten this nested loop. On the experimental branch, this is simply implemented by wrapping
It would appear that Misc ChangesUniverse ShardingUniverses can be sharded via Entity IDs are no longer generationalWe no longer ever use an entity ID as an index, which means we don't need to try and limit the size of the address space via generations. Entity IDs are now defined as Query CachingCaching of query archetype layout matches is now mandatory and always happens. Chunk APIThe chunks returned from
|
@TomGillen I have been using "model handle" which refers to a unique asset as a tag in my code. The handle itself contains an id which is used to fetch the actual asset. This means I can do chunk iteration of transform data tagged by a model handle and do instance drawing per chunk which is really nice. Here is a short snippet showing it off: let mut offset_map = HashMap::new()
let query =
<(Read<Transform>, Tagged<ModelHandle>)>::query().filter();
for chunk in query.par_iter_chunks(world) {
// This is guaranteed to be the same for each chunk
let model = chunk.tag::<ModelHandle>().unwrap();
let offset = *offset_map.get(model).unwrap_or(&0);
let transforms = chunk.components::<Transform>().unwrap();
offset_map.insert(model.clone(), offset + transforms.len());
let model = asset_manager.get_model(model).unwrap();
render_pass
.draw_model_instanced(model, offset as u32..(offset + transforms.len()) as u32);
} How could I achieve something similar using components? If I would use a unique model handle type per asset I would have to have one query per unique model which is certainly not maintainable in the long run. |
Tags are like custom primary keys, which is very useful for modelling data layout. Recommended usage is probably approximately similar to primary keys in RDMSs, except point lookups are obviously not fast. I don't really like the idea of the storage system doing things like repacking automatically based on heuristics. But my understanding is that the grouping feature is opt-in? |
I'm building a general purpose game engine with legion and I do not use tags anywhere. I agree that being able to iterate over "chunks" having the same asset id tags seems powerful on paper, but in practice i don't just care about iterating over sets of entities that use the same model/texture pair. I also care about entity position in the world so I can draw back-to-front or front-to-back. This means that I'm accessing entities randomly anyway. I also consider tags to be confusing for users. I abstract tags out entirely in my "WorldBuilder" api because i believe they negatively affect legibility and introduce cognitive load . I'm sure theres a use case for tags, I just haven't personally encountered it. Ideally tags would be optional / only a part of the api when you need them. |
Yes. Repacking only happens if you call |
It would be great to be able to use Entity(u64) as a stable id for serialization / networking. Currently that isn't possible because entity id allocation is fully abstracted out from the user. It seems like this would be possible with the following changes:
Of course users could always define their own EntityId component, but that seems a bit redundant given that the definition of Entity is "unique identifier of entity in world" and the definition of EntityId would also be "unique identifier of entity in world". It would also mean that I would need to build a higher level wrapper over entity insertion to ensure that all entities have the EntityId component. |
Last message on the subject because I dont want to spam you all with potentially off topic information. I just created a "GuidEntityAllocator" in my legion fork that generates a new random id for each entity and allows "queuing up" new Entity ids. Nothing appears to be broken in my relatively complex project and I was able to write a serde implementation that preserves entity ids. It works well enough for my use cases that I will probably roll with it regardless of the choices made here, but it would be nice to support this in upstream legion. |
And does the |
Functionally it might, but clone_from and merge_from both take the source world by mutable reference. Probably because it's sharing the codepath with Move. Merger::merge_archetype passes the source world along to those impls via mutable reference. |
Yea, it might make more sense to separate the two further to allow duplicate to only require |
I think it will but it's hard to say without trying it. It did feel like there was a case for both a Merger and MergerMut but it might increase the code required by quite a bit. For what it's worth, I'm not planning to use either the Move or Duplicate impls of Merger. Similar to serialization we already have a registry for other reasons (like hooks for serde_diff and atelier assets). The prefab crate provides its own interfaces for downstream code to specify how to clone/transform data. The Merger trait looks like it could do the job but I'd like to implement it to make certain. :) |
@aclysma and @kabergstrom I have reworked how entity IDs, serialization and merging work in response to your feedback. It all makes more sense when you see how it all fits together, but I'll try and explain: Entitites:
Canon Names:
Serialization:
I have also made a few miscellaneous changes: fixed a few types not being exported, separated |
JSON (human readable) serialization format now looks like: {
"entities": {
"14ab8211-4a7c-4817-813b-8acf282c27de": {
"bool": false,
"isize": 4,
"usize": 4
},
"614a1c19-3718-4d30-b249-04d5c45df547": {
"entity_ref": "d6be1905-3714-40be-8226-60f3177ca395",
"isize": 6,
"usize": 6
},
"9d51f554-92c5-446b-b764-5892be5dd14a": {
"bool": false,
"isize": 3,
"usize": 3
},
"c8ca7a5c-834b-4f7e-a359-68a3a3779ed7": {
"entity_ref": "d6be1905-3714-40be-8226-60f3177ca395",
"isize": 7,
"usize": 7
},
"d3ef5175-d430-4f15-ab16-9b6082bfeefd": {
"bool": false,
"isize": 2,
"usize": 2
},
"d6be1905-3714-40be-8226-60f3177ca395": {
"bool": false,
"isize": 1,
"usize": 1
},
"ea27971a-9c71-41a6-babb-4c1a47418c77": {
"entity_ref": "d6be1905-3714-40be-8226-60f3177ca395",
"isize": 8,
"usize": 8
},
"fa5f9c08-a308-4555-a7e5-11121d2e499f": {
"entity_ref": "d6be1905-3714-40be-8226-60f3177ca395",
"isize": 5,
"usize": 5
}
}
} Where "isize", "usize" "bool" and "entity_ref" are the external names registered with the |
Json serializer/deserializer seems to work good, however, bincode deserializer doesn't seem to deserialize very well. // JSON
let serialized = serde_json::to_value(&world.as_serializable(any(), ®istry)).unwrap();
println!("{:#}", serialized.to_string());
let universe = Universe::new();
let world: World = registry
.as_deserialize(&universe)
.deserialize( &mut serde_json::Deserializer::from_str(&serialized.to_string())).unwrap();
println!("Number of entities in deserialized world {}\n\n", world.len());
// BINCODE
let serialized = bincode::serialize(&world.as_serializable(any(), ®istry)).unwrap();
println!("{:?}", serialized);
let universe = Universe::new();
let world: World = registry
.as_deserialize(&universe)
.deserialize( &mut bincode::Deserializer::from_slice(&serialized, bincode::DefaultOptions::default())).unwrap();
println!("Number of entities in deserialized world {}", world.len()) Any ideas why this is? |
@TimonPost Your bincode example won't deserialize correctly because you serialized with You need to use: bincode::DefaultOptions::new()
.with_fixint_encoding()
.allow_trailing_bytes() |
I again came across weird behavior in my bincode serializing code. When a type id is not known it will use This is done in the following two places: https://github.com/TomGillen/legion/pull/115/files#diff-fb7fb3d74cc05a5c59858d2d6b136bd8R249 While the legion logic is correct logic there is some underlying issue considering the nature of I use two separate binaries who use the same registered types and same logic for constructing the registry with the types but somehow the I am not sure if this is something to care about. But if legion is used across multiple binaries/network using Related issues to the Type id problem |
@TimonPost SerializableTypeId has this comment: /// A default serializable type ID. This ID is not stable between compiles, I think it's a bad default to include in the project though, since it's more or less useless for what you'd expect it to be. As for |
I recommend using https://docs.rs/type-uuid/0.1.2/type_uuid/ for your serialized type ID |
I am going to merge this PR. Further feedback should be better served with more focused and specific issues and PRs. I am going to give it a bit more time for us to discover and work through issues with this rewrite before publishing 0.3 to crates.io. |
This started as a simple refactor of legion into smaller sub-modules, as some of the module files were well over 1k LoC and were getting unmaintainable. However, while moving things around, I found some soundness issues and saw some opportunity to simplify things. Those in turn caused more changes downstream, and the simplifications made other improvements that I had been planning fairly trivial to implement. This ended up being a much larger and more impactful refactor of the entire legion_core crate than I had originally planned. This work is not yet complete, but it is wide reaching enough that I want to give everyone the opportunity to see what I've been doing and collect some feedback.
The general changes are:
unsafe
.Changes to
World
(which now more closely resembles a std collection):len
to retrieve entity count.insert
toextend
.push
as sugar for inserting a single entity.delete
toremove
.is_alive
tocontains
.entity(Entity) -> Entry
andentity_mut(Entity) -> EntryMut
functions. Entries contain the APIs for interacting with a single entity. e.g.get_component
,add_component
andlayout
(which in turn offershas_component
etc).iter
anditer_mut
for iterating through all entity entries.Changes to storage:
LayoutIndex
..search(EntityFilter) -> impl Iterator<Item = ArchetypeIndex>
function.[Archetype]
and can be indexed byArchetypeIndex
.Changes to filters:
It is likely that
SystemQuery
won't be needed anymore, too.I don't forsee any more major API changes beyond completing this rework, and legion v1.0.0 will likely closely resemble this, after which things should stabilise and breaking changes will hopefully be rare.