[Iris] Why are NetSerializers so complex?

Is there work being done to streamline NetSerializers? They are…a bit much, when compared to the old NetSerialize function. Which i realize have their own problems, but you go to implement a NetSerializer and it’s just so much, and its generating a lot of questions for me that the docs and comments haven’t cleared up for me, so hoping to gain some clarity:

For one of my cases the struct looks like this,

`struct FStructWithValueAndDelegate
{
UPROPERTY(EditAnywhere)
int8 Value = 0;

UPROPERTY(BlueprintAssignable)
FChangedEvent OnValueChanged;
};`The old NetSerialize, just made sure I only worried about serializing the Value, and on deserialize would call OnValueChanged. Pretty simple.

I implemented the Serialize and Deserialize, that was pretty painless, and was about to be done, but then it tells me - no sir buddy, you have to implement Quantize/Dequantize, what for?? Why isn’t serialize and deserialize the quantization I want or need? I’d like to understand this, because right now I’m just doing the same work, again, but differently and i don’t understand the benefit, all i see are bug vectors.

Then I have to implement IsEqual, not sure i get why.

Then I keep reading code and Find there’s an Apply I can implement, which says:

Serializers that want to be selective about which members to modify in the target instance when applying state should implement Apply where the serializer is responsible for setting the members of the target instance. The function operates on non-quantized state.

What? I implement Serialize and Deserialize, does the network layer some how stomp the non-serialized data in the struct if i do not implement Apply? I guess I don’t understand what this accomplishes and would like some clarity.

DynamicState - what is it? how do i create it? Why would I create it? I see a lot of cloning and freeing code, but really i don’t understand what kinda dynamic state they’re preserving.

The FNetSerializerRegistryDelegates, this guy is trying to make registration easy, but its mostly just confusing. Like I feeeeel like this could be a lot more streamlined, or even automatic, especially if yall changed the NetSerializers to be UObjects, and just have a virtual to get their SourceType or something.

One of the features I was surprised Iris hasn’t fixed from the legacy system is no TMap/TSet replication. With the range of power the NetSerializers give you, im surprised this one isn’t a thing yet. Is it on the roadmap?

Do the fragment descriptors for serializing structures - do they abide by the UPROPERTY(Getter=X,Setter=Y) functions, or do they only directly write to the underlying property.

Two observation from my two uses of custom NetSerializers thus far are as follows,

1) I’ve been required to not create one, but add types linked to the Ability system to the ‘SupportsStructNetSerializerList’ set. This one is annoying, but I get it. The fact that the original Ability targeting data required all subclasses to implement the NetSerializer really caused an issue for conversion.

2) The two main cases I’ve had for custom NetSerializers came down to mostly just needing to know when net replication happened inside a struct, similar to needing it for OnReps for UObjects. Any chance yall could just implement OnRep calling generated code for structures instead? I mean it would be a hellofa boon to not need to implement custom serialization all so that I could know when some property on a structure changes during replication.

Hi,

Serializers are a bit more complicated especially for structs for mainly good reasons. We have separated certain steps in order to minimize CPU time during serialization thanks to the quantization step and also to support delta compression. Serialize/deserialize can really only deal with actual serialization and not game logic. Those functions work on the quantized representation of the data and not the source form of the data. Quantize/Dequantize are also not working on the source/target data. So dequantize will overwrite any property that is replicated. In your case it’s bad for two reasons- you didn’t mark the delegate as NotReplicated and you actually want to call the delegate. I think it’s a good idea to still mark the delegate as NotReplicated but most importantly you should implement Apply so that you can compare the values prior to overwriting the target value and optionally call your delegate. Apply is called at the same time we’re copying the other received replicated data to their respective target so we don’t pollute the instruction cache by calling game logic during deserialization.

IsEqual is used for both delta compression reasons but another reason is the ability to use a custom equality method. In your case because the entire struct is replicated (not having marked the delegate as NotReplicated) equality will happen via the structs Identical function which iterates over all properties and see if they are equal or not. So changing the delegate would force the struct to be considered changed and be replicated. I recommend marking your delegate as NotReplicated but you can also force your serializer’s IsEqual function to be called by setting

constexpr bool bUseSerializerIsEqual = true;true in your NetSerializer.

The dynamic state things are for the case when you have something in the quantized state requiring memory allocations. For baseline duplication we need to deep copy that data and for baseline freeing we obviously need to free the allocations as well. CollectNetReferences is so that we can call the appropriate OnReps when a previously no longer existing object starts replicating and is yet again instantiated and resolvable. You don’t need to implement this for your struct.

TMap/TSet. We’re not going to implement it in Iris unless it’s also implemented it for non-Iris. Iris does not yet support replay recording/playback so the same types need to be supported by both in order to not break that. It’s trivial to implement if just serializing the entire TMap or TSet but if you want to be smart about it similar to how FastArraySerializer tries to minimize what elements it replicates it’s quite complex. I’ve seen people wanting this and simply wrap a TMap or TSet in a struct and implement custom serialization, generating arrays for keys and values and then serializing them. Another way is using arrays, sort them when needed and use binary search algorithms to find the element. If coupled with a FastArray you also get minimal replication.

If you’re using the struct with delegate pattern a lot it does sound like you want to request a feature to have something similar to an OnRep on the struct to avoid all the hassle.

Generally speaking we would like for people to not having to implement serializer at all and cover the most common use cases. For example Iris does not require structs derived from a struct with custom serializer to also implement a custom serializer. With the NetSerialize method/trait not only do you need to know if the base implements a custom serializer you also need to implement it for all derived types. The SupportsStructNetSerializerList is there so you can ideally just marking properties as replicated or not and not have to implement a custom serializer unless it’s needed. It’s definitely good for a few of those existing use cases which forced you to implement a NetSerialize method even if you just added a couple of properties.

Cheers,

Peter

Getters/setters are for blueprints only. We’re not calling them but instead rely on the engine property reflection to get/set raw values.

Server

1) Yes, we have a shadow copy of the source state too to determine what changed. Typically via the property Identical as mentioned

2) Quantize reads from the shadow copy of the source data

3) Yes, it can only access the quantized form of the data (regardless of whether that representation is the same as the source data or not)

Client

1) Yes.

2) Yes

3) Yes, into a temporary instance of the source data form. It’s still not operating on the game target data!

4) Yes. From my perspective I only ever required a few serializers to implement Apply. I do have a couple of cases where I needed to implement a custom fragment instead to avoid doing very expensive temporary copies of arrays… If you have a lot of structs with the delegate then I’m afraid Apply is going to be a common for you to implement.

FGameplayCueParameters. Yes it looked good enough from my perspective to avoid implementing a NetSerializer. One big caveat with custom serializers is that because UE reflection doesn’t provide a hash or anything one must manually detect via custom code if a struct has changed and the serializer needs revision. So not implementing one if one can avoid it can be a big maintenance win.

Cheers,

Peter

OK! This is great Peter, the support staff should definitely turn all this into a Knowledge article at some point.

For default serializing - do they abide by the UPROPERTY(Getter=X,Setter=Y) functions? Depending on what FProperty functions you guys call to set/get the data they might.

Server

?) Before Quantize, does iris make/have a shadow copy of the data to determine if it has changed?

2) Iris/Custom NetSerializer Quantizes the data, when we Dequantize later, we’re writing to a shadow copy of the structure type. When we quantize, are we reading the real structure, or are we reading from the shadow copy?

3) Iris/Custom NetSerializer Serializes the quantized data onto a buffer for the connection

Client

1) There is a shadow copy here as well of the target data, yes?

2) Iris/Custom NetSerializer Deserialize the data, from network stream to quantized type buffer.

3) Iris/Custom NetSerializer Dequantizes the data, from the quantized packed form into the source type.

4) Iris/Custom NetSerializer APPLY the data, if your net serializer fails to implement Apply, your data will copied across the struct from the shadow copy, but only the replicated UProperties correct?

With Iris’s Shadow copy, do you only copy/compare serializable properties by default? e.g. if i have non-uproperties on the structure are they in danger? I absolutely assume my non-uproperties are not in any danger, but I figured better to ask.

The MC delegate originally wasn’t marked as NotReplicated because well, I had a custom NetSerialize function, but also, DynamicDelegates are not network serializable, or at least weren’t, so it was always implied. But like most things with Iris, I suppose I need to be more explicit?

> Another way is using arrays, sort them when needed and use binary search algorithms to find the element. If coupled with a FastArray you also get minimal replication.

Yeah - but without the ability to nest FastArrays inside structures it really limits the ability to make reuseable structures/patterns.

> SupportsStructNetSerializerList

I’m using this for a few things, mostly related to the ability system like base engine. I noticed yall decided not to implement a custom net serializer for FGameplayCueParameters, it does a bunch of work internally to only replicate minimal state compared to the default values of each property.

Are you not bothering because that is now a standard capability of the default iris structure serializer? I found some comment in the code that seems to indicate something along those lines at the cost of 1 bit per property?

> 2) Quantize reads from the shadow copy of the source data

How are you making the shadow copy? I’ve got another structure that needs to access Non-replicated (possibly Non-UProperty) data, depending on how you make the shadow copy it might not be able to access the correct data to replicate the correct state.

Hi,

It should be a full copy if you have custom serialization.

Cheers