perf: client serialization in one iteration!

This commit is contained in:
mischa 2024-09-13 12:32:50 +02:00
parent d8e33f933f
commit eb4561fdd2
3 changed files with 50 additions and 93 deletions

View File

@ -1746,65 +1746,47 @@ static void BroadcastToServer(bool unreliableBaselineElapsed)
if (identity != null)
{
// 'Reliable' sync: send Reliable components over reliable.
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
using (NetworkWriterPooled writerReliable = NetworkWriterPool.Get(),
writerUnreliableDelta = NetworkWriterPool.Get(),
writerUnreliableBaseline = NetworkWriterPool.Get())
{
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
identity.SerializeClient_ReliableComponents(writer);
if (writer.Position > 0)
// serialize reliable and unreliable components in only one iteration.
// serializing reliable and unreliable separately in two iterations would be too costly.
identity.SerializeClient(writerReliable, writerUnreliableBaseline, writerUnreliableDelta, unreliableBaselineElapsed);
// any reliable components serialization?
if (writerReliable.Position > 0)
{
// send state update message
EntityStateMessage message = new EntityStateMessage
{
netId = identity.netId,
payload = writer.ToArraySegment()
payload = writerReliable.ToArraySegment()
};
Send(message);
}
}
// 'Unreliable' sync: send Unreliable components over unreliable
// state is 'initial' for reliable baseline, and 'not initial' for unreliable deltas.
// note that syncInterval is always ignored for unreliable in order to have tick aligned [SyncVars].
// even if we pass SyncMethod.Reliable, it serializes with initialState=true.
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
identity.SerializeClient_UnreliableComponents(false, writer);
// we always need the unreliable delta no matter what.
// this ensures we can smoothly sync even during reliable baseline ticks.
// (do this before baseline, since baseline clears dirty bits)
if (writer.Position > 0)
// any unreliable components serialization?
// we always send unreliable deltas to ensure interpolation always has a data point that arrives immediately.
if (writerUnreliableDelta.Position > 0)
{
EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta
{
// baselineTick: the last unreliable baseline to compare against
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = writer.ToArraySegment()
payload = writerUnreliableDelta.ToArraySegment()
};
Send(message, Channels.Unreliable);
// quake sends unreliable messages twice to make up for message drops.
// this double bandwidth, but allows for smaller buffer time / faster sync.
// best to turn this off unless the game is extremely fast paced.
if (unreliableRedundancy) Send(message, Channels.Unreliable);
}
}
// if it's for a baseline sync, then send a reliable baseline message too.
// this will likely arrive slightly after the unreliable delta above.
if (unreliableBaselineElapsed)
{
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
// time for unreliable baseline sync?
// we always send this after the unreliable delta,
// so there's a higher chance that it arrives after the delta.
// in other words: so that the delta can still be used against previous baseline.
if (unreliableBaselineElapsed)
{
// get serialization for this entity viewed by this connection
// (if anything was serialized this time)
identity.SerializeClient_UnreliableComponents(true, writer);
if (writer.Position > 0)
if (writerUnreliableBaseline.Position > 0)
{
// remember last sent baseline tick for this entity.
// (byte) to minimize bandwidth. we don't need the full tick,
@ -1816,7 +1798,7 @@ static void BroadcastToServer(bool unreliableBaselineElapsed)
{
baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId,
payload = writer.ToArraySegment()
payload = writerUnreliableBaseline.ToArraySegment()
};
Send(message, Channels.Reliable);
}

View File

@ -1265,7 +1265,9 @@ internal void SerializeServer_Broadcast_UnreliableComponents(bool isBaseline, Ne
}
}
internal void SerializeClient_ReliableComponents(NetworkWriter writer)
// serialize Reliable and Unreliable components in one iteration.
// having two separate functions doing two iterations would be too costly.
internal void SerializeClient(NetworkWriter writerReliable, NetworkWriter writerUnreliableBaseline, NetworkWriter writerUnreliableDelta, bool unreliableBaseline)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
@ -1278,7 +1280,8 @@ internal void SerializeClient_ReliableComponents(NetworkWriter writer)
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
ulong dirtyMask = ClientDirtyMask_ReliableComponents();
ulong dirtyMaskReliable = ClientDirtyMask_ReliableComponents();
ulong dirtyMaskUnreliable = ClientDirtyMask_UnreliableComponents();
// varint compresses the mask to 1 byte in most cases.
// instead of writing an 8 byte ulong.
@ -1289,25 +1292,28 @@ internal void SerializeClient_ReliableComponents(NetworkWriter writer)
// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask);
if (dirtyMaskReliable != 0) Compression.CompressVarUInt(writerReliable, dirtyMaskReliable);
if (dirtyMaskUnreliable != 0) Compression.CompressVarUInt(writerUnreliableDelta, dirtyMaskUnreliable);
if (dirtyMaskUnreliable != 0) Compression.CompressVarUInt(writerUnreliableBaseline, dirtyMaskUnreliable);
// serialize all components
// perf: only iterate if dirty mask has dirty bits.
if (dirtyMask != 0)
if (dirtyMaskReliable != 0 || dirtyMaskUnreliable != 0)
{
// serialize all components
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour comp = components[i];
// RELIABLE SERIALIZATION //////////////////////////////////
// is this component dirty?
// reuse the mask instead of calling comp.IsDirty() again here.
if (IsDirty(dirtyMask, i))
if (IsDirty(dirtyMaskReliable, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
// serialize into writer.
// server always knows initialState, we never need to send it
comp.Serialize(writer, false);
comp.Serialize(writerReliable, false);
// clear dirty bits for the components that we serialized.
// do not clear for _all_ components, only the ones that
@ -1317,57 +1323,26 @@ internal void SerializeClient_ReliableComponents(NetworkWriter writer)
// was elapsed, as then they wouldn't be synced.
comp.ClearAllDirtyBits();
}
}
}
}
internal void SerializeClient_UnreliableComponents(bool isBaseline, NetworkWriter writer)
{
// ensure NetworkBehaviours are valid before usage
ValidateComponents();
NetworkBehaviour[] components = NetworkBehaviours;
// check which components are dirty.
// this is quite complicated with SyncMode + SyncDirection.
// see the function for explanation.
//
// instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth.
ulong dirtyMask = ClientDirtyMask_UnreliableComponents();
// varint compresses the mask to 1 byte in most cases.
// instead of writing an 8 byte ulong.
// 7 components fit into 1 byte. (previously 7 bytes)
// 11 components fit into 2 bytes. (previously 11 bytes)
// 16 components fit into 3 bytes. (previously 16 bytes)
// TODO imer: server knows amount of comps, write N bytes instead
// if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty mask!
if (dirtyMask != 0) Compression.CompressVarUInt(writer, dirtyMask);
// serialize all components
// perf: only iterate if dirty mask has dirty bits.
if (dirtyMask != 0)
{
// serialize all components
for (int i = 0; i < components.Length; ++i)
{
NetworkBehaviour comp = components[i];
// UNRELIABLE COMPONENTS ///////////////////////////////////
// is this component dirty?
// reuse the mask instead of calling comp.IsDirty() again here.
if (IsDirty(dirtyMask, i))
else if (IsDirty(dirtyMaskUnreliable, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{
// serialize into writer.
comp.Serialize(writer, isBaseline);
// we always send the unreliable delta no matter what
comp.Serialize(writerUnreliableDelta, false);
// for unreliable components, only clear dirty bits after the reliable baseline.
// unreliable deltas aren't guaranteed to be delivered, no point in clearing bits.
if (isBaseline) comp.ClearAllDirtyBits();
// sometimes we need the unreliable baseline
if (unreliableBaseline)
{
comp.Serialize(writerUnreliableBaseline, true);
// for unreliable components, only clear dirty bits after the reliable baseline.
// unreliable deltas aren't guaranteed to be delivered, no point in clearing bits.
comp.ClearAllDirtyBits();
}
}
////////////////////////////////////////////////////////////
}
}
}

View File

@ -243,7 +243,7 @@ public void SerializeClient_NotInitial_NotDirty_WritesNothing()
// clientComp.value = "42";
// serialize client object
clientIdentity.SerializeClient_ReliableComponents(ownerWriter);
clientIdentity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false);
Assert.That(ownerWriter.Position, Is.EqualTo(0));
}
@ -267,7 +267,7 @@ public void SerializeAndDeserialize_ClientToServer_NOT_OWNED()
comp2.value = "67890";
// serialize all
identity.SerializeClient_ReliableComponents(ownerWriter);
identity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false);
// shouldn't sync anything. because even though it's ClientToServer,
// we don't own this one so we shouldn't serialize & sync it.