Skip to content

feat: Fast buffer reader and fast buffer writer #1082

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 32 commits into from
Sep 16, 2021

Conversation

ShadauxCat
Copy link
Collaborator

@ShadauxCat ShadauxCat commented Aug 24, 2021

Covered by a total of 646 new unit tests.

…(aside from INetworkSerializable, which isn't ready to be tested yet due to lack of BufferSerializer)
- Streamlined the implementations of bit-packing uints and ulongs.
…lacing with byte* allocated directly through UnsafeUtility.Malloc() (same function used under the hood by NativeArray). This is required in order to be able to store pointers to FastBufferReader and FastBufferWriter in order to be able to wrap them with BufferSerializer - NativeArray contains a managed variable in it that disallows taking a pointer to it. And since FBW and FBR are structs, pointers are the only way to wrap them "by reference" in another struct - ref fields aren't allowed even inside ref structs.
…g values in other ref structs in a capture-by-reference style, updated BitReader and BitWriter to use that. Also aggressively inlining properties in FBW and FBR.
…. it's a little slower, but apparently some platforms won't support unaligned memory access (e.g., WEBGL, ARM processors) and there's no compile time way to detect ARM processors since the bytecode is not processor-dependent... the cost of the runtime detection would be more expensive than the cost of just doing the memcpy.
@@ -0,0 +1,2 @@
/BuildInfo.json
Copy link
Contributor

Choose a reason for hiding this comment

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

There's already 3+ .gitignores in the repo can we add this to one of those instead of this deeply nested one?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1, could add StreamingAssets/ line to testproject/.gitignore file :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry about that - my git client defaults to ignoring in the same directory when selecting to ignore files. I'll make sure to edit the .gitignore manually next time instead of using the client.

Copy link
Contributor

Choose a reason for hiding this comment

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

so, let's revert this file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

wtf... I did delete it... why is it still here?

/// <summary>
/// Pads the read bit count to byte alignment and commits the read back to the reader
/// </summary>
public void Dispose()
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you mean to implement IDisposable?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ref structs can't implement interfaces, but c# has special behavior to support using() on ref structs that have a public void Dispose()

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohh, neat. ref structs are pretty new to me. I'm a little uneasy about some of your changes in general because I haven't dipped into some of the new ref syntax - especially the Ref type you're adding. I hope you're seeking review from some folks that have a deep experience with this since this type of code is very risky

Copy link
Contributor

Choose a reason for hiding this comment

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

(To be clear, not saying you shouldn't be doing it, serialization is exactly where I'd expect to be writing this type of code)

Comment on lines +172 to +180
public static void ReadValueSafe(this ref FastBufferReader reader, out GameObject value)
{
reader.ReadValueSafe(out ulong networkObjectId);

if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject networkObject))
{
value = networkObject.gameObject;
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

we intentionally killed GameObject, NetworkObject & NetworkBehaviour serialization from built-in serializers. we wanted to kill NetworkManager/SpawnManager dependency in a primitive impl like this to keep it isolated and dependency-free (as much as we can).
also we were planning to offer more focused NetworkObject/NetworkBehaviour wrappers which would implement INetworkSerializable interface: struct NetworkObjectReference : INetworkSerializable etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree with that approach... I'll look into doing that in the INetworkMessage branch when I refactor INetworkSerializable.

/// Serializers and Deserializers for FastBufferWriter and FasteBufferReader
/// SerializersPacked and DeserializersPacked for BytePacker and ByteUnpacker
/// </summary>
public static class SerializationTypeTable
Copy link
Contributor

Choose a reason for hiding this comment

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

so we're basically saying you can have completely separate serializers and deserializers for the same type? and even completely separate packed and non-packed serializers and deserializers? (in total 2 * 2 = 4 tables per type)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Honestly, support for serializing object is only here because I don't know 100% for sure that I can kill it. I'll know when the IMessage stuff is done... And I'm hoping when I get there, I can just delete this file along with ReadObject and WriteObject entirely.

/// If true, an extra byte will be written to indicate whether or not the value is null.
/// Some types will always write this.
/// </param>
public static void WriteObjectPacked(ref FastBufferWriter writer, object value, bool isNullable = false)
Copy link
Contributor

Choose a reason for hiding this comment

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

since we're not offering anything automagical under this method here (apart from auto type -> method convenience), would it make more sense to drop this WriteObject/ReadObject combo entirely? I think we could have a convo about this and hopefully I might convince people to do so :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See above comment. I'm hoping the answer is yes but don't want to commit to that until I've converted the existing messages without needing it.

Comment on lines +116 to +150
/// <summary>
/// Serialize a GameObject
///
/// Throws OverflowException if the end of the buffer has been reached.
/// Write buffers will grow up to the maximum allowable message size before throwing OverflowException.
/// </summary>
/// <param name="value">Value to serialize</param>
public void SerializeValue(ref GameObject value)
{
m_Implementation.SerializeValue(ref value);
}

/// <summary>
/// Serialize a NetworkObject
///
/// Throws OverflowException if the end of the buffer has been reached.
/// Write buffers will grow up to the maximum allowable message size before throwing OverflowException.
/// </summary>
/// <param name="value">Value to serialize</param>
public void SerializeValue(ref NetworkObject value)
{
m_Implementation.SerializeValue(ref value);
}

/// <summary>
/// Serialize a NetworkBehaviour
///
/// Throws OverflowException if the end of the buffer has been reached.
/// Write buffers will grow up to the maximum allowable message size before throwing OverflowException.
/// </summary>
/// <param name="value">Value to serialize</param>
public void SerializeValue(ref NetworkBehaviour value)
{
m_Implementation.SerializeValue(ref value);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I wouldn't re-introduce these (both here and also reader/writer impls) because we know that we want to have struct NetworkObjectReference etc. for these scenarios. Killing these here and in reader/writer also kills NetworkManager.SpawnManager deps too — so, an easy win-win?

Copy link
Contributor

Choose a reason for hiding this comment

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

(I'd also add Read/Write Object to this context too, I remember calling that out earlier but I wanted to call out again to stress a little bit more)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll say the same thing I said before.. I want to finish INetworkMessage implementation before I kill these. If it turns out they're unused/unneeded once everything's done, I'll happily kill them, but I don't want to kill them now and have to resurrect them later. Converting NetworkObject and NetworkBehaviour to be INetworkSerializable should be trivial... GameObject not so much, but we can also just not support game objects and demand that the player send us the NetworkObject.

]
],
"excludePlatforms": [],
"allowUnsafeCode": true,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: all we need is "allowUnsafeCode": true line manually added here (I don't like Unity auto-gen'ing these things all the time, leaving minimal stuff here makes us more future-proof between different Unity Editor versions)

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.

(ship-it-parrot)

# Conflicts:
#	com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef
@ShadauxCat ShadauxCat enabled auto-merge (squash) September 16, 2021 01:55
@ShadauxCat ShadauxCat merged commit dc708a5 into develop Sep 16, 2021
@ShadauxCat ShadauxCat deleted the feature/fast_buffer_reader_writer branch September 16, 2021 03:09
SamuelBellomo added a commit that referenced this pull request Sep 16, 2021
…nsform

* develop:
  feat: Fast buffer reader and fast buffer writer (#1082)

# Conflicts:
#	com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef
SamuelBellomo added a commit that referenced this pull request Sep 16, 2021
…am/feature/client-network-transform

* sam/feature/interpolation-for-network-transform: (22 commits)
  fixing line issue
  more formatting
  fixing formatting issue
  removing not submitted LiteNetLib from ZooSam
  feat: Fast buffer reader and fast buffer writer (#1082)
  restricting public api
  bumping exec order
  feat: NetworkBehaviour.IsSpawned  (#1190)
  feat: added tip to the network manager inspector that directs to install tools (MTT-1211) (#1182)
  refactor!: remove network dictionary & set, use native container in List, add tests (#1149)
  fix: Fixed remote disconnects not properly cleaning up (#1184)
  test: base changes to PR-1114 (#1165)
  test: verify do not destroy networkobjects on networkmanager shutdown (#1183)
  chore: removal of EnableNetworkVariable in NetworkConfig. It's always True now (#1179)
  fix: Fix DontDestroyWithOwner not returning ownership (#1181)
  test: Giving Android some more room as the connection tests are timing sensitive (#1178)
  fix: unitytransport connectionmode buttons (#1176)
  test: added min frames to multi-instance helper (#1170)
  chore: Add mobile tests to nightly trigger (#1161)
  feat: snapshot spawn pre-requisite (#1166)
  ...

# Conflicts:
#	com.unity.netcode.gameobjects/Components/NetworkTransform.cs
#	testproject/Assets/Scenes/SampleScene.unity
mollstam pushed a commit to Keepsake-Games/com.unity.netcode.gameobjects that referenced this pull request Feb 13, 2023
)

* FastBufferWriter implemented and tested (still need to add and update some xmldoc comments though)

* A few additional tests to cover the last missed cases I can think of (aside from INetworkSerializable, which isn't ready to be tested yet due to lack of BufferSerializer)

* FastBufferReader + tests

* - More tests
- Streamlined the implementations of bit-packing uints and ulongs.

* - Removed NativeArray from FastBufferReader and FastBufferWriter, replacing with byte* allocated directly through UnsafeUtility.Malloc() (same function used under the hood by NativeArray). This is required in order to be able to store pointers to FastBufferReader and FastBufferWriter in order to be able to wrap them with BufferSerializer - NativeArray contains a managed variable in it that disallows taking a pointer to it. And since FBW and FBR are structs, pointers are the only way to wrap them "by reference" in another struct - ref fields aren't allowed even inside ref structs.

* Added utility ref struct "Ref<T>" to more generically support wrapping values in other ref structs in a capture-by-reference style, updated BitReader and BitWriter to use that. Also aggressively inlining properties in FBW and FBR.

* BufferSerializer and tests.

* Removed unnecessary comment.

* XMLDocs + cleanup for PR

* Replaced possibly unaligned memory access with UnsafeUtility.MemCpy... it's a little slower, but apparently some platforms won't support unaligned memory access (e.g., WEBGL, ARM processors) and there's no compile time way to detect ARM processors since the bytecode is not processor-dependent... the cost of the runtime detection would be more expensive than the cost of just doing the memcpy.

* Resurrected BytewiseUtil.FastCopyBytes as a faster alternative to UnsafeUtility.MemCpy for small values, while still supporting unaligned access.

* Reverting an accidental change.

* Removed files that got accidentally duplicated from before the rename.

* Standards fixes

* Removed accidentally added files.

* Added BuildInfo.json to the .gitignore so I stop accidentally checking it in.

* Addressed most of the review feedback. Still need to do a little more restructuring of some of the other tests.

* standards.py --fix

* standards.py --fix

* Fixed incorrect namespaces.

* -Fixed a couple of issues where growing a FastBufferWriter wouldn't work correctly (requesting beyond MaxCapacity and requesting more than double current capacity)
-Added support for FastBufferReader to be used in a mode that doesn't copy the input buffer

* Fix a test failure and better implementation of large growths

* - Removed RefArray
- Fixed incorrect text in a warning in FastBufferReader

* Removed RefArray meta file that stuck around.

* Review feedback: Used nameof() instead of string literal.

* -Removed BytewiseUtility.FastCopyBytes
-Added documentation on PreChecked() functions.

* removed .gitignore.

* Fixed compile errors that somehow didn't happen on my machine until I looked at the files they were in?

* standards.py --fix ... again ... despite no changes since the last success other than integrating develop ...
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.

8 participants