From 9404b80546167634ff3eee1569e85ef68ecab673 Mon Sep 17 00:00:00 2001 From: Liam Cary Date: Thu, 30 Apr 2026 14:31:56 +0800 Subject: [PATCH 1/5] Add NoHook option for all GeneratedSyncVarDeserialize methods. Reduces 32+ bytes of garbage allocation per deserialized syncvar when syncvar has no hook. --- Assets/Mirror/Core/NetworkBehaviour.cs | 24 +++++++++ .../Processors/NetworkBehaviourProcessor.cs | 53 ++++++++++++++----- Assets/Mirror/Editor/Weaver/WeaverTypes.cs | 8 +++ 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index 1f885f0176b..b9b6cf2617b 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -843,6 +843,11 @@ public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, } } + public void GeneratedSyncVarDeserialize_NoHook(ref T field, T value) + { + field = value; + } + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: @@ -920,6 +925,12 @@ public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action< } } + public void GeneratedSyncVarDeserialize_GameObject_NoHook(ref GameObject field, NetworkReader reader, ref uint netIdField) + { + netIdField = reader.ReadUInt(); + field = GetSyncVarGameObject(netIdField, ref field); + } + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: @@ -998,6 +1009,12 @@ public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity fiel } } + public void GeneratedSyncVarDeserialize_NetworkIdentity_NoHook(ref NetworkIdentity field, NetworkReader reader, ref uint netIdField) + { + netIdField = reader.ReadUInt(); + field = GetSyncVarNetworkIdentity(netIdField, ref field); + } + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: @@ -1078,6 +1095,13 @@ public void GeneratedSyncVarDeserialize_NetworkBehaviour(ref T field, Action< } } + public void GeneratedSyncVarDeserialize_NetworkBehaviour_NoHook(ref T field, NetworkReader reader, ref NetworkBehaviourSyncVar netIdField) + where T : NetworkBehaviour + { + netIdField = reader.ReadNetworkBehaviourSyncVar(); + field = GetSyncVarNetworkBehaviour(netIdField, ref field); + } + // helper function for [SyncVar] NetworkIdentities. // dirtyBit is a mask like 00010 protected void SetSyncVarNetworkIdentity(NetworkIdentity newIdentity, ref NetworkIdentity identityField, ulong dirtyBit, ref uint netIdField) diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs index 47f1b940487..37d147d02f3 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -593,20 +593,17 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Ldflda, syncVar); } + bool hasHook = syncVarHookDelegates.TryGetValue(syncVar, out (FieldDefinition hookDelegateField, MethodDefinition) value); + // If a hook exists, then we need to load the hook delegate on the stack // The hook delegate is created once in the constructor and stored in an instance field // We load the delegate from this instance field to avoid instantiating a new delegate instance every time (drastically reduces allocations) - if(syncVarHookDelegates.TryGetValue(syncVar, out (FieldDefinition hookDelegateField, MethodDefinition) value)) + if (hasHook) { // A hook exists. Push this.hookDelegateField onto the stack worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, value.hookDelegateField); } - else - { - // No hook exists. Push 'null' as hook - worker.Emit(OpCodes.Ldnull); - } // call GeneratedSyncVarDeserialize. // special cases for GameObject/NetworkIdentity/NetworkBehaviour @@ -620,7 +617,15 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav FieldDefinition netIdField = syncVarNetIds[syncVar]; worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netIdField); - worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject); + + if (hasHook) + { + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject); + } + else + { + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject_NoHook); + } } else if (syncVar.FieldType.Is()) { @@ -631,7 +636,15 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav FieldDefinition netIdField = syncVarNetIds[syncVar]; worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netIdField); - worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity); + + if (hasHook) + { + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity); + } + else + { + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity_NoHook); + } } // handle both NetworkBehaviour and inheritors. // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 @@ -646,8 +659,16 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netIdField); // make generic version of GeneratedSyncVarSetter_NetworkBehaviour - MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, syncVar.FieldType); - worker.Emit(OpCodes.Call, getFunc); + if (hasHook) + { + MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, syncVar.FieldType); + worker.Emit(OpCodes.Call, getFunc); + } + else + { + MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T_NoHook.MakeGeneric(assembly.MainModule, syncVar.FieldType); + worker.Emit(OpCodes.Call, getFunc); + } } else { @@ -668,8 +689,16 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Call, readFunc); // make generic version of GeneratedSyncVarDeserialize - MethodReference generic = weaverTypes.generatedSyncVarDeserialize.MakeGeneric(assembly.MainModule, syncVar.FieldType); - worker.Emit(OpCodes.Call, generic); + if (hasHook) + { + MethodReference generic = weaverTypes.generatedSyncVarDeserialize.MakeGeneric(assembly.MainModule, syncVar.FieldType); + worker.Emit(OpCodes.Call, generic); + } + else + { + MethodReference generic = weaverTypes.generatedSyncVarDeserialize_NoHook.MakeGeneric(assembly.MainModule, syncVar.FieldType); + worker.Emit(OpCodes.Call, generic); + } } } diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs index 27c3b7ba304..9cdf0953519 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs @@ -40,9 +40,13 @@ public class WeaverTypes public MethodReference generatedSyncVarSetter_NetworkIdentity; public MethodReference generatedSyncVarSetter_NetworkBehaviour_T; public MethodReference generatedSyncVarDeserialize; + public MethodReference generatedSyncVarDeserialize_NoHook; public MethodReference generatedSyncVarDeserialize_GameObject; + public MethodReference generatedSyncVarDeserialize_GameObject_NoHook; public MethodReference generatedSyncVarDeserialize_NetworkIdentity; + public MethodReference generatedSyncVarDeserialize_NetworkIdentity_NoHook; public MethodReference generatedSyncVarDeserialize_NetworkBehaviour_T; + public MethodReference generatedSyncVarDeserialize_NetworkBehaviour_T_NoHook; public MethodReference getSyncVarGameObjectReference; public MethodReference getSyncVarNetworkIdentityReference; public MethodReference getSyncVarNetworkBehaviourReference; @@ -108,9 +112,13 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail generatedSyncVarSetter_NetworkBehaviour_T = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_NetworkBehaviour", ref WeavingFailed); generatedSyncVarDeserialize_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_GameObject", ref WeavingFailed); + generatedSyncVarDeserialize_GameObject_NoHook = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_GameObject_NoHook", ref WeavingFailed); generatedSyncVarDeserialize = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize", ref WeavingFailed); + generatedSyncVarDeserialize_NoHook = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NoHook", ref WeavingFailed); generatedSyncVarDeserialize_NetworkIdentity = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkIdentity", ref WeavingFailed); + generatedSyncVarDeserialize_NetworkIdentity_NoHook = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkIdentity_NoHook", ref WeavingFailed); generatedSyncVarDeserialize_NetworkBehaviour_T = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkBehaviour", ref WeavingFailed); + generatedSyncVarDeserialize_NetworkBehaviour_T_NoHook = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkBehaviour_NoHook", ref WeavingFailed); getSyncVarGameObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarGameObject", ref WeavingFailed); getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkIdentity", ref WeavingFailed); From fdfec830c1a5b8f266c1494d2e77a5eef2189b82 Mon Sep 17 00:00:00 2001 From: Liam Cary Date: Tue, 5 May 2026 14:24:45 +0800 Subject: [PATCH 2/5] Revert "Add NoHook option for all GeneratedSyncVarDeserialize methods." This reverts commit 9404b80546167634ff3eee1569e85ef68ecab673. --- Assets/Mirror/Core/NetworkBehaviour.cs | 24 --------- .../Processors/NetworkBehaviourProcessor.cs | 53 +++++-------------- Assets/Mirror/Editor/Weaver/WeaverTypes.cs | 8 --- 3 files changed, 12 insertions(+), 73 deletions(-) diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index b9b6cf2617b..1f885f0176b 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -843,11 +843,6 @@ public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, } } - public void GeneratedSyncVarDeserialize_NoHook(ref T field, T value) - { - field = value; - } - // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: @@ -925,12 +920,6 @@ public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action< } } - public void GeneratedSyncVarDeserialize_GameObject_NoHook(ref GameObject field, NetworkReader reader, ref uint netIdField) - { - netIdField = reader.ReadUInt(); - field = GetSyncVarGameObject(netIdField, ref field); - } - // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: @@ -1009,12 +998,6 @@ public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity fiel } } - public void GeneratedSyncVarDeserialize_NetworkIdentity_NoHook(ref NetworkIdentity field, NetworkReader reader, ref uint netIdField) - { - netIdField = reader.ReadUInt(); - field = GetSyncVarNetworkIdentity(netIdField, ref field); - } - // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: @@ -1095,13 +1078,6 @@ public void GeneratedSyncVarDeserialize_NetworkBehaviour(ref T field, Action< } } - public void GeneratedSyncVarDeserialize_NetworkBehaviour_NoHook(ref T field, NetworkReader reader, ref NetworkBehaviourSyncVar netIdField) - where T : NetworkBehaviour - { - netIdField = reader.ReadNetworkBehaviourSyncVar(); - field = GetSyncVarNetworkBehaviour(netIdField, ref field); - } - // helper function for [SyncVar] NetworkIdentities. // dirtyBit is a mask like 00010 protected void SetSyncVarNetworkIdentity(NetworkIdentity newIdentity, ref NetworkIdentity identityField, ulong dirtyBit, ref uint netIdField) diff --git a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs index 37d147d02f3..47f1b940487 100644 --- a/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs +++ b/Assets/Mirror/Editor/Weaver/Processors/NetworkBehaviourProcessor.cs @@ -593,17 +593,20 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Ldflda, syncVar); } - bool hasHook = syncVarHookDelegates.TryGetValue(syncVar, out (FieldDefinition hookDelegateField, MethodDefinition) value); - // If a hook exists, then we need to load the hook delegate on the stack // The hook delegate is created once in the constructor and stored in an instance field // We load the delegate from this instance field to avoid instantiating a new delegate instance every time (drastically reduces allocations) - if (hasHook) + if(syncVarHookDelegates.TryGetValue(syncVar, out (FieldDefinition hookDelegateField, MethodDefinition) value)) { // A hook exists. Push this.hookDelegateField onto the stack worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldfld, value.hookDelegateField); } + else + { + // No hook exists. Push 'null' as hook + worker.Emit(OpCodes.Ldnull); + } // call GeneratedSyncVarDeserialize. // special cases for GameObject/NetworkIdentity/NetworkBehaviour @@ -617,15 +620,7 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav FieldDefinition netIdField = syncVarNetIds[syncVar]; worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netIdField); - - if (hasHook) - { - worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject); - } - else - { - worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject_NoHook); - } + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_GameObject); } else if (syncVar.FieldType.Is()) { @@ -636,15 +631,7 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav FieldDefinition netIdField = syncVarNetIds[syncVar]; worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netIdField); - - if (hasHook) - { - worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity); - } - else - { - worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity_NoHook); - } + worker.Emit(OpCodes.Call, weaverTypes.generatedSyncVarDeserialize_NetworkIdentity); } // handle both NetworkBehaviour and inheritors. // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939 @@ -659,16 +646,8 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Ldarg_0); worker.Emit(OpCodes.Ldflda, netIdField); // make generic version of GeneratedSyncVarSetter_NetworkBehaviour - if (hasHook) - { - MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, syncVar.FieldType); - worker.Emit(OpCodes.Call, getFunc); - } - else - { - MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T_NoHook.MakeGeneric(assembly.MainModule, syncVar.FieldType); - worker.Emit(OpCodes.Call, getFunc); - } + MethodReference getFunc = weaverTypes.generatedSyncVarDeserialize_NetworkBehaviour_T.MakeGeneric(assembly.MainModule, syncVar.FieldType); + worker.Emit(OpCodes.Call, getFunc); } else { @@ -689,16 +668,8 @@ void DeserializeField(FieldDefinition syncVar, ILProcessor worker, ref bool Weav worker.Emit(OpCodes.Call, readFunc); // make generic version of GeneratedSyncVarDeserialize - if (hasHook) - { - MethodReference generic = weaverTypes.generatedSyncVarDeserialize.MakeGeneric(assembly.MainModule, syncVar.FieldType); - worker.Emit(OpCodes.Call, generic); - } - else - { - MethodReference generic = weaverTypes.generatedSyncVarDeserialize_NoHook.MakeGeneric(assembly.MainModule, syncVar.FieldType); - worker.Emit(OpCodes.Call, generic); - } + MethodReference generic = weaverTypes.generatedSyncVarDeserialize.MakeGeneric(assembly.MainModule, syncVar.FieldType); + worker.Emit(OpCodes.Call, generic); } } diff --git a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs index 9cdf0953519..27c3b7ba304 100644 --- a/Assets/Mirror/Editor/Weaver/WeaverTypes.cs +++ b/Assets/Mirror/Editor/Weaver/WeaverTypes.cs @@ -40,13 +40,9 @@ public class WeaverTypes public MethodReference generatedSyncVarSetter_NetworkIdentity; public MethodReference generatedSyncVarSetter_NetworkBehaviour_T; public MethodReference generatedSyncVarDeserialize; - public MethodReference generatedSyncVarDeserialize_NoHook; public MethodReference generatedSyncVarDeserialize_GameObject; - public MethodReference generatedSyncVarDeserialize_GameObject_NoHook; public MethodReference generatedSyncVarDeserialize_NetworkIdentity; - public MethodReference generatedSyncVarDeserialize_NetworkIdentity_NoHook; public MethodReference generatedSyncVarDeserialize_NetworkBehaviour_T; - public MethodReference generatedSyncVarDeserialize_NetworkBehaviour_T_NoHook; public MethodReference getSyncVarGameObjectReference; public MethodReference getSyncVarNetworkIdentityReference; public MethodReference getSyncVarNetworkBehaviourReference; @@ -112,13 +108,9 @@ public WeaverTypes(AssemblyDefinition assembly, Logger Log, ref bool WeavingFail generatedSyncVarSetter_NetworkBehaviour_T = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarSetter_NetworkBehaviour", ref WeavingFailed); generatedSyncVarDeserialize_GameObject = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_GameObject", ref WeavingFailed); - generatedSyncVarDeserialize_GameObject_NoHook = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_GameObject_NoHook", ref WeavingFailed); generatedSyncVarDeserialize = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize", ref WeavingFailed); - generatedSyncVarDeserialize_NoHook = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NoHook", ref WeavingFailed); generatedSyncVarDeserialize_NetworkIdentity = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkIdentity", ref WeavingFailed); - generatedSyncVarDeserialize_NetworkIdentity_NoHook = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkIdentity_NoHook", ref WeavingFailed); generatedSyncVarDeserialize_NetworkBehaviour_T = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkBehaviour", ref WeavingFailed); - generatedSyncVarDeserialize_NetworkBehaviour_T_NoHook = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GeneratedSyncVarDeserialize_NetworkBehaviour_NoHook", ref WeavingFailed); getSyncVarGameObjectReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarGameObject", ref WeavingFailed); getSyncVarNetworkIdentityReference = Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "GetSyncVarNetworkIdentity", ref WeavingFailed); From b7e9adb14a6f98df9d8a48b0078e6931ed3c5924 Mon Sep 17 00:00:00 2001 From: Liam Cary Date: Tue, 5 May 2026 14:29:54 +0800 Subject: [PATCH 3/5] Move deferred sync var hook closure into method to avoid allocations when not deferring hook --- Assets/Mirror/Core/NetworkBehaviour.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index 1f885f0176b..e332577df7a 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -832,7 +832,10 @@ public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, // Capture values in closure for deferred execution T capturedPrevious = previous; T capturedNew = field; - deferredSyncVarHooks.Add(() => OnChanged(capturedPrevious, capturedNew)); + + // Anonymous method instance is instantiated upon entering the method containing it. + // Add deferred hook in nested method to avoid allocations when not deferring the hook. + AddDeferredSyncVarHook(OnChanged, capturedPrevious, capturedNew); } else { @@ -843,6 +846,11 @@ public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, } } + void AddDeferredSyncVarHook(Action hook, T capturedPrevious, T capturedNew) + { + deferredSyncVarHooks.Add(() => hook(capturedPrevious, capturedNew)); + } + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: From c6856a196803d88ce0593e6ca0099882d8e6a75c Mon Sep 17 00:00:00 2001 From: Liam Cary Date: Tue, 5 May 2026 14:48:36 +0800 Subject: [PATCH 4/5] Use AddDeferredSyncVarHook method for all GeneratedSyncVarDeserialize methods --- Assets/Mirror/Core/NetworkBehaviour.cs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Assets/Mirror/Core/NetworkBehaviour.cs b/Assets/Mirror/Core/NetworkBehaviour.cs index e332577df7a..7e820e1a240 100644 --- a/Assets/Mirror/Core/NetworkBehaviour.cs +++ b/Assets/Mirror/Core/NetworkBehaviour.cs @@ -766,6 +766,13 @@ public static bool SyncVarNetworkIdentityEqual(NetworkIdentity newIdentity, uint return newNetId == netIdField; } + // Anonymous method instance is instantiated upon entering the method containing it. + // Deferred hooks should only be added in a nested method to avoid allocations when not adding a deferred hook. + void AddDeferredSyncVarHook(Action hook, T capturedPrevious, T capturedNew) + { + deferredSyncVarHooks.Add(() => hook(capturedPrevious, capturedNew)); + } + // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: @@ -832,9 +839,6 @@ public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, // Capture values in closure for deferred execution T capturedPrevious = previous; T capturedNew = field; - - // Anonymous method instance is instantiated upon entering the method containing it. - // Add deferred hook in nested method to avoid allocations when not deferring the hook. AddDeferredSyncVarHook(OnChanged, capturedPrevious, capturedNew); } else @@ -846,11 +850,6 @@ public void GeneratedSyncVarDeserialize(ref T field, Action OnChanged, } } - void AddDeferredSyncVarHook(Action hook, T capturedPrevious, T capturedNew) - { - deferredSyncVarHooks.Add(() => hook(capturedPrevious, capturedNew)); - } - // move the [SyncVar] generated OnDeserialize C# to avoid much IL. // // before: @@ -918,7 +917,7 @@ public void GeneratedSyncVarDeserialize_GameObject(ref GameObject field, Action< { GameObject capturedPrevious = previousGameObject; GameObject capturedNew = field; - deferredSyncVarHooks.Add(() => OnChanged(capturedPrevious, capturedNew)); + AddDeferredSyncVarHook(OnChanged, capturedPrevious, capturedNew); } else { @@ -996,7 +995,7 @@ public void GeneratedSyncVarDeserialize_NetworkIdentity(ref NetworkIdentity fiel { NetworkIdentity capturedPrevious = previousIdentity; NetworkIdentity capturedNew = field; - deferredSyncVarHooks.Add(() => OnChanged(capturedPrevious, capturedNew)); + AddDeferredSyncVarHook(OnChanged, capturedPrevious, capturedNew); } else { @@ -1076,7 +1075,7 @@ public void GeneratedSyncVarDeserialize_NetworkBehaviour(ref T field, Action< { T capturedPrevious = previousBehaviour; T capturedNew = field; - deferredSyncVarHooks.Add(() => OnChanged(capturedPrevious, capturedNew)); + AddDeferredSyncVarHook(OnChanged, capturedPrevious, capturedNew); } else { From cfe83ae1a93202ed258360b6d947d39665cafab2 Mon Sep 17 00:00:00 2001 From: Liam Cary Date: Tue, 5 May 2026 14:49:10 +0800 Subject: [PATCH 5/5] Add AddDeferredOperation method for all sync collections --- Assets/Mirror/Core/SyncDictionary.cs | 10 +++++++--- Assets/Mirror/Core/SyncList.cs | 10 +++++++--- Assets/Mirror/Core/SyncSet.cs | 10 +++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Assets/Mirror/Core/SyncDictionary.cs b/Assets/Mirror/Core/SyncDictionary.cs index f9555715beb..d0fb04e74eb 100644 --- a/Assets/Mirror/Core/SyncDictionary.cs +++ b/Assets/Mirror/Core/SyncDictionary.cs @@ -378,9 +378,7 @@ void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool chec TKey capturedKey = key; TValue capturedItem = item; TValue capturedOld = oldItem; - - networkBehaviour.deferredSyncCollectionActions.Add(() => - InvokeActions(capturedOp, capturedKey, capturedItem, capturedOld)); + AddDeferredOperation(capturedOp, capturedKey, capturedItem, capturedOld); } else { @@ -390,6 +388,12 @@ void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool chec } } + void AddDeferredOperation(Operation capturedOp, TKey capturedKey, TValue capturedItem, TValue capturedOld) + { + networkBehaviour.deferredSyncCollectionActions.Add(() => + InvokeActions(capturedOp, capturedKey, capturedItem, capturedOld)); + } + void InvokeActions(Operation op, TKey key, TValue item, TValue oldItem) { switch (op) diff --git a/Assets/Mirror/Core/SyncList.cs b/Assets/Mirror/Core/SyncList.cs index eec88f13e86..d7b186bd022 100644 --- a/Assets/Mirror/Core/SyncList.cs +++ b/Assets/Mirror/Core/SyncList.cs @@ -139,9 +139,7 @@ void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkA int capturedIndex = itemIndex; T capturedOld = oldItem; T capturedNew = newItem; - - networkBehaviour.deferredSyncCollectionActions.Add(() => - InvokeActions(capturedOp, capturedIndex, capturedOld, capturedNew)); + AddDeferredOperation(capturedOp, capturedIndex, capturedOld, capturedNew); } else { @@ -151,6 +149,12 @@ void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkA } } + void AddDeferredOperation(Operation capturedOp, int capturedIndex, T capturedOld, T capturedNew) + { + networkBehaviour.deferredSyncCollectionActions.Add(() => + InvokeActions(capturedOp, capturedIndex, capturedOld, capturedNew)); + } + void InvokeActions(Operation op, int itemIndex, T oldItem, T newItem) { switch (op) diff --git a/Assets/Mirror/Core/SyncSet.cs b/Assets/Mirror/Core/SyncSet.cs index a098e1164d8..8ff3bd5ae24 100644 --- a/Assets/Mirror/Core/SyncSet.cs +++ b/Assets/Mirror/Core/SyncSet.cs @@ -130,9 +130,7 @@ void AddOperation(Operation op, T oldItem, T newItem, bool checkAccess, bool sho Operation capturedOp = op; T capturedOld = oldItem; T capturedNew = newItem; - - networkBehaviour.deferredSyncCollectionActions.Add(() => - InvokeActions(capturedOp, capturedOld, capturedNew)); + AddDeferredOperation(capturedOp, capturedOld, capturedNew); } else { @@ -142,6 +140,12 @@ void AddOperation(Operation op, T oldItem, T newItem, bool checkAccess, bool sho } } + void AddDeferredOperation(Operation capturedOp, T capturedOld, T capturedNew) + { + networkBehaviour.deferredSyncCollectionActions.Add(() => + InvokeActions(capturedOp, capturedOld, capturedNew)); + } + void AddOperation(Operation op, bool checkAccess) => AddOperation(op, default, default, checkAccess, true); void InvokeActions(Operation op, T oldItem, T newItem)