Skip to content

fix: Fix NetworkVariable<INetworkSerializable> so that it will properly call NetworkSerialize instead of using the default memcpy operation. #1383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 10, 2021

Conversation

ShadauxCat
Copy link
Collaborator

@ShadauxCat ShadauxCat commented Nov 1, 2021

This is implemented by putting NetworkVariable's read and write functionality inside a delegate and using ILPP to override that delegate at startup for INetworkMessage types.

The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this, NetworkVariable would need to do a var is INetworkSerializable check, and then cast to INetworkSerializable, both of which would cause a boxing allocation. Alternatively, NetworkVariable could have been split into NetworkVariable and NetworkSerializableVariable or something like that, which would have caused a poor user experience and an API that's easier to get wrong than right. This is a bit ugly on the implementation side, but it gets the best achievable user experience and performance.

MTT-1444

Changelog

com.unity.netcode.gameobjects

  • Fixed: Fixed NetworkVariable not calling NetworkSerialize on INetworkSerializable types

Testing and Documentation

  • Includes integration tests.
  • No documentation changes or additions were necessary.

@ShadauxCat ShadauxCat requested review from andrews-unity, mattwalsh-unity and 0xFA11 and removed request for andrews-unity November 1, 2021 22:53
Co-authored-by: Andrew Spiering <andrews@unity3d.com>
@@ -24,6 +24,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
- Added `try`/`catch` around RPC calls, preventing exception from causing further RPC calls to fail (#1329)
- Fixed an issue where ServerClientId and LocalClientId could have the same value, causing potential confusion, and also fixed an issue with the UNet where the server could be identified with two different values, one of which might be the same as LocalClientId, and the other of which would not.(#1368)
- IL2CPP would not properly compile (#1359)
- Fixed NetworkVariable not calling NetworkSerialize on INetworkSerializable types (#1383)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this up into the unreleased section as pre.3 is going to go out today and this will likely land in pre.4 which will include this and a UTP update to pre.8 for the UTP Adapter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@0xFA11
Copy link
Contributor

0xFA11 commented Nov 9, 2021

... runtime reflection and boxing ... need to do a var is INetworkSerializable check ...

do you mean var is INetworkSerializable when you say reflection?

also I'm not entirely sure if all this complexity would be justified by just "avoiding boxing allocation".
the trade-off there might not be worth at least until it actually becomes a problem and show up in performance metrics.

@ShadauxCat
Copy link
Collaborator Author

@MFatihMAR

do you mean var is INetworkSerializable when you say reflection?

No - I mean that one way to avoid boxing is to look up the correct method using reflection and basically do exactly what this solution does. The difference being in how it finds the delegate.

Boxing would actually be done twice - apparently var is INetworkSerializable is itself a boxing operation, and then the cast would be another boxing operation. I'm not sure if the compiler is smart enough to reuse the same boxing allocation for both statements or not.

@0xFA11
Copy link
Contributor

0xFA11 commented Nov 9, 2021

alright, I got you now.

well, you can have only 1 object boxing with something like this:

if (myNetVar is INetworkSerializable serializableNetVar) // <-- check type and define the variable with type
{
    // do something with `serializableNetVar` which has `INetworkSerializable` type
}

so, personally, I'm still not entirely convinced that the code complexity would worth it.
however, I'm happy to defer to @andrews-unity and/or @mattwalsh-unity if you'd like.

@andrews-unity
Copy link
Contributor

So the is check will always cause a box in our case but the compiler is smart enough using the syntax above to only do one box.

public interface IThing {
}

public class ABC<T> where T : unmanaged {
    public T x;
    
    public void Rawr() {
        if (x is IThing s) {
          Console.WriteLine(s.ToString());
        }    
    }
    
}

public struct Thing : IThing 
{
}
.method public hidebysig 
        instance void Rawr () cil managed 
    {
        // Method begins at RVA 0x2058
        // Code size 32 (0x20)
        .maxstack 1
        .locals init (
            [0] class IThing s
        )

        IL_0000: ldarg.0
        IL_0001: ldfld !0 class ABC`1<!T>::x
        IL_0006: box !T
        IL_000b: isinst IThing
        IL_0010: stloc.0
        IL_0011: ldloc.0
        IL_0012: brfalse.s IL_001f

        IL_0014: ldloc.0
        IL_0015: callvirt instance string [System.Private.CoreLib]System.Object::ToString()
        IL_001a: call void [System.Console]System.Console::WriteLine(string)

        IL_001f: ret
    } // end of method ABC`1::Rawr

@ShadauxCat
Copy link
Collaborator Author

well, what I will say is this implementation was the result of a couple of hours of conversation with @andrews-unity and @mattwalsh-unity . They're entitled to changing their minds, of course, but this was the solution we landed on after discussing several options, one of which was just accepting the box.

Personally I think this would be one of the worst possible places to have a box because this would cause

One box
Per frame
Per user network variable that was changed
Per network variable write OR read

If user code has 100 objects, each of which is writing to 10 network variables every frame, we'd be saddling our users with 1,000 boxing allocations per frame.... and the number of allocations scales with user usage patterns... I think avoiding that is worth the complexity, but that's just my opinion.

@0xFA11
Copy link
Contributor

0xFA11 commented Nov 10, 2021

if we're really trying to avoid it, we could synthesize serialize/deserialize method delegates in the constructor with reflection (as your other reflection-based suggestion). we could use reader.ReadValueSafe/writer.WriteValueSafe for non-INetworkSerialize and INetworkSerializable.NetworkSerialize for INetworkSerialize-based NetworkVariable<T>s.

that'd avoid ILPP at a reasonable (one-off) runtime cost and get us a non-boxed solution.

@ShadauxCat
Copy link
Collaborator Author

@MFatihMAR That was actually one of my initial suggestions, but I was told Unity style forbids using reflection at all. Plus, based on what we're seeing with tests, xbox one, xbox scarlet, and switch may not even work with that approach.... they seem to be aggressively removing functions that we only access via reflection.

@andrews-unity
Copy link
Contributor

To be clear I do not think we should be using reflection within Runtime code and we generally avoid doing so at all costs with most Unity code. (not saying we don't do it at times but you won't find many cases within the core engine or many packages where we are doing it)

Copy link
Contributor

@0xFA11 0xFA11 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given the constraints and problems we have, I guess I'll bite the bullet here

@andrews-unity andrews-unity merged commit b53926c into develop Nov 10, 2021
@andrews-unity andrews-unity deleted the fix/INetworkSerializable_NetworkVariables branch November 10, 2021 19:43
andrews-unity pushed a commit that referenced this pull request Nov 24, 2021
…ly call NetworkSerialize instead of using the default memcpy operation. (#1383)

* fix: Fix NetworkVariable<INetworkSerializable> so that it will properly call NetworkSerialize instead of using the default memcpy operation.

* Changelog entry

* standards fix

* Whoops, syntax error.

* More standards fixes

* Fixed typo

* Moved changelog entry to Unreleased
andrews-unity added a commit that referenced this pull request Nov 24, 2021
…ly call NetworkSerialize instead of using the default memcpy operation. (#1383) (#1463)

* fix: Fix NetworkVariable<INetworkSerializable> so that it will properly call NetworkSerialize instead of using the default memcpy operation.

* Changelog entry

* standards fix

* Whoops, syntax error.

* More standards fixes

* Fixed typo

* Moved changelog entry to Unreleased

Co-authored-by: Jaedyn Draper <284434+ShadauxCat@users.noreply.github.com>
Co-authored-by: Unity Netcode CI <74025435+netcode-ci-service@users.noreply.github.com>
mollstam pushed a commit to Keepsake-Games/com.unity.netcode.gameobjects that referenced this pull request Feb 13, 2023
…ly call NetworkSerialize instead of using the default memcpy operation. (Unity-Technologies#1383)

* fix: Fix NetworkVariable<INetworkSerializable> so that it will properly call NetworkSerialize instead of using the default memcpy operation.

* Changelog entry

* standards fix

* Whoops, syntax error.

* More standards fixes

* Fixed typo

* Moved changelog entry to Unreleased
mollstam pushed a commit to Keepsake-Games/com.unity.netcode.gameobjects that referenced this pull request Feb 13, 2023
…ly call NetworkSerialize instead of using the default memcpy operation. (Unity-Technologies#1383) (Unity-Technologies#1463)

* fix: Fix NetworkVariable<INetworkSerializable> so that it will properly call NetworkSerialize instead of using the default memcpy operation.

* Changelog entry

* standards fix

* Whoops, syntax error.

* More standards fixes

* Fixed typo

* Moved changelog entry to Unreleased

Co-authored-by: Jaedyn Draper <284434+ShadauxCat@users.noreply.github.com>
Co-authored-by: Unity Netcode CI <74025435+netcode-ci-service@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants