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) if (identity != null)
{ {
// 'Reliable' sync: send Reliable components over reliable. // '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 // serialize reliable and unreliable components in only one iteration.
// (if anything was serialized this time) // serializing reliable and unreliable separately in two iterations would be too costly.
identity.SerializeClient_ReliableComponents(writer); identity.SerializeClient(writerReliable, writerUnreliableBaseline, writerUnreliableDelta, unreliableBaselineElapsed);
if (writer.Position > 0)
// any reliable components serialization?
if (writerReliable.Position > 0)
{ {
// send state update message // send state update message
EntityStateMessage message = new EntityStateMessage EntityStateMessage message = new EntityStateMessage
{ {
netId = identity.netId, netId = identity.netId,
payload = writer.ToArraySegment() payload = writerReliable.ToArraySegment()
}; };
Send(message); Send(message);
} }
}
// 'Unreliable' sync: send Unreliable components over unreliable // any unreliable components serialization?
// state is 'initial' for reliable baseline, and 'not initial' for unreliable deltas. // we always send unreliable deltas to ensure interpolation always has a data point that arrives immediately.
// note that syncInterval is always ignored for unreliable in order to have tick aligned [SyncVars]. if (writerUnreliableDelta.Position > 0)
// 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)
{ {
EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta EntityStateMessageUnreliableDelta message = new EntityStateMessageUnreliableDelta
{ {
// baselineTick: the last unreliable baseline to compare against
baselineTick = identity.lastUnreliableBaselineSent, baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId, netId = identity.netId,
payload = writer.ToArraySegment() payload = writerUnreliableDelta.ToArraySegment()
}; };
Send(message, Channels.Unreliable); 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. // time for unreliable baseline sync?
// this will likely arrive slightly after the unreliable delta above. // we always send this after the unreliable delta,
if (unreliableBaselineElapsed) // 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.
using (NetworkWriterPooled writer = NetworkWriterPool.Get()) if (unreliableBaselineElapsed)
{ {
// get serialization for this entity viewed by this connection if (writerUnreliableBaseline.Position > 0)
// (if anything was serialized this time)
identity.SerializeClient_UnreliableComponents(true, writer);
if (writer.Position > 0)
{ {
// remember last sent baseline tick for this entity. // remember last sent baseline tick for this entity.
// (byte) to minimize bandwidth. we don't need the full tick, // (byte) to minimize bandwidth. we don't need the full tick,
@ -1816,7 +1798,7 @@ static void BroadcastToServer(bool unreliableBaselineElapsed)
{ {
baselineTick = identity.lastUnreliableBaselineSent, baselineTick = identity.lastUnreliableBaselineSent,
netId = identity.netId, netId = identity.netId,
payload = writer.ToArraySegment() payload = writerUnreliableBaseline.ToArraySegment()
}; };
Send(message, Channels.Reliable); 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 // ensure NetworkBehaviours are valid before usage
ValidateComponents(); ValidateComponents();
@ -1278,7 +1280,8 @@ internal void SerializeClient_ReliableComponents(NetworkWriter writer)
// instead of writing a 1 byte index per component, // instead of writing a 1 byte index per component,
// we limit components to 64 bits and write one ulong instead. // we limit components to 64 bits and write one ulong instead.
// the ulong is also varint compressed for minimum bandwidth. // 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. // varint compresses the mask to 1 byte in most cases.
// instead of writing an 8 byte ulong. // 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. // if nothing dirty, then don't even write the mask.
// otherwise, every unchanged object would send a 1 byte dirty 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 // serialize all components
// perf: only iterate if dirty mask has dirty bits. // perf: only iterate if dirty mask has dirty bits.
if (dirtyMask != 0) if (dirtyMaskReliable != 0 || dirtyMaskUnreliable != 0)
{ {
// serialize all components // serialize all components
for (int i = 0; i < components.Length; ++i) for (int i = 0; i < components.Length; ++i)
{ {
NetworkBehaviour comp = components[i]; NetworkBehaviour comp = components[i];
// RELIABLE SERIALIZATION //////////////////////////////////
// is this component dirty? // is this component dirty?
// reuse the mask instead of calling comp.IsDirty() again here. // reuse the mask instead of calling comp.IsDirty() again here.
if (IsDirty(dirtyMask, i)) if (IsDirty(dirtyMaskReliable, i))
// if (isOwned && component.syncDirection == SyncDirection.ClientToServer) // if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{ {
// serialize into writer. // serialize into writer.
// server always knows initialState, we never need to send it // 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. // clear dirty bits for the components that we serialized.
// do not clear for _all_ components, only the ones that // 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. // was elapsed, as then they wouldn't be synced.
comp.ClearAllDirtyBits(); comp.ClearAllDirtyBits();
} }
} // UNRELIABLE COMPONENTS ///////////////////////////////////
}
}
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];
// is this component dirty? // is this component dirty?
// reuse the mask instead of calling comp.IsDirty() again here. // 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) // if (isOwned && component.syncDirection == SyncDirection.ClientToServer)
{ {
// serialize into writer. // we always send the unreliable delta no matter what
comp.Serialize(writer, isBaseline); comp.Serialize(writerUnreliableDelta, false);
// for unreliable components, only clear dirty bits after the reliable baseline. // sometimes we need the unreliable baseline
// unreliable deltas aren't guaranteed to be delivered, no point in clearing bits. if (unreliableBaseline)
if (isBaseline) comp.ClearAllDirtyBits(); {
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"; // clientComp.value = "42";
// serialize client object // serialize client object
clientIdentity.SerializeClient_ReliableComponents(ownerWriter); clientIdentity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false);
Assert.That(ownerWriter.Position, Is.EqualTo(0)); Assert.That(ownerWriter.Position, Is.EqualTo(0));
} }
@ -267,7 +267,7 @@ public void SerializeAndDeserialize_ClientToServer_NOT_OWNED()
comp2.value = "67890"; comp2.value = "67890";
// serialize all // serialize all
identity.SerializeClient_ReliableComponents(ownerWriter); identity.SerializeClient(ownerWriter, new NetworkWriter(), new NetworkWriter(), false);
// shouldn't sync anything. because even though it's ClientToServer, // shouldn't sync anything. because even though it's ClientToServer,
// we don't own this one so we shouldn't serialize & sync it. // we don't own this one so we shouldn't serialize & sync it.