-
Notifications
You must be signed in to change notification settings - Fork 135
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
Feedback from the merged experimental branch #169
Comments
Looking just at that RON file, it looks like it is the component itself which is failing to deserialize (trying to read the tuple as an array). However, I don't see how that would behave any differently based upon the format of the surrounding world data, so this is unlikely to be the case. Do you have a link to the code somewhere?
You need to do this when deserializing, because at some point you need to delegate to the component's As for merging, you should not need to do any temp copies. The
It is difficult to determine why you would need to do this without seeing what you are trying to do.
The previous API didn't need to ensure that the same entity in the serialized data will always be assigned the same runtime ID because it did not perform ID rewriting in components to fix up ID references. We could allow you to remove ID mappings from the I am not at all opposed to allowing you to tell the universe to "forget" an ID mapping if you somehow know that you no longer need runtime ID stability for that entity. However, this issue is not caused by the new serialization code, but rather by the new entity reference rewriting feature; a feature which was needed and cannot be performed anywhere but inside deserialize/clone.
...I'm not sure why it isn't already actually.
Yes, but I don't think the feature justified itself. The use cases were niche, new users found the distinction between components and tags confusing, virtually every time I saw someone using it they were using them wrong (to the point that it would significantly harm performance), and tags significantly increased the complexity of much of legion's code. I am not ruling out the possibility of re-implementing tags (or something similar), but I think it needs more thought before we would go ahead with that. |
An option is to allow the user to provide a trait implementation for ID lookups in all relevant operations (clone/merge/serialize/deserialize), and set this up in a TLS var to robustly support the ID patching. |
When deserializing, either the serialized entity ID has already been assigned a runtime ID (in which case you want to use that), or it hasn't (in which case you want the world to assign a new runtime ID). There aren't any other ways to correctly handle this. For merging, there are a few reasons why you might want to control how IDs are assigned and re-mapped. The /// Indicates how the merger wishes entity IDs to be adjusted while cloning a world.
fn entity_map(&mut self) -> EntityRewrite {
EntityRewrite::default()
} Where /// Describes how a merger wishes `Entity` references inside cloned components to be
/// rewritten.
pub enum EntityRewrite {
/// Replace references to entities which have been cloned with the ID of their clone.
/// May also provide a map of additional IDs to replace.
Auto(Option<HashMap<Entity, Entity, EntityHasher>>),
/// Replace IDs according to the given map.
Explicit(HashMap<Entity, Entity, EntityHasher>),
} I don't think |
The primary goal is to ensure that the functionality mentioned here still works: https://community.amethyst.rs/t/atelier-legion-integration-demo/1352/6 These crates are compiling as long as World::universe() is made public. And I have that temporary fix mentioned above to force legion to always use the human-readable serialization path.
I can see that the previous code was putting a T on the stack, which I didn't realize. However there is still an extra heap allocation here due to the Vec. (NOTE TO SELF: deserialize_single_fn has a Vec alloc that may be avoidable) Old:
Comparing the old/new version, I think the old way would avoid copies and the new one might require them. (To be fair, both implementations somewhat require LLVM to do smart things, but the new implementation is an explicit memcpy and not passing ownership of a temporary.) New implementation is based on legion's implementation:
In the first example above (deserialize_comp_fn) I was calling extend_memcopy_raw passing all components being deserialized as a slice. I can probably change that to call extend_memcopy_raw N times passing a single component rather than once passing a slice of all deserialized components. So this may be avoidable.
While I appreciate that you are trying to solve some of these problems for us, I explicitly DON'T want to rely on legion's behavior for a lot of this for several reasons:
What I want out of legion is an unopinionated data store. I think "push everything upstream into legion" is unsustainable because the more high-level logic that legion provides, the more difficult it will be to keep everyone using it happy.
I'm not convinced that this is something legion should be doing. Ideally, I think Entity would be non-serializable and a new type (which I would implement in the prefab crate) could do this using Entity lookups stashed in TLS. This would mean any component that meant to be serializable must use that particular type to reference the Entity. That's fine as I already support separate design-time and run-time representations. In fact this is a desirable feature as entities in a prefab shouldn't reference entities outside their own prefab. This avoids the continuously growing UUID/Entity map and doesn't require locks. Legion as you said is not aware of lifetimes for these mappings to be able to clean them up when they are no longer needed, but my code definitely is and I've been managing them fine in legion 0.2.4. |
You really shouldn't need to allocate a /// Describes a type which knows how to deserialize the components in a world.
pub trait WorldDeserializer: CanonSource {
/// The stable type ID used to identify each component type in the serialized data.
type TypeId: for<'de> Deserialize<'de>;
/// Converts the serialized type ID into a runtime component type ID.
fn unmap_id(&self, type_id: &Self::TypeId) -> Result<ComponentTypeId, UnknownType>;
/// Adds the specified component to the given entity layout.
fn register_component(&self, type_id: Self::TypeId, layout: &mut EntityLayout);
/// Deserializes a component and inserts it into the given storage.
fn deserialize_insert_component<'de, D: Deserializer<'de>>(
&self,
type_id: ComponentTypeId,
storage: &mut dyn UnknownComponentStorage,
arch_index: ArchetypeIndex,
deserializer: D,
) -> Result<(), D::Error>;
/// Deserializes a single component and returns it as a boxed u8 slice.
fn deserialize_component<'de, D: Deserializer<'de>>(
&self,
type_id: ComponentTypeId,
deserializer: D,
) -> Result<Box<[u8]>, D::Error>;
} It has two deserialize fns her because the "packed" and "entities" layouts are different enough that it would perform poorly if I tried to use just one fn for both. However, both of these expect just one component to be deserialized here. The implementation internally uses a custom seq visitor to move the component directly into the world without an intermediate allocation (which is as fast as we will ever get with how serde works). I don't understand how you could get anything working if you are trying to run that loop yourself. This is certainly why the non human readable format isn't working for you.
Being able to serialize a world is core functionality that virtually all users of the library are going to need. The problem is that while the old traits were suitable for your use case (they were designed specifically for your use-case), they were totally useless for users directly (requiring hundreds of lines of fairly complex code), which nessessitates that another crate be written to provide the "actual" API. If both of us write our own serialization logic for legion, then we will end up fragmenting both the ecosystem and developer effort writing two crates which significantly overlap in functionality.
That might work in your prefab system, but most people using legion might not be using
This was required when I had a stricter canon name concept, but I removed that a while back and now only use UUIDs as a stable offline ID and |
Due to erased_serde and "vtable" requirements in the component registrations, there is an indirect call to invoke the
The old traits were intended to be largely unopinionated and be something to build other crates on top of, indeed by design. An issue I see with building the UUID-as-entity-ID choice into the core serde support is that UUIDs can be inappropriate for use-cases where serialized size is important. This is why I delegated this choice to the user - to allow applications like real-time networking libraries to be more optimized for their use-case (networking will never be amazing due to serde design). I think providing an out-of-the-box solution is great though, and I applaud using UUIDs as the default.
I agree that we should avoid fragmentation.
I think Entity should be serializable, but I would like the API to be less opinionated about providing an exact set of operations, and just give me a callback with all the data available. As an example of something weird: in atelier-assets I use the
Sorry I haven't been keeping up with the recent commits. Feel free to poke when you have pushed important changes. |
I suppose I could do that, but the cost difference between a fn call and a virtual fn call, when considering that the code being called is deserialization code which is typically also going to be invoking IO, is rather inconsequential. The more significant gain would be that the implementation is likely to perform a hashmap lookup, which could be moved out of the loop. Although, even then, given that The trade off for moving this loop into user code is that the user needs to write their own seq visitor to avoid a temp vec allocation.
This could perhaps be made configurable, but it would require the |
I think this is an outdated sentiment. IO is much faster than compute right now, and IO throughput is doubling every 2 years or so. Consumer NVMe PCIe 4 SSDs provide > 5GBps, datacenters commonly have >40Gbps on a single VM, trending towards 100Gbps. Kernel APIs are being improved to reduce IO overhead to near-zero (DMA into user-space for aligned buffers). This is a decision that can mean the difference between an optimized hot loop that can deserialize more than 1 million entities per second, or an indirect call within it that can increase the fixed overhead which could limit throughput to less than 100k entities per second. It's a small change in the API with potentially large impact on throughput. Why bother have a packed layout if the data is not accessible to the user in a contiguous manner?
Deserializing a simple component with float/matrix fields basically boil down to memcopy from the serializer input stream with bincode. Bincode can easily do more than 300MB/s of deserialization for even complex structs. A hashmap lookup would take a significant percentage of the execution time.
It's a pain, yes, but aren't you already providing an implementation of WorldDeserializer for Registry to cover the out-of-the-box use-case?
Since this could be a rather uncommon use-case, it's OK to hide this behind a feature flag. It can probably be made into a strictly additive feature. It will be rare to do in a production game, mostly in editor or tool code. |
I'd still like to get the prefab crate updated to work with the changes, but work is busier at the moment and I have less time available in the short term to work on it. All the work so far has been pushed into branches linked in my earlier comments. The main issue now is that the Entity/UUID mapping code needs to be un-removed. |
I am currently part way through rearranging the serialization code. I have managed to pull everything but the If you need to do something totally different, then, it should be easy to build an entirely separate serialization impl. I do still hope that what is provided by legion should be capable of everything you need, though - but if you are worried about issues with evolving requirements or divergence between legion and legion-prefab, then there should always be this escape hatch available. |
So the core library only provides hooks to choose how to serialize The Bypassing |
@aclysma @kabergstrom Feeback in this? Legion should be even less opinionated on serialization than 0.2.3 was. |
@TomGillen That sounds really good - is |
Yea, I just pushed up some progress on this. The serialization module hasn't been moved into a subcrate yet, but the prep work for that has been done. I also changed the world serialization traits to move the component loop into the trait impl as you suggested. |
Sounds great, looking forward to taking a look soon! |
Very promising so far:
But again, so far this is looking like a huge improvement. Not needing to pass around a universe and not needing to worry if an entity has been initialized yet in canon is really great! |
Oh, yes, it certainly will. I need to do a proper review of what is exported as there are likely a few things that are not part of the main user-facing API but do need to be exported for those going a bit deeper.
That lifetime parameter is the lifetime of the reference to the |
Hey @TomGillen I had a stab at making our prefab stuff work, and had to change a little. Could you have a look? Mainly it was about making the traits a bit more flexible, with fewer strict types like |
Using @kabergstrom's changes I think I'm very close to having minimum up and running using the update prefab crate with legion 0.3. Hoping it will be good to go by end of weekend. |
@kabergstrom I'll take a look at this later tonight. I never liked the rather horrible |
Good news, I still need to implement the serialize/deserialize slice, but I was able to load a prefab, modify it, undo, redo, play, reset, save, and hot reload. There is cleanup to do for sure, but it's almost done! |
I think everything is working now in the prefab crate and minimum. (there's a decent amount of functionality that is currently only exercised in minimum right now).
Compiling branches: |
@kabergstrom Do you think you could open a PR with your serialization changes? |
@TomGillen You mean without @aclysma 's change in his branch here: https://github.com/aclysma/legion/tree/legion-0.3-prefab-support ? |
Ah, no we'll want that too, but that is a trivial change. |
Done @TomGillen #180 |
This is a continuation of feedback given in #115 before it was merged. Some of this is new and some of it I'm just bringing over from the PR and previous discord chats so it's all in one place.
The main issue currently is that deserializing with the non-human-readable path fails.
I wasn't able to sufficiently test clone_merge yet. It was time-consuming to update my code to compile and took a while to figure out the serialization issue.
Other issues:
The text was updated successfully, but these errors were encountered: