Add a "Composite" bool to [GhostComponent] that sets one change bit for an entire component

In [GhostField] there exists an option “Composite” that, when set to true, generates a single change bit for an entire struct. For example a float4x4 would normally require 16 change bits, one for each float value, but with composite on, only a single change bit would be written for all four floats.

This would mean that where normally if a float in that float4x4 would change, only the float would be serialized and sent to the client for changing. When composite is true, all sixteen floats would be sent to the client as it was recorded that all floats changed, instead of a single value.

While this seems costly, for rarely changing structs containing large numbers of values on a dynamic object, a single bit instead of perhaps 16 bits every tick, which adds up quickly.

But this option is only on sub-structs within replicated components. If I had a very rarely changing but significant number of fields in a replicated component, I would need to make a wrapper struct for the component whose field has composite enabled.

Alternatively (and what i’ve been doing) is aliasing the individual fields using explicit struct layout to overlap on to existing structs (e.g., uint4) that are then a ghost field with a composite tag enabled. The components in which I do this “hack” are typically bit compressed and require post processing to use so the loss of Burst vectorization is not an issue.

But this can apply in other cases as well where explicit struct layout is unacceptable given the performance loss. In which case, i would have to generate redundant wrapper structs. Not much of a loss, given that NativeArrays have .Reinterpret<>() that removes wrappers but still very annoying.

An example taken directly from my current code below. See that the ulong located on FieldOffset(0) is aliased by the actual lighting data. If I didn’t designate the replicated ulong as the replicated data, and using default ghost field options, I would need 7 bits to indicate a change in this very rarely changing component. Instead I have 1.

/// * Combined property values for a single bit change flag.
[GhostField] [FieldOffset(0)]
public ulong _netCode;

/// * Primary light radius.
[FieldOffset(0)] private half _lightRad;

/// * Low 4 bits: Soft halo radius. High 4 bits: Soft halo count.
[FieldOffset(2)] private byte _softData;

/// * Low 5 bits: Outer angle. High 3 bits: Inner angle, normalized to outer.
[FieldOffset(3)] public byte ConeData;

/// * Light color. Note additive blending. Intensity is thus alpha.
[FieldOffset(4)] public Color32 Color;

This should not be needed if there’s an option on GhostComponent itself indicating that only a single bit should be used to indicate a change within the component for replication.

Basically, I am requesting a Composite option in GhostComponent itself that applies to all fields within it.

It would also be very nice in applications where variants can use a composite version of the networked struct like LocalTransform. If I had a dynamic entity that had components regularly changing but the actual transform very rarely updated. That way, the 3 change bits default on LocalTransform would be reduced to 1. I don’t know if the Explicit struct trick works for variants though, haven’t tried.

2 Likes

Hey Kmsxkuse! I totally agree with this suggestion. Backlogged, thanks!

2 Likes

Well, you can avoid all these tricks already today if you want by using a specific template for the LocalTransform that uses a single bit. But it is annoying to write today.
For the composite on the GhostComponent, this is something we noticed while ago that was missing (along other stuff…)
I would say, adding the composite to the GhostComponent is not the “right” solution (while is the fastest). The correct one is removing the “Composite” (that is actually an AggregateMask) from the GhostField and having a different attribute that cover both struct (component) and fields and has more correct and controllable “inheritance”/overrides rules.
But we can reason about that.
For now, thanks for the feedback!

3 Likes

Also, if it’s not too much of a complication, I would like to suggest that either the Composite or a new attribute also have a further setting that calculates the change bit off a Unsafe Utility MemCmp (memory comparison) instead of per field equality check. That would make exceptionally large replicated components far faster in equality checks like the values in a dynamic buffer or fixed list.

This only make sense for a single bit change mask and sufficiently large number of fields. In the former, yes, a memcmp is sufficient.

However, the thread off is the cost of invoking the memcmp that can be greater than doing the check on per field basis, given that it is also most of the time auto-vectorised (so up to 4/8 per cycle depend on the SSE or AVX version).
But in general, yes, the generated code can be optimised for certain specific use cases.

And again, for sake of pure performance, none can be context-aware manual written code (apart a second optimisation pass from the compiler)

2 Likes