From eb4561fdd28fd7cad056d3828de8846cbede1312 Mon Sep 17 00:00:00 2001 From: mischa Date: Fri, 13 Sep 2024 12:32:50 +0200 Subject: [PATCH] perf: client serialization in one iteration! --- Assets/Mirror/Core/NetworkClient.cs | 62 ++++++--------- Assets/Mirror/Core/NetworkIdentity.cs | 77 +++++++------------ .../NetworkIdentitySerializationTests.cs | 4 +- 3 files changed, 50 insertions(+), 93 deletions(-) diff --git a/Assets/Mirror/Core/NetworkClient.cs b/Assets/Mirror/Core/NetworkClient.cs index a73c6cea4..1016dda09 100644 --- a/Assets/Mirror/Core/NetworkClient.cs +++ b/Assets/Mirror/Core/NetworkClient.cs @@ -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); } diff --git a/Assets/Mirror/Core/NetworkIdentity.cs b/Assets/Mirror/Core/NetworkIdentity.cs index 01b36ff7d..7fe283e2d 100644 --- a/Assets/Mirror/Core/NetworkIdentity.cs +++ b/Assets/Mirror/Core/NetworkIdentity.cs @@ -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(); + } } + //////////////////////////////////////////////////////////// } } } diff --git a/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs b/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs index 397613949..beed931f3 100644 --- a/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs +++ b/Assets/Mirror/Tests/Editor/NetworkIdentity/NetworkIdentitySerializationTests.cs @@ -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.