diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BitCounter.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BitCounter.cs new file mode 100644 index 0000000000..4feb4477da --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BitCounter.cs @@ -0,0 +1,126 @@ +using System.Runtime.CompilerServices; + +namespace Unity.Netcode +{ + public static class BitCounter + { + // Since we don't have access to BitOperations.LeadingZeroCount() (which would have been the fastest) + // we use the De Bruijn sequence to do this calculation + // See https://en.wikipedia.org/wiki/De_Bruijn_sequence and https://www.chessprogramming.org/De_Bruijn_Sequence + private const ulong k_DeBruijnMagic64 = 0x37E84A99DAE458F; + private const uint k_DeBruijnMagic32 = 0x06EB14F9; + + // We're counting bytes, not bits, so these have all had the operation x/8 + 1 applied + private static readonly int[] k_DeBruijnTableBytes64 = + { + 0/8+1, 1/8+1, 17/8+1, 2/8+1, 18/8+1, 50/8+1, 3/8+1, 57/8+1, + 47/8+1, 19/8+1, 22/8+1, 51/8+1, 29/8+1, 4/8+1, 33/8+1, 58/8+1, + 15/8+1, 48/8+1, 20/8+1, 27/8+1, 25/8+1, 23/8+1, 52/8+1, 41/8+1, + 54/8+1, 30/8+1, 38/8+1, 5/8+1, 43/8+1, 34/8+1, 59/8+1, 8/8+1, + 63/8+1, 16/8+1, 49/8+1, 56/8+1, 46/8+1, 21/8+1, 28/8+1, 32/8+1, + 14/8+1, 26/8+1, 24/8+1, 40/8+1, 53/8+1, 37/8+1, 42/8+1, 7/8+1, + 62/8+1, 55/8+1, 45/8+1, 31/8+1, 13/8+1, 39/8+1, 36/8+1, 6/8+1, + 61/8+1, 44/8+1, 12/8+1, 35/8+1, 60/8+1, 11/8+1, 10/8+1, 9/8+1, + }; + + private static readonly int[] k_DeBruijnTableBytes32 = + { + 0/8+1, 1/8+1, 16/8+1, 2/8+1, 29/8+1, 17/8+1, 3/8+1, 22/8+1, + 30/8+1, 20/8+1, 18/8+1, 11/8+1, 13/8+1, 4/8+1, 7/8+1, 23/8+1, + 31/8+1, 15/8+1, 28/8+1, 21/8+1, 19/8+1, 10/8+1, 12/8+1, 6/8+1, + 14/8+1, 27/8+1, 9/8+1, 5/8+1, 26/8+1, 8/8+1, 25/8+1, 24/8+1, + }; + + // And here we're counting the number of set bits, not the position of the highest set, + // so these still have +1 applied - unfortunately 0 and 1 both return the same value. + private static readonly int[] k_DeBruijnTableBits64 = + { + 0+1, 1+1, 17+1, 2+1, 18+1, 50+1, 3+1, 57+1, + 47+1, 19+1, 22+1, 51+1, 29+1, 4+1, 33+1, 58+1, + 15+1, 48+1, 20+1, 27+1, 25+1, 23+1, 52+1, 41+1, + 54+1, 30+1, 38+1, 5+1, 43+1, 34+1, 59+1, 8+1, + 63+1, 16+1, 49+1, 56+1, 46+1, 21+1, 28+1, 32+1, + 14+1, 26+1, 24+1, 40+1, 53+1, 37+1, 42+1, 7+1, + 62+1, 55+1, 45+1, 31+1, 13+1, 39+1, 36+1, 6+1, + 61+1, 44+1, 12+1, 35+1, 60+1, 11+1, 10+1, 9+1, + }; + + private static readonly int[] k_DeBruijnTableBits32 = + { + 0+1, 1+1, 16+1, 2+1, 29+1, 17+1, 3+1, 22+1, + 30+1, 20+1, 18+1, 11+1, 13+1, 4+1, 7+1, 23+1, + 31+1, 15+1, 28+1, 21+1, 19+1, 10+1, 12+1, 6+1, + 14+1, 27+1, 9+1, 5+1, 26+1, 8+1, 25+1, 24+1, + }; + + /// + /// Get the minimum number of bytes required to represent the given value + /// + /// The value + /// The number of bytes required + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUsedByteCount(uint value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value = value & ~(value >> 1); + return k_DeBruijnTableBytes32[value * k_DeBruijnMagic32 >> 27]; + } + + /// + /// Get the minimum number of bytes required to represent the given value + /// + /// The value + /// The number of bytes required + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUsedByteCount(ulong value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value |= value >> 32; + value = value & ~(value >> 1); + return k_DeBruijnTableBytes64[value * k_DeBruijnMagic64 >> 58]; + } + + /// + /// Get the minimum number of bits required to represent the given value + /// + /// The value + /// The number of bits required + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUsedBitCount(uint value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value = value & ~(value >> 1); + return k_DeBruijnTableBits32[value * k_DeBruijnMagic32 >> 27]; + } + + /// + /// Get the minimum number of bits required to represent the given value + /// + /// The value + /// The number of bits required + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetUsedBitCount(ulong value) + { + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + value |= value >> 32; + value = value & ~(value >> 1); + return k_DeBruijnTableBits64[value * k_DeBruijnMagic64 >> 58]; + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BitCounter.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/BitCounter.cs.meta new file mode 100644 index 0000000000..f9d96d15b6 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BitCounter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6983de23935090341bf45d5564401b9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs new file mode 100644 index 0000000000..bb97d0c95b --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs @@ -0,0 +1,218 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + /// + /// Helper class for doing bitwise reads for a FastBufferReader. + /// Ensures all bitwise reads end on proper byte alignment so FastBufferReader doesn't have to be concerned + /// with misaligned reads. + /// + public ref struct BitReader + { + private Ref m_Reader; + private readonly unsafe byte* m_BufferPointer; + private readonly int m_Position; + private int m_BitPosition; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + private int m_AllowedBitwiseReadMark; +#endif + + private const int k_BitsPerByte = 8; + + /// + /// Whether or not the current BitPosition is evenly divisible by 8. I.e. whether or not the BitPosition is at a byte boundary. + /// + public bool BitAligned + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (m_BitPosition & 7) == 0; + } + + internal unsafe BitReader(ref FastBufferReader reader) + { + m_Reader = new Ref(ref reader); + + m_BufferPointer = m_Reader.Value.BufferPointer + m_Reader.Value.Position; + m_Position = m_Reader.Value.Position; + m_BitPosition = 0; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_AllowedBitwiseReadMark = (m_Reader.Value.AllowedReadMark - m_Position) * k_BitsPerByte; +#endif + } + + /// + /// Pads the read bit count to byte alignment and commits the read back to the reader + /// + public void Dispose() + { + var bytesWritten = m_BitPosition >> 3; + if (!BitAligned) + { + // Accounting for the partial read + ++bytesWritten; + } + + m_Reader.Value.CommitBitwiseReads(bytesWritten); + } + + /// + /// Verifies the requested bit count can be read from the buffer. + /// This exists as a separate method to allow multiple bit reads to be bounds checked with a single call. + /// If it returns false, you may not read, and in editor and development builds, attempting to do so will + /// throw an exception. In release builds, attempting to do so will read junk memory. + /// + /// Number of bits you want to read, in total + /// True if you can read, false if that would exceed buffer bounds + public bool TryBeginReadBits(uint bitCount) + { + var newBitPosition = m_BitPosition + bitCount; + var totalBytesWrittenInBitwiseContext = newBitPosition >> 3; + if ((newBitPosition & 7) != 0) + { + // Accounting for the partial read + ++totalBytesWrittenInBitwiseContext; + } + + if (m_Reader.Value.PositionInternal + totalBytesWrittenInBitwiseContext > m_Reader.Value.LengthInternal) + { + return false; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_AllowedBitwiseReadMark = (int)newBitPosition; +#endif + return true; + } + + /// + /// Read a certain amount of bits from the stream. + /// + /// Value to store bits into. + /// Amount of bits to read + public unsafe void ReadBits(out ulong value, uint bitCount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (bitCount > 64) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read more than 64 bits from a 64-bit value!"); + } + + if (bitCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read fewer than 0 bits!"); + } + + int checkPos = (int)(m_BitPosition + bitCount); + if (checkPos > m_AllowedBitwiseReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()"); + } +#endif + ulong val = 0; + + int wholeBytes = (int)bitCount / k_BitsPerByte; + byte* asBytes = (byte*)&val; + if (BitAligned) + { + if (wholeBytes != 0) + { + ReadPartialValue(out val, wholeBytes); + } + } + else + { + for (var i = 0; i < wholeBytes; ++i) + { + ReadMisaligned(out asBytes[i]); + } + } + + val |= (ulong)ReadByteBits((int)bitCount & 7) << ((int)bitCount & ~7); + value = val; + } + + /// + /// Read bits from stream. + /// + /// Value to store bits into. + /// Amount of bits to read. + public void ReadBits(out byte value, uint bitCount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int checkPos = (int)(m_BitPosition + bitCount); + if (checkPos > m_AllowedBitwiseReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()"); + } +#endif + value = ReadByteBits((int)bitCount); + } + + /// + /// Read a single bit from the buffer + /// + /// Out value of the bit. True represents 1, False represents 0 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBit(out bool bit) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int checkPos = (m_BitPosition + 1); + if (checkPos > m_AllowedBitwiseReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()"); + } +#endif + + int offset = m_BitPosition & 7; + int pos = m_BitPosition >> 3; + bit = (m_BufferPointer[pos] & (1 << offset)) != 0; + ++m_BitPosition; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ReadPartialValue(out T value, int bytesToRead, int offsetBytes = 0) where T : unmanaged + { + var val = new T(); + byte* ptr = ((byte*)&val) + offsetBytes; + byte* bufferPointer = m_BufferPointer + m_Position; + UnsafeUtility.MemCpy(ptr, bufferPointer, bytesToRead); + + m_BitPosition += bytesToRead * k_BitsPerByte; + value = val; + } + + private byte ReadByteBits(int bitCount) + { + if (bitCount > 8) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read more than 8 bits into an 8-bit value!"); + } + + if (bitCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read fewer than 0 bits!"); + } + + int result = 0; + var convert = new ByteBool(); + for (int i = 0; i < bitCount; ++i) + { + ReadBit(out bool bit); + result |= convert.Collapse(bit) << i; + } + + return (byte)result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ReadMisaligned(out byte value) + { + int off = m_BitPosition & 7; + int pos = m_BitPosition >> 3; + int shift1 = 8 - off; + + value = (byte)((m_BufferPointer[pos] >> shift1) | (m_BufferPointer[(m_BitPosition += 8) >> 3] << shift1)); + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs.meta new file mode 100644 index 0000000000..9b0d159683 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 72e2d94a96ca96a4fb2921df9adc2fdf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs new file mode 100644 index 0000000000..3895c506fe --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs @@ -0,0 +1,211 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + /// + /// Helper class for doing bitwise writes for a FastBufferWriter. + /// Ensures all bitwise writes end on proper byte alignment so FastBufferWriter doesn't have to be concerned + /// with misaligned writes. + /// + public ref struct BitWriter + { + private Ref m_Writer; + private unsafe byte* m_BufferPointer; + private readonly int m_Position; + private int m_BitPosition; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + private int m_AllowedBitwiseWriteMark; +#endif + private const int k_BitsPerByte = 8; + + /// + /// Whether or not the current BitPosition is evenly divisible by 8. I.e. whether or not the BitPosition is at a byte boundary. + /// + public bool BitAligned + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (m_BitPosition & 7) == 0; + } + + internal unsafe BitWriter(ref FastBufferWriter writer) + { + m_Writer = new Ref(ref writer); + m_BufferPointer = writer.BufferPointer + writer.PositionInternal; + m_Position = writer.PositionInternal; + m_BitPosition = 0; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_AllowedBitwiseWriteMark = (m_Writer.Value.AllowedWriteMark - m_Writer.Value.Position) * k_BitsPerByte; +#endif + } + + /// + /// Pads the written bit count to byte alignment and commits the write back to the writer + /// + public void Dispose() + { + var bytesWritten = m_BitPosition >> 3; + if (!BitAligned) + { + // Accounting for the partial write + ++bytesWritten; + } + + m_Writer.Value.CommitBitwiseWrites(bytesWritten); + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be writing multiple fields back-to-back and you know the total size, + /// you can call TryBeginWriteBits() once on the total size, and then follow it with calls to + /// WriteBit() or WriteBits(). + /// + /// Bitwise write operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using TryBeginWriteBits(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using TryBeginWrite is to avoid bounds checking in the following + /// operations in release builds. Instead, attempting to write past the marked position in release builds + /// will write to random memory and cause undefined behavior, likely including instability and crashes. + /// + /// Number of bits you want to write, in total + /// True if you can write, false if that would exceed buffer bounds + public unsafe bool TryBeginWriteBits(int bitCount) + { + var newBitPosition = m_BitPosition + bitCount; + var totalBytesWrittenInBitwiseContext = newBitPosition >> 3; + if ((newBitPosition & 7) != 0) + { + // Accounting for the partial write + ++totalBytesWrittenInBitwiseContext; + } + + if (m_Position + totalBytesWrittenInBitwiseContext > m_Writer.Value.CapacityInternal) + { + if (m_Position + totalBytesWrittenInBitwiseContext > m_Writer.Value.MaxCapacityInternal) + { + return false; + } + if (m_Writer.Value.CapacityInternal < m_Writer.Value.MaxCapacityInternal) + { + m_Writer.Value.Grow(totalBytesWrittenInBitwiseContext); + m_BufferPointer = m_Writer.Value.BufferPointer + m_Writer.Value.PositionInternal; + } + else + { + return false; + } + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_AllowedBitwiseWriteMark = newBitPosition; +#endif + return true; + } + + /// + /// Write s certain amount of bits to the stream. + /// + /// Value to get bits from. + /// Amount of bits to write + public unsafe void WriteBits(ulong value, uint bitCount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (bitCount > 64) + { + throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot write more than 64 bits from a 64-bit value!"); + } + + int checkPos = (int)(m_BitPosition + bitCount); + if (checkPos > m_AllowedBitwiseWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWriteBits)}()"); + } +#endif + + int wholeBytes = (int)bitCount / k_BitsPerByte; + byte* asBytes = (byte*)&value; + if (BitAligned) + { + if (wholeBytes != 0) + { + WritePartialValue(value, wholeBytes); + } + } + else + { + for (var i = 0; i < wholeBytes; ++i) + { + WriteMisaligned(asBytes[i]); + } + } + + for (var count = wholeBytes * k_BitsPerByte; count < bitCount; ++count) + { + WriteBit((value & (1UL << count)) != 0); + } + } + + /// + /// Write bits to stream. + /// + /// Value to get bits from. + /// Amount of bits to write. + public void WriteBits(byte value, uint bitCount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int checkPos = (int)(m_BitPosition + bitCount); + if (checkPos > m_AllowedBitwiseWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWriteBits)}()"); + } +#endif + + for (int i = 0; i < bitCount; ++i) + { + WriteBit(((value >> i) & 1) != 0); + } + } + + /// + /// Write a single bit to the buffer + /// + /// Value of the bit. True represents 1, False represents 0 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBit(bool bit) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + int checkPos = (m_BitPosition + 1); + if (checkPos > m_AllowedBitwiseWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWriteBits)}()"); + } +#endif + + int offset = m_BitPosition & 7; + int pos = m_BitPosition >> 3; + ++m_BitPosition; + m_BufferPointer[pos] = (byte)(bit ? (m_BufferPointer[pos] & ~(1 << offset)) | (1 << offset) : (m_BufferPointer[pos] & ~(1 << offset))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void WritePartialValue(T value, int bytesToWrite, int offsetBytes = 0) where T : unmanaged + { + byte* ptr = ((byte*)&value) + offsetBytes; + byte* bufferPointer = m_BufferPointer + m_Position; + UnsafeUtility.MemCpy(bufferPointer, ptr, bytesToWrite); + + m_BitPosition += bytesToWrite * k_BitsPerByte; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void WriteMisaligned(byte value) + { + int off = m_BitPosition & 7; + int pos = m_BitPosition >> 3; + int shift1 = 8 - off; + m_BufferPointer[pos + 1] = (byte)((m_BufferPointer[pos + 1] & (0xFF << off)) | (value >> shift1)); + m_BufferPointer[pos] = (byte)((m_BufferPointer[pos] & (0xFF >> shift1)) | (value << off)); + + m_BitPosition += 8; + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs.meta new file mode 100644 index 0000000000..604982f3c0 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d6360e096142c149a11a2e86560c350 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializer.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializer.cs new file mode 100644 index 0000000000..acfecdd2d5 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializer.cs @@ -0,0 +1,343 @@ +using System; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Two-way serializer wrapping FastBufferReader or FastBufferWriter. + /// + /// Implemented as a ref struct for two reasons: + /// 1. The BufferSerializer cannot outlive the FBR/FBW it wraps or using it will cause a crash + /// 2. The BufferSerializer must always be passed by reference and can't be copied + /// + /// Ref structs help enforce both of those rules: they can't out live the stack context in which they were + /// created, and they're always passed by reference no matter what. + /// + /// BufferSerializer doesn't wrapp FastBufferReader or FastBufferWriter directly because it can't. + /// ref structs can't implement interfaces, and in order to be able to have two different implementations with + /// the same interface (which allows us to avoid an "if(IsReader)" on every call), the thing directly wrapping + /// the struct has to implement an interface. So IBufferSerializerImplementation exists as the interface, + /// which is implemented by a normal struct, while the ref struct wraps the normal one to enforce the two above + /// requirements. (Allowing direct access to the IBufferSerializerImplementation struct would allow dangerous + /// things to happen because the struct's lifetime could outlive the Reader/Writer's.) + /// + /// The implementation struct + public ref struct BufferSerializer where TImplementation : IBufferSerializerImplementation + { + private TImplementation m_Implementation; + + /// + /// Check if the contained implementation is a reader + /// + public bool IsReader => m_Implementation.IsReader; + + /// + /// Check if the contained implementation is a writer + /// + public bool IsWriter => m_Implementation.IsWriter; + + public BufferSerializer(TImplementation implementation) + { + m_Implementation = implementation; + } + + /// + /// Retrieves the FastBufferReader instance. Only valid if IsReader = true, throws + /// InvalidOperationException otherwise. + /// + /// Reader instance + public ref FastBufferReader GetFastBufferReader() + { + return ref m_Implementation.GetFastBufferReader(); + } + + /// + /// Retrieves the FastBufferWriter instance. Only valid if IsWriter = true, throws + /// InvalidOperationException otherwise. + /// + /// Writer instance + public ref FastBufferWriter GetFastBufferWriter() + { + return ref m_Implementation.GetFastBufferWriter(); + } + + /// + /// Serialize an object value. + /// Note: Will ALWAYS cause allocations when reading. + /// This function is also much slower than the others as it has to figure out how to serialize + /// the object using runtime reflection. + /// It's recommended not to use this unless you have no choice. + /// + /// 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. + /// + /// Value to serialize + /// Type to deserialize to when reading + /// + /// If true, will force an isNull byte to be written. + /// Some types will write this byte regardless. + /// + public void SerializeValue(ref object value, Type type, bool isNullable = false) + { + m_Implementation.SerializeValue(ref value, type, isNullable); + } + + /// + /// Serialize an INetworkSerializable + /// If your INetworkSerializable is implemented by a struct, as opposed to a class, use this + /// function instead of SerializeValue. SerializeValue will incur a boxing allocation, + /// SerializeNetworkSerializable will not. + /// + /// A definition of SerializeValue that doesn't allocate can't be created because C# + /// doesn't allow overriding generics based solely on the constraint, so this would conflict + /// with WriteValue<T>(ref T value) where T: unmanaged + /// + /// 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. + /// + /// Value to serialize + public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable + { + m_Implementation.SerializeNetworkSerializable(ref value); + } + + /// + /// Serialize an INetworkSerializable + /// + /// 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. + /// + /// Value to serialize + public void SerializeValue(ref INetworkSerializable value) + { + m_Implementation.SerializeValue(ref value); + } + + /// + /// 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. + /// + /// Value to serialize + public void SerializeValue(ref GameObject value) + { + m_Implementation.SerializeValue(ref value); + } + + /// + /// 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. + /// + /// Value to serialize + public void SerializeValue(ref NetworkObject value) + { + m_Implementation.SerializeValue(ref value); + } + + /// + /// 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. + /// + /// Value to serialize + public void SerializeValue(ref NetworkBehaviour value) + { + m_Implementation.SerializeValue(ref value); + } + + /// + /// Serialize a string. + /// + /// Note: Will ALWAYS allocate a new string when reading. + /// + /// 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. + /// + /// Value to serialize + /// + /// If true, will truncate each char to one byte. + /// This is slower than two-byte chars, but uses less bandwidth. + /// + public void SerializeValue(ref string s, bool oneByteChars = false) + { + m_Implementation.SerializeValue(ref s, oneByteChars); + } + + /// + /// Serialize an array value. + /// + /// Note: Will ALWAYS allocate a new array when reading. + /// If you have a statically-sized array that you know is large enough, it's recommended to + /// serialize the size yourself and iterate serializing array members. + /// + /// (This is because C# doesn't allow setting an array's length value, so deserializing + /// into an existing array of larger size would result in an array that doesn't have as many values + /// as its Length indicates it should.) + /// + /// 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. + /// + /// Value to serialize + public void SerializeValue(ref T[] array) where T : unmanaged + { + m_Implementation.SerializeValue(ref array); + } + + /// + /// Serialize a single byte + /// + /// 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. + /// + /// Value to serialize + public void SerializeValue(ref byte value) + { + m_Implementation.SerializeValue(ref value); + } + + /// + /// Serialize an unmanaged type. Supports basic value types as well as structs. + /// The provided type will be copied to/from the buffer as it exists in memory. + /// + /// 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. + /// + /// Value to serialize + public void SerializeValue(ref T value) where T : unmanaged + { + m_Implementation.SerializeValue(ref value); + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be writing multiple fields back-to-back and you know the total size, + /// you can call PreCheck() once on the total size, and then follow it with calls to + /// SerializeValuePreChecked() for faster serialization. Write buffers will grow during PreCheck() + /// if needed. + /// + /// PreChecked serialization operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using PreCheck(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using PreCheck is to avoid bounds checking in the following + /// operations in release builds. + /// + /// To get the correct size to check for, use FastBufferWriter.GetWriteSize(value) or + /// FastBufferWriter.GetWriteSize<type>() + /// + /// Number of bytes you plan to read or write + /// True if the read/write can proceed, false otherwise. + public bool PreCheck(int amount) + { + return m_Implementation.PreCheck(amount); + } + + /// + /// Serialize a GameObject. + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref GameObject value) + { + m_Implementation.SerializeValuePreChecked(ref value); + } + + /// + /// Serialize a NetworkObject + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref NetworkObject value) + { + m_Implementation.SerializeValuePreChecked(ref value); + } + + /// + /// Serialize a NetworkBehaviour + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref NetworkBehaviour value) + { + m_Implementation.SerializeValuePreChecked(ref value); + } + + /// + /// Serialize a string. + /// + /// Note: Will ALWAYS allocate a new string when reading. + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + /// + /// If true, will truncate each char to one byte. + /// This is slower than two-byte chars, but uses less bandwidth. + /// + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) + { + m_Implementation.SerializeValuePreChecked(ref s, oneByteChars); + } + + /// + /// Serialize an array value. + /// + /// Note: Will ALWAYS allocate a new array when reading. + /// If you have a statically-sized array that you know is large enough, it's recommended to + /// serialize the size yourself and iterate serializing array members. + /// + /// (This is because C# doesn't allow setting an array's length value, so deserializing + /// into an existing array of larger size would result in an array that doesn't have as many values + /// as its Length indicates it should.) + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref T[] array) where T : unmanaged + { + m_Implementation.SerializeValuePreChecked(ref array); + } + + /// + /// Serialize a single byte + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref byte value) + { + m_Implementation.SerializeValuePreChecked(ref value); + } + + /// + /// Serialize an unmanaged type. Supports basic value types as well as structs. + /// The provided type will be copied to/from the buffer as it exists in memory. + /// + /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only + /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple + /// serialization operations in one function call instead of having to do bounds checking on every call. + /// + /// Value to serialize + public void SerializeValuePreChecked(ref T value) where T : unmanaged + { + m_Implementation.SerializeValuePreChecked(ref value); + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializer.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializer.cs.meta new file mode 100644 index 0000000000..fe70dbe5be --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fca519b9bc3b32e4f9bd7cd138d690af +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerReader.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerReader.cs new file mode 100644 index 0000000000..ae777d3735 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerReader.cs @@ -0,0 +1,118 @@ +using System; +using UnityEngine; + +namespace Unity.Netcode +{ + internal struct BufferSerializerReader : IBufferSerializerImplementation + { + private Ref m_Reader; + + public BufferSerializerReader(ref FastBufferReader reader) + { + m_Reader = new Ref(ref reader); + } + + public bool IsReader => true; + public bool IsWriter => false; + + public ref FastBufferReader GetFastBufferReader() + { + return ref m_Reader.Value; + } + + public ref FastBufferWriter GetFastBufferWriter() + { + throw new InvalidOperationException("Cannot retrieve a FastBufferWriter from a serializer where IsWriter = false"); + } + + public void SerializeValue(ref object value, Type type, bool isNullable = false) + { + m_Reader.Value.ReadObject(out value, type, isNullable); + } + + public void SerializeValue(ref INetworkSerializable value) + { + m_Reader.Value.ReadNetworkSerializable(out value); + } + + public void SerializeValue(ref GameObject value) + { + m_Reader.Value.ReadValueSafe(out value); + } + + public void SerializeValue(ref NetworkObject value) + { + m_Reader.Value.ReadValueSafe(out value); + } + + public void SerializeValue(ref NetworkBehaviour value) + { + m_Reader.Value.ReadValueSafe(out value); + } + + public void SerializeValue(ref string s, bool oneByteChars = false) + { + m_Reader.Value.ReadValueSafe(out s, oneByteChars); + } + + public void SerializeValue(ref T[] array) where T : unmanaged + { + m_Reader.Value.ReadValueSafe(out array); + } + + public void SerializeValue(ref byte value) + { + m_Reader.Value.ReadByteSafe(out value); + } + + public void SerializeValue(ref T value) where T : unmanaged + { + m_Reader.Value.ReadValueSafe(out value); + } + + public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable + { + m_Reader.Value.ReadNetworkSerializable(out value); + } + + public bool PreCheck(int amount) + { + return m_Reader.Value.TryBeginRead(amount); + } + + public void SerializeValuePreChecked(ref GameObject value) + { + m_Reader.Value.ReadValue(out value); + } + + public void SerializeValuePreChecked(ref NetworkObject value) + { + m_Reader.Value.ReadValue(out value); + } + + public void SerializeValuePreChecked(ref NetworkBehaviour value) + { + m_Reader.Value.ReadValue(out value); + } + + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) + { + m_Reader.Value.ReadValue(out s, oneByteChars); + } + + public void SerializeValuePreChecked(ref T[] array) where T : unmanaged + { + m_Reader.Value.ReadValue(out array); + } + + public void SerializeValuePreChecked(ref byte value) + { + m_Reader.Value.ReadValue(out value); + } + + public void SerializeValuePreChecked(ref T value) where T : unmanaged + { + m_Reader.Value.ReadValue(out value); + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerReader.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerReader.cs.meta new file mode 100644 index 0000000000..ed94cf148a --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 70dd17b6c14f7cd43ba5380d01cf91ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerWriter.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerWriter.cs new file mode 100644 index 0000000000..f3a345ea99 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerWriter.cs @@ -0,0 +1,118 @@ +using System; +using UnityEngine; + +namespace Unity.Netcode +{ + internal struct BufferSerializerWriter : IBufferSerializerImplementation + { + private Ref m_Writer; + + public BufferSerializerWriter(ref FastBufferWriter writer) + { + m_Writer = new Ref(ref writer); + } + + public bool IsReader => false; + public bool IsWriter => true; + + public ref FastBufferReader GetFastBufferReader() + { + throw new InvalidOperationException("Cannot retrieve a FastBufferReader from a serializer where IsReader = false"); + } + + public ref FastBufferWriter GetFastBufferWriter() + { + return ref m_Writer.Value; + } + + public void SerializeValue(ref object value, Type type, bool isNullable = false) + { + m_Writer.Value.WriteObject(value, isNullable); + } + + public void SerializeValue(ref INetworkSerializable value) + { + m_Writer.Value.WriteNetworkSerializable(value); + } + + public void SerializeValue(ref GameObject value) + { + m_Writer.Value.WriteValueSafe(value); + } + + public void SerializeValue(ref NetworkObject value) + { + m_Writer.Value.WriteValueSafe(value); + } + + public void SerializeValue(ref NetworkBehaviour value) + { + m_Writer.Value.WriteValueSafe(value); + } + + public void SerializeValue(ref string s, bool oneByteChars = false) + { + m_Writer.Value.WriteValueSafe(s, oneByteChars); + } + + public void SerializeValue(ref T[] array) where T : unmanaged + { + m_Writer.Value.WriteValueSafe(array); + } + + public void SerializeValue(ref byte value) + { + m_Writer.Value.WriteByteSafe(value); + } + + public void SerializeValue(ref T value) where T : unmanaged + { + m_Writer.Value.WriteValueSafe(value); + } + + public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable + { + m_Writer.Value.WriteNetworkSerializable(value); + } + + public bool PreCheck(int amount) + { + return m_Writer.Value.TryBeginWrite(amount); + } + + public void SerializeValuePreChecked(ref GameObject value) + { + m_Writer.Value.WriteValue(value); + } + + public void SerializeValuePreChecked(ref NetworkObject value) + { + m_Writer.Value.WriteValue(value); + } + + public void SerializeValuePreChecked(ref NetworkBehaviour value) + { + m_Writer.Value.WriteValue(value); + } + + public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) + { + m_Writer.Value.WriteValue(s, oneByteChars); + } + + public void SerializeValuePreChecked(ref T[] array) where T : unmanaged + { + m_Writer.Value.WriteValue(array); + } + + public void SerializeValuePreChecked(ref byte value) + { + m_Writer.Value.WriteByte(value); + } + + public void SerializeValuePreChecked(ref T value) where T : unmanaged + { + m_Writer.Value.WriteValue(value); + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerWriter.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerWriter.cs.meta new file mode 100644 index 0000000000..183a7bafad --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BufferSerializerWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c05ed8e3061e62147a012cc01a64b5a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BytePacker.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BytePacker.cs new file mode 100644 index 0000000000..9eb5854e67 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BytePacker.cs @@ -0,0 +1,617 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Utility class for packing values in serialization. + /// + public static class BytePacker + { + #region Managed TypePacking + + /// + /// Writes a boxed object in a packed format + /// Named differently from other WriteValuePacked methods to avoid accidental boxing. + /// Don't use this method unless you have no other choice. + /// + /// Writer to write to + /// The object to write + /// + /// If true, an extra byte will be written to indicate whether or not the value is null. + /// Some types will always write this. + /// + public static void WriteObjectPacked(ref FastBufferWriter writer, object value, bool isNullable = false) + { +#if UNITY_NETCODE_DEBUG_NO_PACKING + writer.WriteObject(value, isNullable); + return; +#endif + if (isNullable || value.GetType().IsNullable()) + { + bool isNull = value == null || (value is UnityEngine.Object o && o == null); + + WriteValuePacked(ref writer, isNull); + + if (isNull) + { + return; + } + } + + var type = value.GetType(); + var hasSerializer = SerializationTypeTable.SerializersPacked.TryGetValue(type, out var serializer); + if (hasSerializer) + { + serializer(ref writer, value); + return; + } + + if (value is Array array) + { + WriteValuePacked(ref writer, array.Length); + + for (int i = 0; i < array.Length; i++) + { + WriteObjectPacked(ref writer, array.GetValue(i)); + } + } + + if (value.GetType().IsEnum) + { + switch (Convert.GetTypeCode(value)) + { + case TypeCode.Boolean: + WriteValuePacked(ref writer, (byte)value); + break; + case TypeCode.Char: + WriteValuePacked(ref writer, (char)value); + break; + case TypeCode.SByte: + WriteValuePacked(ref writer, (sbyte)value); + break; + case TypeCode.Byte: + WriteValuePacked(ref writer, (byte)value); + break; + case TypeCode.Int16: + WriteValuePacked(ref writer, (short)value); + break; + case TypeCode.UInt16: + WriteValuePacked(ref writer, (ushort)value); + break; + case TypeCode.Int32: + WriteValuePacked(ref writer, (int)value); + break; + case TypeCode.UInt32: + WriteValuePacked(ref writer, (uint)value); + break; + case TypeCode.Int64: + WriteValuePacked(ref writer, (long)value); + break; + case TypeCode.UInt64: + WriteValuePacked(ref writer, (ulong)value); + break; + } + return; + } + if (value is GameObject) + { + ((GameObject)value).TryGetComponent(out var networkObject); + if (networkObject == null) + { + throw new ArgumentException($"{nameof(NetworkWriter)} cannot write {nameof(GameObject)} types that does not has a {nameof(NetworkObject)} component attached. {nameof(GameObject)}: {((GameObject)value).name}"); + } + + if (!networkObject.IsSpawned) + { + throw new ArgumentException($"{nameof(NetworkWriter)} cannot write {nameof(NetworkObject)} types that are not spawned. {nameof(GameObject)}: {((GameObject)value).name}"); + } + + WriteValuePacked(ref writer, networkObject.NetworkObjectId); + return; + } + if (value is NetworkObject) + { + if (!((NetworkObject)value).IsSpawned) + { + throw new ArgumentException($"{nameof(NetworkWriter)} cannot write {nameof(NetworkObject)} types that are not spawned. {nameof(GameObject)}: {((NetworkObject)value).gameObject.name}"); + } + + WriteValuePacked(ref writer, ((NetworkObject)value).NetworkObjectId); + return; + } + if (value is NetworkBehaviour) + { + if (!((NetworkBehaviour)value).HasNetworkObject || !((NetworkBehaviour)value).NetworkObject.IsSpawned) + { + throw new ArgumentException($"{nameof(NetworkWriter)} cannot write {nameof(NetworkBehaviour)} types that are not spawned. {nameof(GameObject)}: {((NetworkBehaviour)value).gameObject.name}"); + } + + WriteValuePacked(ref writer, ((NetworkBehaviour)value).NetworkObjectId); + WriteValuePacked(ref writer, ((NetworkBehaviour)value).NetworkBehaviourId); + return; + } + if (value is INetworkSerializable) + { + //TODO ((INetworkSerializable)value).NetworkSerialize(new NetworkSerializer(this)); + return; + } + + throw new ArgumentException($"{nameof(NetworkWriter)} cannot write type {value.GetType().Name} - it does not implement {nameof(INetworkSerializable)}"); + } + #endregion + + #region Unmanaged Type Packing + +#if UNITY_NETCODE_DEBUG_NO_PACKING + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValuePacked(ref FastBufferWriter writer, T value) where T: unmanaged => writer.WriteValueSafe(value); +#else + /// + /// Write a packed enum value. + /// + /// The writer to write to + /// The value to write + /// An enum type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void WriteValuePacked(ref FastBufferWriter writer, TEnum value) where TEnum : unmanaged, Enum + { + TEnum enumValue = value; + switch (sizeof(TEnum)) + { + case sizeof(int): + WriteValuePacked(ref writer, *(int*)&enumValue); + break; + case sizeof(byte): + WriteValuePacked(ref writer, *(byte*)&enumValue); + break; + case sizeof(short): + WriteValuePacked(ref writer, *(short*)&enumValue); + break; + case sizeof(long): + WriteValuePacked(ref writer, *(long*)&enumValue); + break; + } + } + + /// + /// Write single-precision floating point value to the buffer as a varint + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, float value) + { + WriteUInt32Packed(ref writer, ToUint(value)); + } + + /// + /// Write double-precision floating point value to the buffer as a varint + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, double value) + { + WriteUInt64Packed(ref writer, ToUlong(value)); + } + + /// + /// Write a byte to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, byte value) => writer.WriteByteSafe(value); + + /// + /// Write a signed byte to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, sbyte value) => writer.WriteByteSafe((byte)value); + + /// + /// Write a bool to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, bool value) => writer.WriteValueSafe(value); + + + /// + /// Write a signed short (Int16) as a ZigZag encoded varint to the buffer. + /// WARNING: If the value you're writing is > 2287, this will use MORE space + /// (3 bytes instead of 2), and if your value is > 240 you'll get no savings at all. + /// Only use this if you're certain your value will be small. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, short value) => WriteUInt32Packed(ref writer, (ushort)Arithmetic.ZigZagEncode(value)); + + /// + /// Write an unsigned short (UInt16) as a varint to the buffer. + /// WARNING: If the value you're writing is > 2287, this will use MORE space + /// (3 bytes instead of 2), and if your value is > 240 you'll get no savings at all. + /// Only use this if you're certain your value will be small. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, ushort value) => WriteUInt32Packed(ref writer, value); + + /// + /// Write a two-byte character as a varint to the buffer. + /// WARNING: If the value you're writing is > 2287, this will use MORE space + /// (3 bytes instead of 2), and if your value is > 240 you'll get no savings at all. + /// Only use this if you're certain your value will be small. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, char c) => WriteUInt32Packed(ref writer, c); + + /// + /// Write a signed int (Int32) as a ZigZag encoded varint to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, int value) => WriteUInt32Packed(ref writer, (uint)Arithmetic.ZigZagEncode(value)); + + /// + /// Write an unsigned int (UInt32) to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, uint value) => WriteUInt32Packed(ref writer, value); + + /// + /// Write an unsigned long (UInt64) to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, ulong value) => WriteUInt64Packed(ref writer, value); + + /// + /// Write a signed long (Int64) as a ZigZag encoded varint to the buffer. + /// + /// The writer to write to + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, long value) => WriteUInt64Packed(ref writer, Arithmetic.ZigZagEncode(value)); + + /// + /// Convenience method that writes two packed Vector3 from the ray to the buffer + /// + /// The writer to write to + /// Ray to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, Ray ray) + { + WriteValuePacked(ref writer, ray.origin); + WriteValuePacked(ref writer, ray.direction); + } + + /// + /// Convenience method that writes two packed Vector2 from the ray to the buffer + /// + /// The writer to write to + /// Ray2D to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, Ray2D ray2d) + { + WriteValuePacked(ref writer, ray2d.origin); + WriteValuePacked(ref writer, ray2d.direction); + } + + /// + /// Convenience method that writes four varint floats from the color to the buffer + /// + /// The writer to write to + /// Color to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, Color color) + { + WriteValuePacked(ref writer, color.r); + WriteValuePacked(ref writer, color.g); + WriteValuePacked(ref writer, color.b); + WriteValuePacked(ref writer, color.a); + } + + /// + /// Convenience method that writes four varint floats from the color to the buffer + /// + /// The writer to write to + /// Color to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, Color32 color) + { + WriteValuePacked(ref writer, color.r); + WriteValuePacked(ref writer, color.g); + WriteValuePacked(ref writer, color.b); + WriteValuePacked(ref writer, color.a); + } + + /// + /// Convenience method that writes two varint floats from the vector to the buffer + /// + /// The writer to write to + /// Vector to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, Vector2 vector2) + { + WriteValuePacked(ref writer, vector2.x); + WriteValuePacked(ref writer, vector2.y); + } + + /// + /// Convenience method that writes three varint floats from the vector to the buffer + /// + /// The writer to write to + /// Vector to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, Vector3 vector3) + { + WriteValuePacked(ref writer, vector3.x); + WriteValuePacked(ref writer, vector3.y); + WriteValuePacked(ref writer, vector3.z); + } + + /// + /// Convenience method that writes four varint floats from the vector to the buffer + /// + /// The writer to write to + /// Vector to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, Vector4 vector4) + { + WriteValuePacked(ref writer, vector4.x); + WriteValuePacked(ref writer, vector4.y); + WriteValuePacked(ref writer, vector4.z); + WriteValuePacked(ref writer, vector4.w); + } + + /// + /// Writes the rotation to the buffer. + /// + /// The writer to write to + /// Rotation to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, Quaternion rotation) + { + WriteValuePacked(ref writer, rotation.x); + WriteValuePacked(ref writer, rotation.y); + WriteValuePacked(ref writer, rotation.z); + WriteValuePacked(ref writer, rotation.w); + } + + /// + /// Writes a string in a packed format + /// + /// The writer to write to + /// The value to pack + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteValuePacked(ref FastBufferWriter writer, string s) + { + WriteValuePacked(ref writer, (uint)s.Length); + int target = s.Length; + for (int i = 0; i < target; ++i) + { + WriteValuePacked(ref writer, s[i]); + } + } +#endif + #endregion + + #region Bit Packing + +#if UNITY_NETCODE_DEBUG_NO_PACKING + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteValueBitPacked(ref FastBufferWriter writer, T value) where T: unmanaged => writer.WriteValueSafe(value); +#else + /// + /// Writes a 14-bit signed short to the buffer in a bit-encoded packed format. + /// The first bit indicates whether the value is 1 byte or 2. + /// The sign bit takes up another bit. + /// That leaves 14 bits for the value. + /// A value greater than 2^14-1 or less than -2^14 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its two + /// most significant bits after zig-zag encoding. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(ref FastBufferWriter writer, short value) => WriteValueBitPacked(ref writer, (ushort)Arithmetic.ZigZagEncode(value)); + + /// + /// Writes a 15-bit unsigned short to the buffer in a bit-encoded packed format. + /// The first bit indicates whether the value is 1 byte or 2. + /// That leaves 15 bits for the value. + /// A value greater than 2^15-1 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its + /// most significant bit. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(ref FastBufferWriter writer, ushort value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (value >= 0b1000_0000_0000_0000) + { + throw new ArgumentException("BitPacked ushorts must be <= 15 bits"); + } +#endif + + if (value <= 0b0111_1111) + { + if (!writer.TryBeginWriteInternal(1)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteByte((byte)(value << 1)); + return; + } + + if (!writer.TryBeginWriteInternal(2)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteValue((ushort)((value << 1) | 0b1)); + } + + /// + /// Writes a 29-bit signed int to the buffer in a bit-encoded packed format. + /// The first two bits indicate whether the value is 1, 2, 3, or 4 bytes. + /// The sign bit takes up another bit. + /// That leaves 29 bits for the value. + /// A value greater than 2^29-1 or less than -2^29 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its three + /// most significant bits after zig-zag encoding. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(ref FastBufferWriter writer, int value) => WriteValueBitPacked(ref writer, (uint)Arithmetic.ZigZagEncode(value)); + + /// + /// Writes a 30-bit unsigned int to the buffer in a bit-encoded packed format. + /// The first two bits indicate whether the value is 1, 2, 3, or 4 bytes. + /// That leaves 30 bits for the value. + /// A value greater than 2^30-1 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its two + /// most significant bits. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(ref FastBufferWriter writer, uint value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (value >= 0b0100_0000_0000_0000_0000_0000_0000_0000) + { + throw new ArgumentException("BitPacked uints must be <= 30 bits"); + } +#endif + value <<= 2; + var numBytes = BitCounter.GetUsedByteCount(value); + if (!writer.TryBeginWriteInternal(numBytes)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WritePartialValue(value | (uint)(numBytes - 1), numBytes); + } + + /// + /// Writes a 60-bit signed long to the buffer in a bit-encoded packed format. + /// The first three bits indicate whether the value is 1, 2, 3, 4, 5, 6, 7, or 8 bytes. + /// The sign bit takes up another bit. + /// That leaves 60 bits for the value. + /// A value greater than 2^60-1 or less than -2^60 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its four + /// most significant bits after zig-zag encoding. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(ref FastBufferWriter writer, long value) => WriteValueBitPacked(ref writer, Arithmetic.ZigZagEncode(value)); + + /// + /// Writes a 61-bit unsigned long to the buffer in a bit-encoded packed format. + /// The first three bits indicate whether the value is 1, 2, 3, 4, 5, 6, 7, or 8 bytes. + /// That leaves 31 bits for the value. + /// A value greater than 2^61-1 will throw an exception in editor and development builds. + /// In release builds builds the exception is not thrown and the value is truncated by losing its three + /// most significant bits. + /// + /// The writer to write to + /// The value to pack + public static void WriteValueBitPacked(ref FastBufferWriter writer, ulong value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (value >= 0b0010_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000) + { + throw new ArgumentException("BitPacked ulongs must be <= 61 bits"); + } +#endif + value <<= 3; + var numBytes = BitCounter.GetUsedByteCount(value); + if (!writer.TryBeginWriteInternal(numBytes)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WritePartialValue(value | (uint)(numBytes - 1), numBytes); + } +#endif + #endregion + + #region Private Methods + private static void WriteUInt64Packed(ref FastBufferWriter writer, ulong value) + { + if (value <= 240) + { + writer.WriteByteSafe((byte)value); + return; + } + if (value <= 2287) + { + writer.WriteByteSafe((byte)(((value - 240) >> 8) + 241)); + writer.WriteByteSafe((byte)(value - 240)); + return; + } + var writeBytes = BitCounter.GetUsedByteCount(value); + + if (!writer.TryBeginWriteInternal(writeBytes + 1)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteByte((byte)(247 + writeBytes)); + writer.WritePartialValue(value, writeBytes); + } + + // Looks like the same code as WriteUInt64Packed? + // It's actually different because it will call the more efficient 32-bit version + // of BytewiseUtility.GetUsedByteCount(). + private static void WriteUInt32Packed(ref FastBufferWriter writer, uint value) + { + if (value <= 240) + { + writer.WriteByteSafe((byte)value); + return; + } + if (value <= 2287) + { + writer.WriteByteSafe((byte)(((value - 240) >> 8) + 241)); + writer.WriteByteSafe((byte)(value - 240)); + return; + } + var writeBytes = BitCounter.GetUsedByteCount(value); + + if (!writer.TryBeginWriteInternal(writeBytes + 1)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteByte((byte)(247 + writeBytes)); + writer.WritePartialValue(value, writeBytes); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe uint ToUint(T value) where T : unmanaged + { + uint* asUint = (uint*)&value; + return *asUint; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ulong ToUlong(T value) where T : unmanaged + { + ulong* asUlong = (ulong*)&value; + return *asUlong; + } + #endregion + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BytePacker.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/BytePacker.cs.meta new file mode 100644 index 0000000000..dfb5e75613 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BytePacker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3ec13587ae68cb49b82af8612d47698 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/ByteUnpacker.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/ByteUnpacker.cs new file mode 100644 index 0000000000..857829e126 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/ByteUnpacker.cs @@ -0,0 +1,697 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; + +namespace Unity.Netcode +{ + public static class ByteUnpacker + { + #region Managed TypePacking + + /// + /// Reads a boxed object in a packed format + /// Named differently from other ReadValuePacked methods to avoid accidental boxing + /// Don't use this method unless you have no other choice. + /// + /// The reader to read from + /// The object to read + /// The type of the object to read (i.e., typeof(int)) + /// + /// If true, reads a byte indicating whether or not the object is null. + /// Should match the way the object was written. + /// + public static void ReadObjectPacked(ref FastBufferReader reader, out object value, Type type, bool isNullable = false) + { +#if UNITY_NETCODE_DEBUG_NO_PACKING + reader.ReadObject(out value, type, isNullable); + return; +#endif + if (isNullable || type.IsNullable()) + { + reader.ReadValueSafe(out bool isNull); + + if (isNull) + { + value = null; + return; + } + } + + var hasDeserializer = SerializationTypeTable.DeserializersPacked.TryGetValue(type, out var deserializer); + if (hasDeserializer) + { + deserializer(ref reader, out value); + return; + } + + if (type.IsArray && type.HasElementType) + { + ReadValuePacked(ref reader, out int length); + + var arr = Array.CreateInstance(type.GetElementType(), length); + + for (int i = 0; i < length; i++) + { + ReadObjectPacked(ref reader, out object item, type.GetElementType()); + arr.SetValue(item, i); + } + + value = arr; + return; + } + + if (type.IsEnum) + { + switch (Type.GetTypeCode(type)) + { + case TypeCode.Boolean: + ReadValuePacked(ref reader, out byte boolVal); + value = Enum.ToObject(type, boolVal != 0); + return; + case TypeCode.Char: + ReadValuePacked(ref reader, out char charVal); + value = Enum.ToObject(type, charVal); + return; + case TypeCode.SByte: + ReadValuePacked(ref reader, out byte sbyteVal); + value = Enum.ToObject(type, sbyteVal); + return; + case TypeCode.Byte: + ReadValuePacked(ref reader, out byte byteVal); + value = Enum.ToObject(type, byteVal); + return; + case TypeCode.Int16: + ReadValuePacked(ref reader, out short shortVal); + value = Enum.ToObject(type, shortVal); + return; + case TypeCode.UInt16: + ReadValuePacked(ref reader, out ushort ushortVal); + value = Enum.ToObject(type, ushortVal); + return; + case TypeCode.Int32: + ReadValuePacked(ref reader, out int intVal); + value = Enum.ToObject(type, intVal); + return; + case TypeCode.UInt32: + ReadValuePacked(ref reader, out uint uintVal); + value = Enum.ToObject(type, uintVal); + return; + case TypeCode.Int64: + ReadValuePacked(ref reader, out long longVal); + value = Enum.ToObject(type, longVal); + return; + case TypeCode.UInt64: + ReadValuePacked(ref reader, out ulong ulongVal); + value = Enum.ToObject(type, ulongVal); + return; + } + } + + if (type == typeof(GameObject)) + { + reader.ReadValueSafe(out GameObject go); + value = go; + return; + } + + if (type == typeof(NetworkObject)) + { + reader.ReadValueSafe(out NetworkObject no); + value = no; + return; + } + + if (typeof(NetworkBehaviour).IsAssignableFrom(type)) + { + reader.ReadValueSafe(out NetworkBehaviour nb); + value = nb; + return; + } + /*if (value is INetworkSerializable) + { + //TODO ((INetworkSerializable)value).NetworkSerialize(new NetworkSerializer(this)); + return; + }*/ + + throw new ArgumentException($"{nameof(FastBufferReader)} cannot read type {type.Name} - it does not implement {nameof(INetworkSerializable)}"); + } + #endregion + + #region Unmanaged Type Packing + +#if UNITY_NETCODE_DEBUG_NO_PACKING + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValuePacked(ref FastBufferReader reader, out T value) where T: unmanaged => reader.ReadValueSafe(out value); +#else + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void ReadValuePacked(ref FastBufferReader reader, out TEnum value) where TEnum : unmanaged, Enum + { + switch (sizeof(TEnum)) + { + case sizeof(int): + ReadValuePacked(ref reader, out int asInt); + value = *(TEnum*)&asInt; + break; + case sizeof(byte): + ReadValuePacked(ref reader, out byte asByte); + value = *(TEnum*)&asByte; + break; + case sizeof(short): + ReadValuePacked(ref reader, out short asShort); + value = *(TEnum*)&asShort; + break; + case sizeof(long): + ReadValuePacked(ref reader, out long asLong); + value = *(TEnum*)&asLong; + break; + default: + throw new InvalidOperationException("Enum is a size that cannot exist?!"); + } + } + + /// + /// Read single-precision floating point value from the stream as a varint + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out float value) + { + ReadUInt32Packed(ref reader, out uint asUInt); + value = ToSingle(asUInt); + } + + /// + /// Read double-precision floating point value from the stream as a varint + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out double value) + { + ReadUInt64Packed(ref reader, out ulong asULong); + value = ToDouble(asULong); + } + + /// + /// Read a byte from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out byte value) => reader.ReadByteSafe(out value); + + /// + /// Read a signed byte from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out sbyte value) + { + reader.ReadByteSafe(out byte byteVal); + value = (sbyte)byteVal; + } + + /// + /// Read a boolean from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out bool value) => reader.ReadValueSafe(out value); + + + /// + /// Read an usigned short (Int16) as a varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out short value) + { + ReadUInt32Packed(ref reader, out uint readValue); + value = (short)Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read an unsigned short (UInt16) as a varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out ushort value) + { + ReadUInt32Packed(ref reader, out uint readValue); + value = (ushort)readValue; + } + + /// + /// Read a two-byte character as a varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out char c) + { + ReadUInt32Packed(ref reader, out uint readValue); + c = (char)readValue; + } + + /// + /// Read a signed int (Int32) as a ZigZag encoded varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out int value) + { + ReadUInt32Packed(ref reader, out uint readValue); + value = (int)Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read an unsigned int (UInt32) from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out uint value) => ReadUInt32Packed(ref reader, out value); + + /// + /// Read an unsigned long (UInt64) from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out ulong value) => ReadUInt64Packed(ref reader, out value); + + /// + /// Read a signed long (Int64) as a ZigZag encoded varint from the stream. + /// + /// The reader to read from + /// Value to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out long value) + { + ReadUInt64Packed(ref reader, out ulong readValue); + value = Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Convenience method that reads two packed Vector3 from the ray from the stream + /// + /// The reader to read from + /// Ray to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out Ray ray) + { + ReadValuePacked(ref reader, out Vector3 origin); + ReadValuePacked(ref reader, out Vector3 direction); + ray = new Ray(origin, direction); + } + + /// + /// Convenience method that reads two packed Vector2 from the ray from the stream + /// + /// The reader to read from + /// Ray2D to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out Ray2D ray2d) + { + ReadValuePacked(ref reader, out Vector2 origin); + ReadValuePacked(ref reader, out Vector2 direction); + ray2d = new Ray2D(origin, direction); + } + + /// + /// Convenience method that reads four varint floats from the color from the stream + /// + /// The reader to read from + /// Color to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out Color color) + { + color = new Color(); + ReadValuePacked(ref reader, out color.r); + ReadValuePacked(ref reader, out color.g); + ReadValuePacked(ref reader, out color.b); + ReadValuePacked(ref reader, out color.a); + } + + /// + /// Convenience method that reads four varint floats from the color from the stream + /// + /// The reader to read from + /// Color to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out Color32 color) + { + color = new Color32(); + ReadValuePacked(ref reader, out color.r); + ReadValuePacked(ref reader, out color.g); + ReadValuePacked(ref reader, out color.b); + ReadValuePacked(ref reader, out color.a); + } + + /// + /// Convenience method that reads two varint floats from the vector from the stream + /// + /// The reader to read from + /// Vector to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out Vector2 vector2) + { + vector2 = new Vector2(); + ReadValuePacked(ref reader, out vector2.x); + ReadValuePacked(ref reader, out vector2.y); + } + + /// + /// Convenience method that reads three varint floats from the vector from the stream + /// + /// The reader to read from + /// Vector to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out Vector3 vector3) + { + vector3 = new Vector3(); + ReadValuePacked(ref reader, out vector3.x); + ReadValuePacked(ref reader, out vector3.y); + ReadValuePacked(ref reader, out vector3.z); + } + + /// + /// Convenience method that reads four varint floats from the vector from the stream + /// + /// The reader to read from + /// Vector to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out Vector4 vector4) + { + vector4 = new Vector4(); + ReadValuePacked(ref reader, out vector4.x); + ReadValuePacked(ref reader, out vector4.y); + ReadValuePacked(ref reader, out vector4.z); + ReadValuePacked(ref reader, out vector4.w); + } + + /// + /// Reads the rotation from the stream. + /// + /// The reader to read from + /// Rotation to read + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadValuePacked(ref FastBufferReader reader, out Quaternion rotation) + { + rotation = new Quaternion(); + ReadValuePacked(ref reader, out rotation.x); + ReadValuePacked(ref reader, out rotation.y); + ReadValuePacked(ref reader, out rotation.z); + ReadValuePacked(ref reader, out rotation.w); + } + + /// + /// Reads a string in a packed format + /// + /// The reader to read from + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe void ReadValuePacked(ref FastBufferReader reader, out string s) + { + ReadValuePacked(ref reader, out uint length); + s = "".PadRight((int)length); + int target = s.Length; + fixed (char* c = s) + { + for (int i = 0; i < target; ++i) + { + ReadValuePacked(ref reader, out c[i]); + } + } + } +#endif + #endregion + + #region Bit Packing + +#if UNITY_NETCODE_DEBUG_NO_PACKING + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ReadValueBitPacked(ref FastBufferReader reader, T value) where T: unmanaged => reader.ReadValueSafe(out value); +#else + /// + /// Read a bit-packed 14-bit signed short from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static void ReadValueBitPacked(ref FastBufferReader reader, out short value) + { + ReadValueBitPacked(ref reader, out ushort readValue); + value = (short)Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read a bit-packed 15-bit unsigned short from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static unsafe void ReadValueBitPacked(ref FastBufferReader reader, out ushort value) + { + ushort returnValue = 0; + byte* ptr = ((byte*)&returnValue); + byte* data = reader.GetUnsafePtrAtCurrentPosition(); + int numBytes = (data[0] & 0b1) + 1; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.MarkBytesRead(numBytes); + switch (numBytes) + { + case 1: + *ptr = *data; + break; + case 2: + *ptr = *data; + *(ptr + 1) = *(data + 1); + break; + default: + throw new InvalidOperationException("Could not read bit-packed value: impossible byte count"); + } + + value = (ushort)(returnValue >> 1); + } + + /// + /// Read a bit-packed 29-bit signed int from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static void ReadValueBitPacked(ref FastBufferReader reader, out int value) + { + ReadValueBitPacked(ref reader, out uint readValue); + value = (int)Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read a bit-packed 30-bit unsigned int from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static unsafe void ReadValueBitPacked(ref FastBufferReader reader, out uint value) + { + uint returnValue = 0; + byte* ptr = ((byte*)&returnValue); + byte* data = reader.GetUnsafePtrAtCurrentPosition(); + int numBytes = (data[0] & 0b11) + 1; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.MarkBytesRead(numBytes); + switch (numBytes) + { + case 1: + *ptr = *data; + break; + case 2: + *ptr = *data; + *(ptr + 1) = *(data + 1); + break; + case 3: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + break; + case 4: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + break; + } + + value = returnValue >> 2; + } + + /// + /// Read a bit-packed 60-bit signed long from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static void ReadValueBitPacked(ref FastBufferReader reader, out long value) + { + ReadValueBitPacked(ref reader, out ulong readValue); + value = Arithmetic.ZigZagDecode(readValue); + } + + /// + /// Read a bit-packed 61-bit signed long from the stream. + /// See BytePacker.cs for a description of the format. + /// + /// The reader to read from + /// The value to read + public static unsafe void ReadValueBitPacked(ref FastBufferReader reader, out ulong value) + { + ulong returnValue = 0; + byte* ptr = ((byte*)&returnValue); + byte* data = reader.GetUnsafePtrAtCurrentPosition(); + int numBytes = (data[0] & 0b111) + 1; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.MarkBytesRead(numBytes); + switch (numBytes) + { + case 1: + *ptr = *data; + break; + case 2: + *ptr = *data; + *(ptr + 1) = *(data + 1); + break; + case 3: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + break; + case 4: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + break; + case 5: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + *(ptr + 4) = *(data + 4); + break; + case 6: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + *(ptr + 4) = *(data + 4); + *(ptr + 5) = *(data + 5); + break; + case 7: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + *(ptr + 4) = *(data + 4); + *(ptr + 5) = *(data + 5); + *(ptr + 6) = *(data + 6); + break; + case 8: + *ptr = *data; + *(ptr + 1) = *(data + 1); + *(ptr + 2) = *(data + 2); + *(ptr + 3) = *(data + 3); + *(ptr + 4) = *(data + 4); + *(ptr + 5) = *(data + 5); + *(ptr + 6) = *(data + 6); + *(ptr + 7) = *(data + 7); + break; + } + + value = returnValue >> 3; + } +#endif + #endregion + + #region Private Methods + private static void ReadUInt64Packed(ref FastBufferReader reader, out ulong value) + { + reader.ReadByteSafe(out byte firstByte); + if (firstByte <= 240) + { + value = firstByte; + return; + } + + if (firstByte <= 248) + { + reader.ReadByteSafe(out byte secondByte); + value = 240UL + ((firstByte - 241UL) << 8) + secondByte; + return; + } + + var numBytes = firstByte - 247; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.ReadPartialValue(out value, numBytes); + } + + private static void ReadUInt32Packed(ref FastBufferReader reader, out uint value) + { + reader.ReadByteSafe(out byte firstByte); + if (firstByte <= 240) + { + value = firstByte; + return; + } + + if (firstByte <= 248) + { + reader.ReadByteSafe(out byte secondByte); + value = 240U + ((firstByte - 241U) << 8) + secondByte; + return; + } + + var numBytes = firstByte - 247; + if (!reader.TryBeginReadInternal(numBytes)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + reader.ReadPartialValue(out value, numBytes); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe float ToSingle(T value) where T : unmanaged + { + float* asFloat = (float*)&value; + return *asFloat; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe double ToDouble(T value) where T : unmanaged + { + double* asDouble = (double*)&value; + return *asDouble; + } + #endregion + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/ByteUnpacker.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/ByteUnpacker.cs.meta new file mode 100644 index 0000000000..f3784bcdb4 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/ByteUnpacker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 73484532f9cd8a7418b6a7ac770df851 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs new file mode 100644 index 0000000000..add6a0d487 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs @@ -0,0 +1,742 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + public struct FastBufferReader : IDisposable + { + internal readonly unsafe byte* BufferPointer; + internal int PositionInternal; + internal readonly int LengthInternal; + private readonly Allocator m_Allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + internal int AllowedReadMark; + private bool m_InBitwiseContext; +#endif + + /// + /// Get the current read position + /// + public int Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => PositionInternal; + } + + /// + /// Get the total length of the buffer + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => LengthInternal; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void CommitBitwiseReads(int amount) + { + PositionInternal += amount; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_InBitwiseContext = false; +#endif + } + + public unsafe FastBufferReader(NativeArray buffer, Allocator allocator, int length = -1, int offset = 0) + { + LengthInternal = Math.Max(1, length == -1 ? buffer.Length : length); + if (allocator == Allocator.None) + { + BufferPointer = (byte*)buffer.GetUnsafePtr() + offset; + } + else + { + void* bufferPtr = UnsafeUtility.Malloc(LengthInternal, UnsafeUtility.AlignOf(), allocator); + UnsafeUtility.MemCpy(bufferPtr, (byte*)buffer.GetUnsafePtr() + offset, LengthInternal); + BufferPointer = (byte*)bufferPtr; + } + PositionInternal = offset; + m_Allocator = allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedReadMark = 0; + m_InBitwiseContext = false; +#endif + } + + /// + /// Create a FastBufferReader from an ArraySegment. + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// The buffer to copy from + /// The allocator to use + /// The number of bytes to copy (all if this is -1) + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(ArraySegment buffer, Allocator allocator, int length = -1, int offset = 0) + { + LengthInternal = Math.Max(1, length == -1 ? (buffer.Count - offset) : length); + if (allocator == Allocator.None) + { + throw new NotSupportedException("Allocator.None cannot be used with managed source buffers."); + } + else + { + void* bufferPtr = UnsafeUtility.Malloc(LengthInternal, UnsafeUtility.AlignOf(), allocator); + fixed (byte* data = buffer.Array) + { + UnsafeUtility.MemCpy(bufferPtr, data + offset, LengthInternal); + } + + BufferPointer = (byte*)bufferPtr; + } + + PositionInternal = 0; + m_Allocator = allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedReadMark = 0; + m_InBitwiseContext = false; +#endif + } + + /// + /// Create a FastBufferReader from an existing byte array. + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// The buffer to copy from + /// The allocator to use + /// The number of bytes to copy (all if this is -1) + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(byte[] buffer, Allocator allocator, int length = -1, int offset = 0) + { + LengthInternal = Math.Max(1, length == -1 ? (buffer.Length - offset) : length); + if (allocator == Allocator.None) + { + throw new NotSupportedException("Allocator.None cannot be used with managed source buffers."); + } + else + { + void* bufferPtr = UnsafeUtility.Malloc(LengthInternal, UnsafeUtility.AlignOf(), allocator); + fixed (byte* data = buffer) + { + UnsafeUtility.MemCpy(bufferPtr, data + offset, LengthInternal); + } + + BufferPointer = (byte*)bufferPtr; + } + + PositionInternal = 0; + m_Allocator = allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedReadMark = 0; + m_InBitwiseContext = false; +#endif + } + + /// + /// Create a FastBufferReader from an existing byte buffer. + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// The buffer to copy from + /// The allocator to use + /// The number of bytes to copy + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(byte* buffer, Allocator allocator, int length, int offset = 0) + { + LengthInternal = Math.Max(1, length); + if (allocator == Allocator.None) + { + BufferPointer = buffer + offset; + } + else + { + void* bufferPtr = UnsafeUtility.Malloc(LengthInternal, UnsafeUtility.AlignOf(), allocator); + UnsafeUtility.MemCpy(bufferPtr, buffer + offset, LengthInternal); + BufferPointer = (byte*)bufferPtr; + } + + PositionInternal = 0; + m_Allocator = allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedReadMark = 0; + m_InBitwiseContext = false; +#endif + } + + /// + /// Create a FastBufferReader from a FastBufferWriter. + /// A new buffer will be created using the given allocator and the value will be copied in. + /// FastBufferReader will then own the data. + /// + /// The writer to copy from + /// The allocator to use + /// The number of bytes to copy (all if this is -1) + /// The offset of the buffer to start copying from + public unsafe FastBufferReader(ref FastBufferWriter writer, Allocator allocator, int length = -1, int offset = 0) + { + LengthInternal = Math.Max(1, length == -1 ? writer.Length : length); + void* bufferPtr = UnsafeUtility.Malloc(LengthInternal, UnsafeUtility.AlignOf(), allocator); + UnsafeUtility.MemCpy(bufferPtr, writer.GetUnsafePtr() + offset, LengthInternal); + BufferPointer = (byte*)bufferPtr; + PositionInternal = 0; + m_Allocator = allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedReadMark = 0; + m_InBitwiseContext = false; +#endif + } + + /// + /// Frees the allocated buffer + /// + public unsafe void Dispose() + { + UnsafeUtility.Free(BufferPointer, m_Allocator); + } + + /// + /// Move the read position in the stream + /// + /// Absolute value to move the position to, truncated to Length + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Seek(int where) + { + PositionInternal = Math.Min(Length, where); + } + + /// + /// Mark that some bytes are going to be read via GetUnsafePtr(). + /// + /// Amount that will be read + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void MarkBytesRead(int amount) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (PositionInternal + amount > AllowedReadMark) + { + throw new OverflowException("Attempted to read without first calling TryBeginRead()"); + } +#endif + PositionInternal += amount; + } + + /// + /// Retrieve a BitReader to be able to perform bitwise operations on the buffer. + /// No bytewise operations can be performed on the buffer until bitReader.Dispose() has been called. + /// At the end of the operation, FastBufferReader will remain byte-aligned. + /// + /// A BitReader + public BitReader EnterBitwiseContext() + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_InBitwiseContext = true; +#endif + return new BitReader(ref this); + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be reading multiple fields back-to-back and you know the total size, + /// you can call TryBeginRead() once on the total size, and then follow it with calls to + /// ReadValue() instead of ReadValueSafe() for faster serialization. + /// + /// Unsafe read operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using TryBeginRead(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using TryBeginRead is to avoid bounds checking in the following + /// operations in release builds. + /// + /// Amount of bytes to read + /// True if the read is allowed, false otherwise + /// If called while in a bitwise context + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryBeginRead(int bytes) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + if (PositionInternal + bytes > LengthInternal) + { + return false; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedReadMark = PositionInternal + bytes; +#endif + return true; + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be reading multiple fields back-to-back and you know the total size, + /// you can call TryBeginRead() once on the total size, and then follow it with calls to + /// ReadValue() instead of ReadValueSafe() for faster serialization. + /// + /// Unsafe read operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using TryBeginRead(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using TryBeginRead is to avoid bounds checking in the following + /// operations in release builds. + /// + /// The value you want to read + /// True if the read is allowed, false otherwise + /// If called while in a bitwise context + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool TryBeginReadValue(in T value) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + int len = sizeof(T); + if (PositionInternal + len > LengthInternal) + { + return false; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedReadMark = PositionInternal + len; +#endif + return true; + } + + /// + /// Internal version of TryBeginRead. + /// Differs from TryBeginRead only in that it won't ever move the AllowedReadMark backward. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal bool TryBeginReadInternal(int bytes) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + if (PositionInternal + bytes > LengthInternal) + { + return false; + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (PositionInternal + bytes > AllowedReadMark) + { + AllowedReadMark = PositionInternal + bytes; + } +#endif + return true; + } + + /// + /// Returns an array representation of the underlying byte buffer. + /// !!Allocates a new array!! + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte[] ToArray() + { + byte[] ret = new byte[Length]; + fixed (byte* b = ret) + { + UnsafeUtility.MemCpy(b, BufferPointer, Length); + } + return ret; + } + + /// + /// Gets a direct pointer to the underlying buffer + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte* GetUnsafePtr() + { + return BufferPointer; + } + + /// + /// Gets a direct pointer to the underlying buffer at the current read position + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte* GetUnsafePtrAtCurrentPosition() + { + return BufferPointer + PositionInternal; + } + + /// + /// Reads a string + /// NOTE: ALLOCATES + /// + /// Stores the read string + /// Whether or not to use one byte per character. This will only allow ASCII + public unsafe void ReadValue(out string s, bool oneByteChars = false) + { + ReadValue(out uint length); + s = "".PadRight((int)length); + int target = s.Length; + fixed (char* native = s) + { + if (oneByteChars) + { + for (int i = 0; i < target; ++i) + { + ReadByte(out byte b); + native[i] = (char)b; + } + } + else + { + ReadBytes((byte*)native, target * sizeof(char)); + } + } + } + + /// + /// Reads a string. + /// NOTE: ALLOCATES + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Stores the read string + /// Whether or not to use one byte per character. This will only allow ASCII + public unsafe void ReadValueSafe(out string s, bool oneByteChars = false) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(sizeof(uint))) + { + throw new OverflowException("Reading past the end of the buffer"); + } + + ReadValue(out uint length); + + if (!TryBeginReadInternal((int)length * (oneByteChars ? 1 : sizeof(char)))) + { + throw new OverflowException("Reading past the end of the buffer"); + } + s = "".PadRight((int)length); + int target = s.Length; + fixed (char* native = s) + { + if (oneByteChars) + { + for (int i = 0; i < target; ++i) + { + ReadByte(out byte b); + native[i] = (char)b; + } + } + else + { + ReadBytes((byte*)native, target * sizeof(char)); + } + } + } + + /// + /// Writes an unmanaged array + /// NOTE: ALLOCATES + /// + /// Stores the read array + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValue(out T[] array) where T : unmanaged + { + ReadValue(out int sizeInTs); + int sizeInBytes = sizeInTs * sizeof(T); + array = new T[sizeInTs]; + fixed (T* native = array) + { + byte* bytes = (byte*)(native); + ReadBytes(bytes, sizeInBytes); + } + } + + /// + /// Reads an unmanaged array + /// NOTE: ALLOCATES + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Stores the read array + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValueSafe(out T[] array) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(sizeof(int))) + { + throw new OverflowException("Writing past the end of the buffer"); + } + ReadValue(out int sizeInTs); + int sizeInBytes = sizeInTs * sizeof(T); + if (!TryBeginReadInternal(sizeInBytes)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + array = new T[sizeInTs]; + fixed (T* native = array) + { + byte* bytes = (byte*)(native); + ReadBytes(bytes, sizeInBytes); + } + } + + /// + /// Read a partial value. The value is zero-initialized and then the specified number of bytes is read into it. + /// + /// Value to read + /// Number of bytes + /// Offset into the value to write the bytes + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadPartialValue(out T value, int bytesToRead, int offsetBytes = 0) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (PositionInternal + bytesToRead > AllowedReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + } +#endif + + var val = new T(); + byte* ptr = ((byte*)&val) + offsetBytes; + byte* bufferPointer = BufferPointer + PositionInternal; + UnsafeUtility.MemCpy(ptr, bufferPointer, bytesToRead); + + PositionInternal += bytesToRead; + value = val; + } + + /// + /// Read a byte to the stream. + /// + /// Stores the read value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadByte(out byte value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (PositionInternal + 1 > AllowedReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + } +#endif + value = BufferPointer[PositionInternal++]; + } + + /// + /// Read a byte to the stream. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Stores the read value + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadByteSafe(out byte value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(1)) + { + throw new OverflowException("Reading past the end of the buffer"); + } + value = BufferPointer[PositionInternal++]; + } + + /// + /// Read multiple bytes to the stream + /// + /// Pointer to the destination buffer + /// Number of bytes to read - MUST BE <= BUFFER SIZE + /// Offset of the byte buffer to store into + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBytes(byte* value, int size, int offset = 0) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (PositionInternal + size > AllowedReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + } +#endif + UnsafeUtility.MemCpy(value + offset, (BufferPointer + PositionInternal), size); + PositionInternal += size; + } + + /// + /// Read multiple bytes to the stream + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Pointer to the destination buffer + /// Number of bytes to read - MUST BE <= BUFFER SIZE + /// Offset of the byte buffer to store into + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBytesSafe(byte* value, int size, int offset = 0) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(size)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + UnsafeUtility.MemCpy(value + offset, (BufferPointer + PositionInternal), size); + PositionInternal += size; + } + + /// + /// Read multiple bytes from the stream + /// + /// Pointer to the destination buffer + /// Number of bytes to read - MUST BE <= BUFFER SIZE + /// Offset of the byte buffer to store into + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBytes(ref byte[] value, int size, int offset = 0) + { + fixed (byte* ptr = value) + { + ReadBytes(ptr, size, offset); + } + } + + /// + /// Read multiple bytes from the stream + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// Pointer to the destination buffer + /// Number of bytes to read - MUST BE <= BUFFER SIZE + /// Offset of the byte buffer to store into + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadBytesSafe(ref byte[] value, int size, int offset = 0) + { + fixed (byte* ptr = value) + { + ReadBytesSafe(ptr, size, offset); + } + } + + /// + /// Read a value of any unmanaged type to the buffer. + /// It will be copied from the buffer exactly as it existed in memory on the writing end. + /// + /// The read value + /// Any unmanaged type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValue(out T value) where T : unmanaged + { + int len = sizeof(T); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } + if (PositionInternal + len > AllowedReadMark) + { + throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); + } +#endif + + fixed (T* ptr = &value) + { + UnsafeUtility.MemCpy((byte*)ptr, BufferPointer + PositionInternal, len); + } + PositionInternal += len; + } + + /// + /// Read a value of any unmanaged type to the buffer. + /// It will be copied from the buffer exactly as it existed in memory on the writing end. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// The read value + /// Any unmanaged type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void ReadValueSafe(out T value) where T : unmanaged + { + int len = sizeof(T); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferReader in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginReadInternal(len)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + + + fixed (T* ptr = &value) + { + UnsafeUtility.MemCpy((byte*)ptr, BufferPointer + PositionInternal, len); + } + PositionInternal += len; + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs.meta new file mode 100644 index 0000000000..3667f738aa --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5479786a4b1f57648a0fe56bd37a823b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReaderExtensions.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReaderExtensions.cs new file mode 100644 index 0000000000..02f567a9df --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReaderExtensions.cs @@ -0,0 +1,286 @@ + +using System; +using UnityEngine; + +namespace Unity.Netcode +{ + public static class FastBufferReaderExtensions + { + + /// + /// Reads a boxed object in a standard format + /// Named differently from other ReadValue methods to avoid accidental boxing + /// + /// The object to read + /// The type to be read + /// + /// If true, reads a byte indicating whether or not the object is null. + /// Should match the way the object was written. + /// + public static void ReadObject(this ref FastBufferReader reader, out object value, Type type, bool isNullable = false) + { + if (isNullable || type.IsNullable()) + { + reader.ReadValueSafe(out bool isNull); + + if (isNull) + { + value = null; + return; + } + } + + var hasDeserializer = SerializationTypeTable.Deserializers.TryGetValue(type, out var deserializer); + if (hasDeserializer) + { + deserializer(ref reader, out value); + return; + } + + if (type.IsArray && type.HasElementType) + { + reader.ReadValueSafe(out int length); + + var arr = Array.CreateInstance(type.GetElementType(), length); + + for (int i = 0; i < length; i++) + { + reader.ReadObject(out object item, type.GetElementType()); + arr.SetValue(item, i); + } + + value = arr; + return; + } + + if (type.IsEnum) + { + switch (Type.GetTypeCode(type)) + { + case TypeCode.Boolean: + reader.ReadValueSafe(out byte boolVal); + value = Enum.ToObject(type, boolVal != 0); + return; + case TypeCode.Char: + reader.ReadValueSafe(out char charVal); + value = Enum.ToObject(type, charVal); + return; + case TypeCode.SByte: + reader.ReadValueSafe(out sbyte sbyteVal); + value = Enum.ToObject(type, sbyteVal); + return; + case TypeCode.Byte: + reader.ReadValueSafe(out byte byteVal); + value = Enum.ToObject(type, byteVal); + return; + case TypeCode.Int16: + reader.ReadValueSafe(out short shortVal); + value = Enum.ToObject(type, shortVal); + return; + case TypeCode.UInt16: + reader.ReadValueSafe(out ushort ushortVal); + value = Enum.ToObject(type, ushortVal); + return; + case TypeCode.Int32: + reader.ReadValueSafe(out int intVal); + value = Enum.ToObject(type, intVal); + return; + case TypeCode.UInt32: + reader.ReadValueSafe(out uint uintVal); + value = Enum.ToObject(type, uintVal); + return; + case TypeCode.Int64: + reader.ReadValueSafe(out long longVal); + value = Enum.ToObject(type, longVal); + return; + case TypeCode.UInt64: + reader.ReadValueSafe(out ulong ulongVal); + value = Enum.ToObject(type, ulongVal); + return; + } + } + + if (type == typeof(GameObject)) + { + reader.ReadValueSafe(out GameObject go); + value = go; + return; + } + + if (type == typeof(NetworkObject)) + { + reader.ReadValueSafe(out NetworkObject no); + value = no; + return; + } + + if (typeof(NetworkBehaviour).IsAssignableFrom(type)) + { + reader.ReadValueSafe(out NetworkBehaviour nb); + value = nb; + return; + } + /*if (value is INetworkSerializable) + { + //TODO ((INetworkSerializable)value).NetworkSerialize(new NetworkSerializer(this)); + return; + }*/ + + throw new ArgumentException($"{nameof(FastBufferReader)} cannot read type {type.Name} - it does not implement {nameof(INetworkSerializable)}"); + } + + /// + /// Read an INetworkSerializable + /// + /// INetworkSerializable instance + /// + /// + public static void ReadNetworkSerializable(this ref FastBufferReader reader, out T value) where T : INetworkSerializable + { + throw new NotImplementedException(); + } + + /// + /// Read a GameObject + /// + /// value to read + public static void ReadValue(this ref FastBufferReader reader, out GameObject value) + { + reader.ReadValue(out ulong networkObjectId); + + if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject networkObject)) + { + value = networkObject.gameObject; + return; + } + + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(FastBufferReader)} cannot find the {nameof(GameObject)} sent in the {nameof(NetworkSpawnManager.SpawnedObjects)} list, it may have been destroyed. {nameof(networkObjectId)}: {networkObjectId}"); + } + + value = null; + } + + /// + /// Read a GameObject + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// value to read + 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; + } + + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(FastBufferReader)} cannot find the {nameof(GameObject)} sent in the {nameof(NetworkSpawnManager.SpawnedObjects)} list, it may have been destroyed. {nameof(networkObjectId)}: {networkObjectId}"); + } + + value = null; + } + + /// + /// Read a NetworkObject + /// + /// value to read + public static void ReadValue(this ref FastBufferReader reader, out NetworkObject value) + { + reader.ReadValue(out ulong networkObjectId); + + if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject networkObject)) + { + value = networkObject; + return; + } + + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(FastBufferReader)} cannot find the {nameof(GameObject)} sent in the {nameof(NetworkSpawnManager.SpawnedObjects)} list, it may have been destroyed. {nameof(networkObjectId)}: {networkObjectId}"); + } + + value = null; + } + + /// + /// Read a NetworkObject + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// value to read + public static void ReadValueSafe(this ref FastBufferReader reader, out NetworkObject value) + { + reader.ReadValueSafe(out ulong networkObjectId); + + if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject networkObject)) + { + value = networkObject; + return; + } + + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(FastBufferReader)} cannot find the {nameof(GameObject)} sent in the {nameof(NetworkSpawnManager.SpawnedObjects)} list, it may have been destroyed. {nameof(networkObjectId)}: {networkObjectId}"); + } + + value = null; + } + + /// + /// Read a NetworkBehaviour + /// + /// value to read + public static void ReadValue(this ref FastBufferReader reader, out NetworkBehaviour value) + { + reader.ReadValue(out ulong networkObjectId); + reader.ReadValue(out ushort networkBehaviourId); + + if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject networkObject)) + { + value = networkObject.GetNetworkBehaviourAtOrderIndex(networkBehaviourId); + return; + } + + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(FastBufferReader)} cannot find the {nameof(NetworkBehaviour)} sent in the {nameof(NetworkSpawnManager.SpawnedObjects)} list, it may have been destroyed. {nameof(networkObjectId)}: {networkObjectId}"); + } + + value = null; + } + + /// + /// Read a NetworkBehaviour + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple reads at once by calling TryBeginRead. + /// + /// value to read + public static void ReadValueSafe(this ref FastBufferReader reader, out NetworkBehaviour value) + { + reader.ReadValueSafe(out ulong networkObjectId); + reader.ReadValueSafe(out ushort networkBehaviourId); + + if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(networkObjectId, out NetworkObject networkObject)) + { + value = networkObject.GetNetworkBehaviourAtOrderIndex(networkBehaviourId); + return; + } + + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(FastBufferReader)} cannot find the {nameof(NetworkBehaviour)} sent in the {nameof(NetworkSpawnManager.SpawnedObjects)} list, it may have been destroyed. {nameof(networkObjectId)}: {networkObjectId}"); + } + + value = null; + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReaderExtensions.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReaderExtensions.cs.meta new file mode 100644 index 0000000000..d00798303c --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReaderExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4dc2c9158967ec847895c0d9653283fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs new file mode 100644 index 0000000000..8e7ec57933 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs @@ -0,0 +1,772 @@ +using System; +using System.Runtime.CompilerServices; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +namespace Unity.Netcode +{ + public struct FastBufferWriter : IDisposable + { + internal unsafe byte* BufferPointer; + internal int PositionInternal; + private int m_Length; + internal int CapacityInternal; + internal readonly int MaxCapacityInternal; + private readonly Allocator m_Allocator; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + internal int AllowedWriteMark; + private bool m_InBitwiseContext; +#endif + + /// + /// The current write position + /// + public int Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => PositionInternal; + } + + /// + /// The current total buffer size + /// + public int Capacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => CapacityInternal; + } + + /// + /// The maximum possible total buffer size + /// + public int MaxCapacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => MaxCapacityInternal; + } + + /// + /// The total amount of bytes that have been written to the stream + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => PositionInternal > m_Length ? PositionInternal : m_Length; + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void CommitBitwiseWrites(int amount) + { + PositionInternal += amount; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_InBitwiseContext = false; +#endif + } + + /// + /// Create a FastBufferWriter. + /// + /// Size of the buffer to create + /// Allocator to use in creating it + /// Maximum size the buffer can grow to. If less than size, buffer cannot grow. + public unsafe FastBufferWriter(int size, Allocator allocator, int maxSize = -1) + { + void* buffer = UnsafeUtility.Malloc(size, UnsafeUtility.AlignOf(), allocator); +#if DEVELOPMENT_BUILD || UNITY_EDITOR + UnsafeUtility.MemSet(buffer, 0, size); +#endif + BufferPointer = (byte*)buffer; + PositionInternal = 0; + m_Length = 0; + CapacityInternal = size; + m_Allocator = allocator; + MaxCapacityInternal = maxSize < size ? size : maxSize; +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedWriteMark = 0; + m_InBitwiseContext = false; +#endif + } + + /// + /// Frees the allocated buffer + /// + public unsafe void Dispose() + { + UnsafeUtility.Free(BufferPointer, m_Allocator); + } + + /// + /// Move the write position in the stream. + /// Note that moving forward past the current length will extend the buffer's Length value even if you don't write. + /// + /// Absolute value to move the position to, truncated to Capacity + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Seek(int where) + { + // This avoids us having to synchronize length all the time. + // Writing things is a much more common operation than seeking + // or querying length. The length here is a high watermark of + // what's been written. So before we seek, if the current position + // is greater than the length, we update that watermark. + // When querying length later, we'll return whichever of the two + // values is greater, thus if we write past length, length increases + // because position increases, and if we seek backward, length remembers + // the position it was in. + // Seeking forward will not update the length. + where = Math.Min(where, CapacityInternal); + if (PositionInternal > m_Length && where < PositionInternal) + { + m_Length = PositionInternal; + } + PositionInternal = where; + } + + /// + /// Truncate the stream by setting Length to the specified value. + /// If Position is greater than the specified value, it will be moved as well. + /// + /// The value to truncate to. If -1, the current position will be used. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Truncate(int where = -1) + { + if (where == -1) + { + where = Position; + } + + if (PositionInternal > where) + { + PositionInternal = where; + } + if (m_Length > where) + { + m_Length = where; + } + } + + /// + /// Retrieve a BitWriter to be able to perform bitwise operations on the buffer. + /// No bytewise operations can be performed on the buffer until bitWriter.Dispose() has been called. + /// At the end of the operation, FastBufferWriter will remain byte-aligned. + /// + /// A BitWriter + public BitWriter EnterBitwiseContext() + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + m_InBitwiseContext = true; +#endif + return new BitWriter(ref this); + } + + internal unsafe void Grow(int additionalSizeRequired) + { + var desiredSize = CapacityInternal * 2; + while (desiredSize < Position + additionalSizeRequired) + { + desiredSize *= 2; + } + var newSize = Math.Min(desiredSize, MaxCapacityInternal); + void* buffer = UnsafeUtility.Malloc(newSize, UnsafeUtility.AlignOf(), m_Allocator); +#if DEVELOPMENT_BUILD || UNITY_EDITOR + UnsafeUtility.MemSet(buffer, 0, newSize); +#endif + UnsafeUtility.MemCpy(buffer, BufferPointer, Length); + UnsafeUtility.Free(BufferPointer, m_Allocator); + BufferPointer = (byte*)buffer; + CapacityInternal = newSize; + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be writing multiple fields back-to-back and you know the total size, + /// you can call TryBeginWrite() once on the total size, and then follow it with calls to + /// WriteValue() instead of WriteValueSafe() for faster serialization. + /// + /// Unsafe write operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using TryBeginWrite(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using TryBeginWrite is to avoid bounds checking in the following + /// operations in release builds. + /// + /// Amount of bytes to write + /// True if the write is allowed, false otherwise + /// If called while in a bitwise context + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryBeginWrite(int bytes) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + if (PositionInternal + bytes > CapacityInternal) + { + if (PositionInternal + bytes > MaxCapacityInternal) + { + return false; + } + if (CapacityInternal < MaxCapacityInternal) + { + Grow(bytes); + } + else + { + return false; + } + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedWriteMark = PositionInternal + bytes; +#endif + return true; + } + + /// + /// Allows faster serialization by batching bounds checking. + /// When you know you will be writing multiple fields back-to-back and you know the total size, + /// you can call TryBeginWrite() once on the total size, and then follow it with calls to + /// WriteValue() instead of WriteValueSafe() for faster serialization. + /// + /// Unsafe write operations will throw OverflowException in editor and development builds if you + /// go past the point you've marked using TryBeginWrite(). In release builds, OverflowException will not be thrown + /// for performance reasons, since the point of using TryBeginWrite is to avoid bounds checking in the following + /// operations in release builds. Instead, attempting to write past the marked position in release builds + /// will write to random memory and cause undefined behavior, likely including instability and crashes. + /// + /// The value you want to write + /// True if the write is allowed, false otherwise + /// If called while in a bitwise context + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe bool TryBeginWriteValue(in T value) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + int len = sizeof(T); + if (PositionInternal + len > CapacityInternal) + { + if (PositionInternal + len > MaxCapacityInternal) + { + return false; + } + if (CapacityInternal < MaxCapacityInternal) + { + Grow(len); + } + else + { + return false; + } + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + AllowedWriteMark = PositionInternal + len; +#endif + return true; + } + + /// + /// Internal version of TryBeginWrite. + /// Differs from TryBeginWrite only in that it won't ever move the AllowedWriteMark backward. + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryBeginWriteInternal(int bytes) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + if (PositionInternal + bytes > CapacityInternal) + { + if (PositionInternal + bytes > MaxCapacityInternal) + { + return false; + } + if (CapacityInternal < MaxCapacityInternal) + { + Grow(bytes); + } + else + { + return false; + } + } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (PositionInternal + bytes > AllowedWriteMark) + { + AllowedWriteMark = PositionInternal + bytes; + } +#endif + return true; + } + + /// + /// Returns an array representation of the underlying byte buffer. + /// !!Allocates a new array!! + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte[] ToArray() + { + byte[] ret = new byte[Length]; + fixed (byte* b = ret) + { + UnsafeUtility.MemCpy(b, BufferPointer, Length); + } + return ret; + } + + /// + /// Gets a direct pointer to the underlying buffer + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte* GetUnsafePtr() + { + return BufferPointer; + } + + /// + /// Gets a direct pointer to the underlying buffer at the current read position + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe byte* GetUnsafePtrAtCurrentPosition() + { + return BufferPointer + PositionInternal; + } + + /// + /// Get the required size to write a string + /// + /// The string to write + /// Whether or not to use one byte per character. This will only allow ASCII + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetWriteSize(string s, bool oneByteChars = false) + { + return sizeof(int) + s.Length * (oneByteChars ? sizeof(byte) : sizeof(char)); + } + + /// + /// Writes a string + /// + /// The string to write + /// Whether or not to use one byte per character. This will only allow ASCII + public unsafe void WriteValue(string s, bool oneByteChars = false) + { + WriteValue((uint)s.Length); + int target = s.Length; + if (oneByteChars) + { + for (int i = 0; i < target; ++i) + { + WriteByte((byte)s[i]); + } + } + else + { + fixed (char* native = s) + { + WriteBytes((byte*)native, target * sizeof(char)); + } + } + } + + /// + /// Writes a string + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The string to write + /// Whether or not to use one byte per character. This will only allow ASCII + public unsafe void WriteValueSafe(string s, bool oneByteChars = false) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + int sizeInBytes = GetWriteSize(s, oneByteChars); + + if (!TryBeginWriteInternal(sizeInBytes)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + + WriteValue((uint)s.Length); + int target = s.Length; + if (oneByteChars) + { + for (int i = 0; i < target; ++i) + { + WriteByte((byte)s[i]); + } + } + else + { + fixed (char* native = s) + { + WriteBytes((byte*)native, target * sizeof(char)); + } + } + } + + /// + /// Get the required size to write an unmanaged array + /// + /// The array to write + /// The amount of elements to write + /// Where in the array to start + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int GetWriteSize(T[] array, int count = -1, int offset = 0) where T : unmanaged + { + int sizeInTs = count != -1 ? count : array.Length - offset; + int sizeInBytes = sizeInTs * sizeof(T); + return sizeof(int) + sizeInBytes; + } + + /// + /// Writes an unmanaged array + /// + /// The array to write + /// The amount of elements to write + /// Where in the array to start + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteValue(T[] array, int count = -1, int offset = 0) where T : unmanaged + { + int sizeInTs = count != -1 ? count : array.Length - offset; + int sizeInBytes = sizeInTs * sizeof(T); + WriteValue(sizeInTs); + fixed (T* native = array) + { + byte* bytes = (byte*)(native + offset); + WriteBytes(bytes, sizeInBytes); + } + } + + /// + /// Writes an unmanaged array + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The array to write + /// The amount of elements to write + /// Where in the array to start + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteValueSafe(T[] array, int count = -1, int offset = 0) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + int sizeInTs = count != -1 ? count : array.Length - offset; + int sizeInBytes = sizeInTs * sizeof(T); + + if (!TryBeginWriteInternal(sizeInBytes + sizeof(int))) + { + throw new OverflowException("Writing past the end of the buffer"); + } + WriteValue(sizeInTs); + fixed (T* native = array) + { + byte* bytes = (byte*)(native + offset); + WriteBytes(bytes, sizeInBytes); + } + } + + /// + /// Write a partial value. The specified number of bytes is written from the value and the rest is ignored. + /// + /// Value to write + /// Number of bytes + /// Offset into the value to begin reading the bytes + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WritePartialValue(T value, int bytesToWrite, int offsetBytes = 0) where T : unmanaged + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } + if (PositionInternal + bytesToWrite > AllowedWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + } +#endif + + byte* ptr = ((byte*)&value) + offsetBytes; + byte* bufferPointer = BufferPointer + PositionInternal; + UnsafeUtility.MemCpy(bufferPointer, ptr, bytesToWrite); + + PositionInternal += bytesToWrite; + } + + /// + /// Write a byte to the stream. + /// + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteByte(byte value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } + if (PositionInternal + 1 > AllowedWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + } +#endif + BufferPointer[PositionInternal++] = value; + } + + /// + /// Write a byte to the stream. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// Value to write + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteByteSafe(byte value) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginWriteInternal(1)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + BufferPointer[PositionInternal++] = value; + } + + /// + /// Write multiple bytes to the stream + /// + /// Value to write + /// Number of bytes to write + /// Offset into the buffer to begin writing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBytes(byte* value, int size, int offset = 0) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } + if (PositionInternal + size > AllowedWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + } +#endif + UnsafeUtility.MemCpy((BufferPointer + PositionInternal), value + offset, size); + PositionInternal += size; + } + + /// + /// Write multiple bytes to the stream + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// Value to write + /// Number of bytes to write + /// Offset into the buffer to begin writing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBytesSafe(byte* value, int size, int offset = 0) + { +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginWriteInternal(size)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + UnsafeUtility.MemCpy((BufferPointer + PositionInternal), value + offset, size); + PositionInternal += size; + } + + /// + /// Write multiple bytes to the stream + /// + /// Value to write + /// Number of bytes to write + /// Offset into the buffer to begin writing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBytes(byte[] value, int size, int offset = 0) + { + fixed (byte* ptr = value) + { + WriteBytes(ptr, size, offset); + } + } + + /// + /// Write multiple bytes to the stream + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// Value to write + /// Number of bytes to write + /// Offset into the buffer to begin writing + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteBytesSafe(byte[] value, int size, int offset = 0) + { + fixed (byte* ptr = value) + { + WriteBytesSafe(ptr, size, offset); + } + } + + /// + /// Copy the contents of this writer into another writer. + /// The contents will be copied from the beginning of this writer to its current position. + /// They will be copied to the other writer starting at the other writer's current position. + /// + /// Writer to copy to + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void CopyTo(FastBufferWriter other) + { + other.WriteBytes(BufferPointer, PositionInternal); + } + + /// + /// Copy the contents of another writer into this writer. + /// The contents will be copied from the beginning of the other writer to its current position. + /// They will be copied to this writer starting at this writer's current position. + /// + /// Writer to copy to + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void CopyFrom(FastBufferWriter other) + { + WriteBytes(other.BufferPointer, other.PositionInternal); + } + + /// + /// Get the size required to write an unmanaged value + /// + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe int GetWriteSize(in T value) where T : unmanaged + { + return sizeof(T); + } + + /// + /// Get the size required to write an unmanaged value of type T + /// + /// + /// + /// + public static unsafe int GetWriteSize() where T : unmanaged + { + return sizeof(T); + } + + /// + /// Write a value of any unmanaged type (including unmanaged structs) to the buffer. + /// It will be copied into the buffer exactly as it exists in memory. + /// + /// The value to copy + /// Any unmanaged type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteValue(in T value) where T : unmanaged + { + int len = sizeof(T); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } + if (PositionInternal + len > AllowedWriteMark) + { + throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); + } +#endif + + fixed (T* ptr = &value) + { + UnsafeUtility.MemCpy(BufferPointer + PositionInternal, (byte*)ptr, len); + } + PositionInternal += len; + } + + /// + /// Write a value of any unmanaged type (including unmanaged structs) to the buffer. + /// It will be copied into the buffer exactly as it exists in memory. + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The value to copy + /// Any unmanaged type + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe void WriteValueSafe(in T value) where T : unmanaged + { + int len = sizeof(T); + +#if DEVELOPMENT_BUILD || UNITY_EDITOR + if (m_InBitwiseContext) + { + throw new InvalidOperationException( + "Cannot use BufferWriter in bytewise mode while in a bitwise context."); + } +#endif + + if (!TryBeginWriteInternal(len)) + { + throw new OverflowException("Writing past the end of the buffer"); + } + + fixed (T* ptr = &value) + { + UnsafeUtility.MemCpy(BufferPointer + PositionInternal, (byte*)ptr, len); + } + PositionInternal += len; + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs.meta new file mode 100644 index 0000000000..0c31b46524 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 819a511316a46104db673c8a0eab9e72 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriterExtensions.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriterExtensions.cs new file mode 100644 index 0000000000..af269f811e --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriterExtensions.cs @@ -0,0 +1,297 @@ + +using System; +using UnityEngine; + +namespace Unity.Netcode +{ + public static class FastBufferWriterExtensions + { + + /// + /// Writes a boxed object in a standard format + /// Named differently from other WriteValue methods to avoid accidental boxing + /// + /// The object to write + /// + /// If true, an extra byte will be written to indicate whether or not the value is null. + /// Some types will always write this. + /// + public static void WriteObject(this ref FastBufferWriter writer, object value, bool isNullable = false) + { + if (isNullable || value.GetType().IsNullable()) + { + bool isNull = value == null || (value is UnityEngine.Object o && o == null); + + writer.WriteValueSafe(isNull); + + if (isNull) + { + return; + } + } + + var type = value.GetType(); + var hasSerializer = SerializationTypeTable.Serializers.TryGetValue(type, out var serializer); + if (hasSerializer) + { + serializer(ref writer, value); + return; + } + + if (value is Array array) + { + writer.WriteValueSafe(array.Length); + + for (int i = 0; i < array.Length; i++) + { + writer.WriteObject(array.GetValue(i)); + } + + return; + } + + if (value.GetType().IsEnum) + { + switch (Convert.GetTypeCode(value)) + { + case TypeCode.Boolean: + writer.WriteValueSafe((byte)value); + break; + case TypeCode.Char: + writer.WriteValueSafe((char)value); + break; + case TypeCode.SByte: + writer.WriteValueSafe((sbyte)value); + break; + case TypeCode.Byte: + writer.WriteValueSafe((byte)value); + break; + case TypeCode.Int16: + writer.WriteValueSafe((short)value); + break; + case TypeCode.UInt16: + writer.WriteValueSafe((ushort)value); + break; + case TypeCode.Int32: + writer.WriteValueSafe((int)value); + break; + case TypeCode.UInt32: + writer.WriteValueSafe((uint)value); + break; + case TypeCode.Int64: + writer.WriteValueSafe((long)value); + break; + case TypeCode.UInt64: + writer.WriteValueSafe((ulong)value); + break; + } + return; + } + if (value is GameObject) + { + writer.WriteValueSafe((GameObject)value); + return; + } + if (value is NetworkObject) + { + writer.WriteValueSafe((NetworkObject)value); + return; + } + if (value is NetworkBehaviour) + { + writer.WriteValueSafe((NetworkBehaviour)value); + return; + } + if (value is INetworkSerializable) + { + //TODO ((INetworkSerializable)value).NetworkSerialize(new NetworkSerializer(this)); + return; + } + + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write type {value.GetType().Name} - it does not implement {nameof(INetworkSerializable)}"); + } + + /// + /// Write an INetworkSerializable + /// + /// The value to write + /// + public static void WriteNetworkSerializable(this ref FastBufferWriter writer, in T value) where T : INetworkSerializable + { + // TODO + } + + /// + /// Get the required amount of space to write a GameObject + /// + /// + /// + public static int GetWriteSize(GameObject value) + { + return sizeof(ulong); + } + + /// + /// Get the required amount of space to write a GameObject + /// + /// + public static int GetGameObjectWriteSize() + { + return sizeof(ulong); + } + + /// + /// Write a GameObject + /// + /// The value to write + public static void WriteValue(this ref FastBufferWriter writer, GameObject value) + { + value.TryGetComponent(out var networkObject); + if (networkObject == null) + { + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write {nameof(GameObject)} types that does not has a {nameof(NetworkObject)} component attached. {nameof(GameObject)}: {(value).name}"); + } + + if (!networkObject.IsSpawned) + { + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write {nameof(NetworkObject)} types that are not spawned. {nameof(GameObject)}: {(value).name}"); + } + + writer.WriteValue(networkObject.NetworkObjectId); + } + + /// + /// Write a GameObject + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The value to write + public static void WriteValueSafe(this ref FastBufferWriter writer, GameObject value) + { + value.TryGetComponent(out var networkObject); + if (networkObject == null) + { + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write {nameof(GameObject)} types that does not has a {nameof(NetworkObject)} component attached. {nameof(GameObject)}: {(value).name}"); + } + + if (!networkObject.IsSpawned) + { + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write {nameof(NetworkObject)} types that are not spawned. {nameof(GameObject)}: {(value).name}"); + } + + writer.WriteValueSafe(networkObject.NetworkObjectId); + } + + /// + /// Get the required size to write a NetworkObject + /// + /// + /// + public static int GetWriteSize(NetworkObject value) + { + return sizeof(ulong); + } + + /// + /// Get the required size to write a NetworkObject + /// + /// + public static int GetNetworkObjectWriteSize() + { + return sizeof(ulong); + } + + + /// + /// Write a NetworkObject + /// + /// The value to write + public static void WriteValue(this ref FastBufferWriter writer, in NetworkObject value) + { + if (!value.IsSpawned) + { + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write {nameof(NetworkObject)} types that are not spawned. {nameof(GameObject)}: {value.name}"); + } + + writer.WriteValue(value.NetworkObjectId); + } + + /// + /// Write a NetworkObject + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The value to write + public static void WriteValueSafe(this ref FastBufferWriter writer, NetworkObject value) + { + if (!value.IsSpawned) + { + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write {nameof(NetworkObject)} types that are not spawned. {nameof(GameObject)}: {value.name}"); + } + writer.WriteValueSafe(value.NetworkObjectId); + } + + /// + /// Get the required size to write a NetworkBehaviour + /// + /// + /// + public static int GetWriteSize(NetworkBehaviour value) + { + return sizeof(ulong) + sizeof(ushort); + } + + + /// + /// Get the required size to write a NetworkBehaviour + /// + /// + public static int GetNetworkBehaviourWriteSize() + { + return sizeof(ulong) + sizeof(ushort); + } + + + /// + /// Write a NetworkBehaviour + /// + /// The value to write + public static void WriteValue(this ref FastBufferWriter writer, NetworkBehaviour value) + { + if (!value.HasNetworkObject || !value.NetworkObject.IsSpawned) + { + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write {nameof(NetworkBehaviour)} types that are not spawned. {nameof(GameObject)}: {(value).gameObject.name}"); + } + + writer.WriteValue(value.NetworkObjectId); + writer.WriteValue(value.NetworkBehaviourId); + } + + /// + /// Write a NetworkBehaviour + /// + /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking + /// for multiple writes at once by calling TryBeginWrite. + /// + /// The value to write + /// + /// + public static void WriteValueSafe(this ref FastBufferWriter writer, NetworkBehaviour value) + { + if (!value.HasNetworkObject || !value.NetworkObject.IsSpawned) + { + throw new ArgumentException($"{nameof(FastBufferWriter)} cannot write {nameof(NetworkBehaviour)} types that are not spawned. {nameof(GameObject)}: {(value).gameObject.name}"); + } + + if (!writer.TryBeginWriteInternal(sizeof(ulong) + sizeof(ushort))) + { + throw new OverflowException("Writing past the end of the buffer"); + } + writer.WriteValue(value.NetworkObjectId); + writer.WriteValue(value.NetworkBehaviourId); + } + + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriterExtensions.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriterExtensions.cs.meta new file mode 100644 index 0000000000..b96c1f3b21 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriterExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1a9dd964797964b4a99e03706516a8a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/IBufferSerializerImplementation.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/IBufferSerializerImplementation.cs new file mode 100644 index 0000000000..d97954c243 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/IBufferSerializerImplementation.cs @@ -0,0 +1,38 @@ +using System; +using UnityEngine; + +namespace Unity.Netcode +{ + public interface IBufferSerializerImplementation + { + bool IsReader { get; } + bool IsWriter { get; } + + ref FastBufferReader GetFastBufferReader(); + ref FastBufferWriter GetFastBufferWriter(); + + void SerializeValue(ref object value, Type type, bool isNullable = false); + void SerializeValue(ref INetworkSerializable value); + void SerializeValue(ref GameObject value); + void SerializeValue(ref NetworkObject value); + void SerializeValue(ref NetworkBehaviour value); + void SerializeValue(ref string s, bool oneByteChars = false); + void SerializeValue(ref T[] array) where T : unmanaged; + void SerializeValue(ref byte value); + void SerializeValue(ref T value) where T : unmanaged; + + // Has to have a different name to avoid conflicting with "where T: unmananged" + // Using SerializeValue(INetworkSerializable) will result in boxing on struct INetworkSerializables + // So this is provided as an alternative to avoid boxing allocations. + void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable; + + bool PreCheck(int amount); + void SerializeValuePreChecked(ref GameObject value); + void SerializeValuePreChecked(ref NetworkObject value); + void SerializeValuePreChecked(ref NetworkBehaviour value); + void SerializeValuePreChecked(ref string s, bool oneByteChars = false); + void SerializeValuePreChecked(ref T[] array) where T : unmanaged; + void SerializeValuePreChecked(ref byte value); + void SerializeValuePreChecked(ref T value) where T : unmanaged; + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/IBufferSerializerImplementation.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/IBufferSerializerImplementation.cs.meta new file mode 100644 index 0000000000..f5c741d324 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/IBufferSerializerImplementation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9220f80eab4051e4d9b9504ef840eba4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/SerializationTypeTable.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/SerializationTypeTable.cs new file mode 100644 index 0000000000..fb861c91d7 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/SerializationTypeTable.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Unity.Netcode +{ + /// + /// Registry for telling FastBufferWriter and FastBufferReader how to read types when passed to + /// WriteObject and ReadObject, as well as telling BytePacker and ByteUnpacker how to do it when passed to + /// WriteObjectPacked and ReadObjectPacked. + /// + /// These object-based serialization functions shouldn't be used if at all possible, but if they're required, + /// and you need to serialize a type that's not natively supported, you can register it with the dictionaries here: + /// + /// Serializers and Deserializers for FastBufferWriter and FasteBufferReader + /// SerializersPacked and DeserializersPacked for BytePacker and ByteUnpacker + /// + public static class SerializationTypeTable + { + public delegate void Serialize(ref FastBufferWriter writer, object value); + public delegate void Deserialize(ref FastBufferReader reader, out object value); + + public static Dictionary Serializers = new Dictionary + { + [typeof(byte)] = (ref FastBufferWriter writer, object value) => writer.WriteByteSafe((byte)value), + [typeof(sbyte)] = (ref FastBufferWriter writer, object value) => writer.WriteByteSafe((byte)(sbyte)value), + + [typeof(ushort)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((ushort)value), + [typeof(short)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((short)value), + [typeof(uint)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((uint)value), + [typeof(int)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((int)value), + [typeof(ulong)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((ulong)value), + [typeof(long)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((long)value), + + [typeof(float)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((float)value), + [typeof(double)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((double)value), + + [typeof(string)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((string)value), + + [typeof(Vector2)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Vector2)value), + [typeof(Vector3)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Vector3)value), + [typeof(Vector4)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Vector4)value), + [typeof(Color)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Color)value), + [typeof(Color32)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Color32)value), + [typeof(Ray)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Ray)value), + [typeof(Ray2D)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Ray2D)value), + [typeof(Quaternion)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Quaternion)value), + + [typeof(char)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((char)value), + + [typeof(bool)] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((bool)value), + + + [typeof(byte[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((byte[])value), + [typeof(sbyte[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((sbyte[])value), + + [typeof(ushort[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((ushort[])value), + [typeof(short[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((short[])value), + [typeof(uint[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((uint[])value), + [typeof(int[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((int[])value), + [typeof(ulong[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((ulong[])value), + [typeof(long[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((long[])value), + + [typeof(float[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((float[])value), + [typeof(double[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((double[])value), + + [typeof(Vector2[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Vector2[])value), + [typeof(Vector3[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Vector3[])value), + [typeof(Vector4[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Vector4[])value), + [typeof(Color[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Color[])value), + [typeof(Color32[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Color32[])value), + [typeof(Ray[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Ray[])value), + [typeof(Ray2D[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Ray2D[])value), + [typeof(Quaternion[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((Quaternion[])value), + + [typeof(char[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((char[])value), + + [typeof(bool[])] = (ref FastBufferWriter writer, object value) => writer.WriteValueSafe((bool[])value), + }; + + public static Dictionary Deserializers = new Dictionary + { + [typeof(byte)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadByteSafe(out byte tmp); + value = tmp; + }, + [typeof(sbyte)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadByteSafe(out byte tmp); + value = (sbyte)tmp; + }, + + [typeof(ushort)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out ushort tmp); + value = tmp; + }, + [typeof(short)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out short tmp); + value = tmp; + }, + [typeof(uint)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out uint tmp); + value = tmp; + }, + [typeof(int)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out int tmp); + value = tmp; + }, + [typeof(ulong)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out ulong tmp); + value = tmp; + }, + [typeof(long)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out long tmp); + value = tmp; + }, + + [typeof(float)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out float tmp); + value = tmp; + }, + [typeof(double)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out double tmp); + value = tmp; + }, + + [typeof(string)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out string tmp); + value = tmp; + }, + + [typeof(Vector2)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Vector2 tmp); + value = tmp; + }, + [typeof(Vector3)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Vector3 tmp); + value = tmp; + }, + [typeof(Vector4)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Vector4 tmp); + value = tmp; + }, + [typeof(Color)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Color tmp); + value = tmp; + }, + [typeof(Color32)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Color32 tmp); + value = tmp; + }, + [typeof(Ray)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Ray tmp); + value = tmp; + }, + [typeof(Ray2D)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Ray2D tmp); + value = tmp; + }, + [typeof(Quaternion)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Quaternion tmp); + value = tmp; + }, + + [typeof(char)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out char tmp); + value = tmp; + }, + + [typeof(bool)] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out bool tmp); + value = tmp; + }, + + + [typeof(byte[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out byte[] tmp); + value = tmp; + }, + [typeof(sbyte[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out sbyte[] tmp); + value = tmp; + }, + + [typeof(ushort[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out ushort[] tmp); + value = tmp; + }, + [typeof(short[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out short[] tmp); + value = tmp; + }, + [typeof(uint[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out uint[] tmp); + value = tmp; + }, + [typeof(int[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out int[] tmp); + value = tmp; + }, + [typeof(ulong[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out ulong[] tmp); + value = tmp; + }, + [typeof(long[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out long[] tmp); + value = tmp; + }, + + [typeof(float[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out float[] tmp); + value = tmp; + }, + [typeof(double[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out double[] tmp); + value = tmp; + }, + + [typeof(Vector2[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Vector2[] tmp); + value = tmp; + }, + [typeof(Vector3[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Vector3[] tmp); + value = tmp; + }, + [typeof(Vector4[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Vector4[] tmp); + value = tmp; + }, + [typeof(Color[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Color[] tmp); + value = tmp; + }, + [typeof(Color32[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Color32[] tmp); + value = tmp; + }, + [typeof(Ray[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Ray[] tmp); + value = tmp; + }, + [typeof(Ray2D[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Ray2D[] tmp); + value = tmp; + }, + [typeof(Quaternion[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out Quaternion[] tmp); + value = tmp; + }, + + [typeof(char[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out char[] tmp); + value = tmp; + }, + + [typeof(bool[])] = (ref FastBufferReader reader, out object value) => + { + reader.ReadValueSafe(out bool[] tmp); + value = tmp; + }, + }; + + public static Dictionary SerializersPacked = new Dictionary + { + [typeof(byte)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (byte)value), + [typeof(sbyte)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (byte)(sbyte)value), + + [typeof(ushort)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (ushort)value), + [typeof(short)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (short)value), + [typeof(uint)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (uint)value), + [typeof(int)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (int)value), + [typeof(ulong)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (ulong)value), + [typeof(long)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (long)value), + + [typeof(float)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (float)value), + [typeof(double)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (double)value), + + [typeof(string)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (string)value), + + [typeof(Vector2)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (Vector2)value), + [typeof(Vector3)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (Vector3)value), + [typeof(Vector4)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (Vector4)value), + [typeof(Color)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (Color)value), + [typeof(Color32)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (Color32)value), + [typeof(Ray)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (Ray)value), + [typeof(Ray2D)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (Ray2D)value), + [typeof(Quaternion)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (Quaternion)value), + + [typeof(char)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (char)value), + + [typeof(bool)] = (ref FastBufferWriter writer, object value) => BytePacker.WriteValuePacked(ref writer, (bool)value), + }; + + public static Dictionary DeserializersPacked = new Dictionary + { + [typeof(byte)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out byte tmp); + value = tmp; + }, + [typeof(sbyte)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out byte tmp); + value = (sbyte)tmp; + }, + + [typeof(ushort)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out ushort tmp); + value = tmp; + }, + [typeof(short)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out short tmp); + value = tmp; + }, + [typeof(uint)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out uint tmp); + value = tmp; + }, + [typeof(int)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out int tmp); + value = tmp; + }, + [typeof(ulong)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out ulong tmp); + value = tmp; + }, + [typeof(long)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out long tmp); + value = tmp; + }, + + [typeof(float)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out float tmp); + value = tmp; + }, + [typeof(double)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out double tmp); + value = tmp; + }, + + [typeof(string)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out string tmp); + value = tmp; + }, + + [typeof(Vector2)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out Vector2 tmp); + value = tmp; + }, + [typeof(Vector3)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out Vector3 tmp); + value = tmp; + }, + [typeof(Vector4)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out Vector4 tmp); + value = tmp; + }, + [typeof(Color)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out Color tmp); + value = tmp; + }, + [typeof(Color32)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out Color32 tmp); + value = tmp; + }, + [typeof(Ray)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out Ray tmp); + value = tmp; + }, + [typeof(Ray2D)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out Ray2D tmp); + value = tmp; + }, + [typeof(Quaternion)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out Quaternion tmp); + value = tmp; + }, + + [typeof(char)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out char tmp); + value = tmp; + }, + + [typeof(bool)] = (ref FastBufferReader reader, out object value) => + { + ByteUnpacker.ReadValuePacked(ref reader, out bool tmp); + value = tmp; + }, + }; + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/SerializationTypeTable.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/SerializationTypeTable.cs.meta new file mode 100644 index 0000000000..58c25d1646 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/SerializationTypeTable.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 49ee6a0e2ea6e9441a74b173c31cf389 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Runtime/Utility.meta b/com.unity.netcode.gameobjects/Runtime/Utility.meta new file mode 100644 index 0000000000..a71098491b --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Utility.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 07815748c1ee48a69d6981ec990575ed +timeCreated: 1629412677 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Utility/Ref.cs b/com.unity.netcode.gameobjects/Runtime/Utility/Ref.cs new file mode 100644 index 0000000000..02572cbc46 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Utility/Ref.cs @@ -0,0 +1,25 @@ +using System.Runtime.CompilerServices; + +namespace Unity.Netcode +{ + public struct Ref where T : unmanaged + { + private unsafe T* m_Value; + + public unsafe Ref(ref T value) + { + fixed (T* ptr = &value) + { + m_Value = ptr; + } + } + + public unsafe bool IsSet => m_Value != null; + + public unsafe ref T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref *m_Value; + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Utility/Ref.cs.meta b/com.unity.netcode.gameobjects/Runtime/Utility/Ref.cs.meta new file mode 100644 index 0000000000..56afe0844f --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Utility/Ref.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 00adc41a9c8699349a50dba23dbd46a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization.meta new file mode 100644 index 0000000000..b3c7beee0b --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e12c4be6e89f459aa2826abba8c8d301 +timeCreated: 1628799671 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs new file mode 100644 index 0000000000..fa067d73a6 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs @@ -0,0 +1,664 @@ +using System; +using NUnit.Framework; +using Unity.Netcode.EditorTests; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode +{ + public abstract class BaseFastBufferReaderWriterTest + { + + #region Test Types + protected enum ByteEnum : byte + { + A, + B, + C + }; + protected enum SByteEnum : sbyte + { + A, + B, + C + }; + protected enum ShortEnum : short + { + A, + B, + C + }; + protected enum UShortEnum : ushort + { + A, + B, + C + }; + protected enum IntEnum : int + { + A, + B, + C + }; + protected enum UIntEnum : uint + { + A, + B, + C + }; + protected enum LongEnum : long + { + A, + B, + C + }; + protected enum ULongEnum : ulong + { + A, + B, + C + }; + + protected struct TestStruct + { + public byte A; + public short B; + public ushort C; + public int D; + public uint E; + public long F; + public ulong G; + public bool H; + public char I; + public float J; + public double K; + } + + public enum WriteType + { + WriteDirect, + WriteSafe, + WriteAsObject + } + #endregion + + + protected abstract void RunTypeTest(T valueToTest) where T : unmanaged; + + protected abstract void RunTypeTestSafe(T valueToTest) where T : unmanaged; + + protected abstract void RunObjectTypeTest(T valueToTest) where T : unmanaged; + + protected abstract void RunTypeArrayTest(T[] valueToTest) where T : unmanaged; + + protected abstract void RunTypeArrayTestSafe(T[] valueToTest) where T : unmanaged; + + protected abstract void RunObjectTypeArrayTest(T[] valueToTest) where T : unmanaged; + + #region Helpers + protected TestStruct GetTestStruct() + { + var random = new Random(); + + var testStruct = new TestStruct + { + A = (byte)random.Next(), + B = (short)random.Next(), + C = (ushort)random.Next(), + D = (int)random.Next(), + E = (uint)random.Next(), + F = ((long)random.Next() << 32) + random.Next(), + G = ((ulong)random.Next() << 32) + (ulong)random.Next(), + H = true, + I = '\u263a', + J = (float)random.NextDouble(), + K = random.NextDouble(), + }; + + return testStruct; + } + + protected delegate void GameObjectTestDelegate(GameObject obj, NetworkBehaviour networkBehaviour, + NetworkObject networkObject); + protected void RunGameObjectTest(GameObjectTestDelegate testCode) + { + var obj = new GameObject("Object"); + var networkBehaviour = obj.AddComponent(); + var networkObject = obj.AddComponent(); + // Create networkManager component + var networkManager = obj.AddComponent(); + networkManager.SetSingleton(); + networkObject.NetworkManagerOwner = networkManager; + + // Set the NetworkConfig + networkManager.NetworkConfig = new NetworkConfig() + { + // Set transport + NetworkTransport = obj.AddComponent() + }; + + networkManager.StartHost(); + + try + { + testCode(obj, networkBehaviour, networkObject); + } + finally + { + UnityEngine.Object.DestroyImmediate(obj); + networkManager.Shutdown(); + } + } + #endregion + + public void BaseTypeTest(Type testType, WriteType writeType) + { + var random = new Random(); + + void RunTypeTestLocal(T val, WriteType wt) where T : unmanaged + { + switch (wt) + { + case WriteType.WriteDirect: + RunTypeTest(val); + break; + case WriteType.WriteSafe: + RunTypeTestSafe(val); + break; + default: + RunObjectTypeTest(val); + break; + } + } + + if (testType == typeof(byte)) + { + RunTypeTestLocal((byte)random.Next(), writeType); + } + else if (testType == typeof(sbyte)) + { + RunTypeTestLocal((sbyte)random.Next(), writeType); + } + else if (testType == typeof(short)) + { + RunTypeTestLocal((short)random.Next(), writeType); + } + else if (testType == typeof(ushort)) + { + RunTypeTestLocal((ushort)random.Next(), writeType); + } + else if (testType == typeof(int)) + { + RunTypeTestLocal((int)random.Next(), writeType); + } + else if (testType == typeof(uint)) + { + RunTypeTestLocal((uint)random.Next(), writeType); + } + else if (testType == typeof(long)) + { + RunTypeTestLocal(((long)random.Next() << 32) + random.Next(), writeType); + } + else if (testType == typeof(ulong)) + { + RunTypeTestLocal(((ulong)random.Next() << 32) + (ulong)random.Next(), writeType); + } + else if (testType == typeof(bool)) + { + RunTypeTestLocal(true, writeType); + } + else if (testType == typeof(char)) + { + RunTypeTestLocal('a', writeType); + RunTypeTestLocal('\u263a', writeType); + } + else if (testType == typeof(float)) + { + RunTypeTestLocal((float)random.NextDouble(), writeType); + } + else if (testType == typeof(double)) + { + RunTypeTestLocal(random.NextDouble(), writeType); + } + else if (testType == typeof(ByteEnum)) + { + RunTypeTestLocal(ByteEnum.C, writeType); + } + else if (testType == typeof(SByteEnum)) + { + RunTypeTestLocal(SByteEnum.C, writeType); + } + else if (testType == typeof(ShortEnum)) + { + RunTypeTestLocal(ShortEnum.C, writeType); + } + else if (testType == typeof(UShortEnum)) + { + RunTypeTestLocal(UShortEnum.C, writeType); + } + else if (testType == typeof(IntEnum)) + { + RunTypeTestLocal(IntEnum.C, writeType); + } + else if (testType == typeof(UIntEnum)) + { + RunTypeTestLocal(UIntEnum.C, writeType); + } + else if (testType == typeof(LongEnum)) + { + RunTypeTestLocal(LongEnum.C, writeType); + } + else if (testType == typeof(ULongEnum)) + { + RunTypeTestLocal(ULongEnum.C, writeType); + } + else if (testType == typeof(Vector2)) + { + RunTypeTestLocal(new Vector2((float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Vector3)) + { + RunTypeTestLocal(new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Vector4)) + { + RunTypeTestLocal(new Vector4((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Quaternion)) + { + RunTypeTestLocal(new Quaternion((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Color)) + { + RunTypeTestLocal(new Color((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), writeType); + } + else if (testType == typeof(Color32)) + { + RunTypeTestLocal(new Color32((byte)random.Next(), (byte)random.Next(), (byte)random.Next(), (byte)random.Next()), writeType); + } + else if (testType == typeof(Ray)) + { + RunTypeTestLocal(new Ray( + new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), + new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble())), writeType); + } + else if (testType == typeof(Ray2D)) + { + RunTypeTestLocal(new Ray2D( + new Vector2((float)random.NextDouble(), (float)random.NextDouble()), + new Vector2((float)random.NextDouble(), (float)random.NextDouble())), writeType); + } + else if (testType == typeof(TestStruct)) + { + SerializationTypeTable.Serializers[typeof(TestStruct)] = (ref FastBufferWriter writer, object obj) => + { + writer.WriteValueSafe((TestStruct)obj); + }; + SerializationTypeTable.Deserializers[typeof(TestStruct)] = (ref FastBufferReader reader, out object obj) => + { + reader.ReadValueSafe(out TestStruct value); + obj = value; + }; + try + { + RunTypeTestLocal(GetTestStruct(), writeType); + } + finally + { + SerializationTypeTable.Serializers.Remove(typeof(TestStruct)); + SerializationTypeTable.Deserializers.Remove(typeof(TestStruct)); + } + } + else + { + Assert.Fail("No type handler was provided for this type in the test!"); + } + } + + public void BaseArrayTypeTest(Type testType, WriteType writeType) + { + var random = new Random(); + void RunTypeTestLocal(T[] val, WriteType wt) where T : unmanaged + { + switch (wt) + { + case WriteType.WriteDirect: + RunTypeArrayTest(val); + break; + case WriteType.WriteSafe: + RunTypeArrayTestSafe(val); + break; + default: + RunObjectTypeArrayTest(val); + break; + } + } + + if (testType == typeof(byte)) + { + RunTypeTestLocal(new[]{ + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next(), + (byte) random.Next() + }, writeType); + } + else if (testType == typeof(sbyte)) + { + RunTypeTestLocal(new[]{ + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next(), + (sbyte) random.Next() + }, writeType); + } + else if (testType == typeof(short)) + { + RunTypeTestLocal(new[]{ + (short) random.Next(), + (short) random.Next(), + (short) random.Next(), + (short) random.Next(), + (short) random.Next() + }, writeType); + } + else if (testType == typeof(ushort)) + { + RunTypeTestLocal(new[]{ + (ushort) random.Next(), + (ushort) random.Next(), + (ushort) random.Next(), + (ushort) random.Next(), + (ushort) random.Next(), + (ushort) random.Next() + }, writeType); + } + else if (testType == typeof(int)) + { + RunTypeTestLocal(new[]{ + random.Next(), + random.Next(), + random.Next(), + random.Next(), + random.Next(), + random.Next(), + random.Next(), + random.Next() + }, writeType); + } + else if (testType == typeof(uint)) + { + RunTypeTestLocal(new[]{ + (uint) random.Next(), + (uint) random.Next(), + (uint) random.Next(), + (uint) random.Next(), + (uint) random.Next(), + (uint) random.Next() + }, writeType); + } + else if (testType == typeof(long)) + { + RunTypeTestLocal(new[]{ + ((long)random.Next() << 32) + (long)random.Next(), + ((long)random.Next() << 32) + (long)random.Next(), + ((long)random.Next() << 32) + (long)random.Next(), + ((long)random.Next() << 32) + (long)random.Next() + }, writeType); + } + else if (testType == typeof(ulong)) + { + RunTypeTestLocal(new[]{ + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next(), + ((ulong)random.Next() << 32) + (ulong)random.Next() + }, writeType); + } + else if (testType == typeof(bool)) + { + RunTypeTestLocal(new[]{ + true, + false, + true, + true, + false, + false, + true, + false, + true + }, writeType); + } + else if (testType == typeof(char)) + { + RunTypeTestLocal(new[]{ + 'a', + '\u263a' + }, writeType); + } + else if (testType == typeof(float)) + { + RunTypeTestLocal(new[]{ + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble(), + (float)random.NextDouble() + }, writeType); + } + else if (testType == typeof(double)) + { + RunTypeTestLocal(new[]{ + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble(), + random.NextDouble() + }, writeType); + } + else if (testType == typeof(ByteEnum)) + { + RunTypeTestLocal(new[]{ + ByteEnum.C, + ByteEnum.A, + ByteEnum.B + }, writeType); + } + else if (testType == typeof(SByteEnum)) + { + RunTypeTestLocal(new[]{ + SByteEnum.C, + SByteEnum.A, + SByteEnum.B + }, writeType); + } + else if (testType == typeof(ShortEnum)) + { + RunTypeTestLocal(new[]{ + ShortEnum.C, + ShortEnum.A, + ShortEnum.B + }, writeType); + } + else if (testType == typeof(UShortEnum)) + { + RunTypeTestLocal(new[]{ + UShortEnum.C, + UShortEnum.A, + UShortEnum.B + }, writeType); + } + else if (testType == typeof(IntEnum)) + { + RunTypeTestLocal(new[]{ + IntEnum.C, + IntEnum.A, + IntEnum.B + }, writeType); + } + else if (testType == typeof(UIntEnum)) + { + RunTypeTestLocal(new[]{ + UIntEnum.C, + UIntEnum.A, + UIntEnum.B + }, writeType); + } + else if (testType == typeof(LongEnum)) + { + RunTypeTestLocal(new[]{ + LongEnum.C, + LongEnum.A, + LongEnum.B + }, writeType); + } + else if (testType == typeof(ULongEnum)) + { + RunTypeTestLocal(new[]{ + ULongEnum.C, + ULongEnum.A, + ULongEnum.B + }, writeType); + } + else if (testType == typeof(Vector2)) + { + RunTypeTestLocal(new[]{ + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Vector3)) + { + RunTypeTestLocal(new[]{ + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Vector4)) + { + RunTypeTestLocal(new[]{ + new Vector4((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector4((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector4((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Quaternion)) + { + RunTypeTestLocal(new[]{ + new Quaternion((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble(), (float) random.NextDouble()), + new Quaternion((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble(), (float) random.NextDouble()), + new Quaternion((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble(), (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Color)) + { + RunTypeTestLocal(new[]{ + new Color((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Color((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Color((float) random.NextDouble(), (float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + }, writeType); + } + else if (testType == typeof(Color32)) + { + RunTypeTestLocal(new[]{ + new Color32((byte) random.Next(), (byte) random.Next(), (byte) random.Next(), (byte) random.Next()), + new Color32((byte) random.Next(), (byte) random.Next(), (byte) random.Next(), (byte) random.Next()), + new Color32((byte) random.Next(), (byte) random.Next(), (byte) random.Next(), (byte) random.Next()), + }, writeType); + } + else if (testType == typeof(Ray)) + { + RunTypeTestLocal(new[]{ + new Ray( + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble())), + new Ray( + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble())), + new Ray( + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble()), + new Vector3((float) random.NextDouble(), (float) random.NextDouble(), + (float) random.NextDouble())), + }, writeType); + } + else if (testType == typeof(Ray2D)) + { + RunTypeTestLocal(new[]{ + new Ray2D( + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble())), + new Ray2D( + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble())), + new Ray2D( + new Vector2((float) random.NextDouble(), (float) random.NextDouble()), + new Vector2((float) random.NextDouble(), (float) random.NextDouble())), + }, writeType); + } + else if (testType == typeof(TestStruct)) + { + SerializationTypeTable.Serializers[typeof(TestStruct)] = (ref FastBufferWriter writer, object obj) => + { + writer.WriteValueSafe((TestStruct)obj); + }; + SerializationTypeTable.Deserializers[typeof(TestStruct)] = (ref FastBufferReader reader, out object obj) => + { + reader.ReadValueSafe(out TestStruct value); + obj = value; + }; + try + { + RunTypeTestLocal(new[] { + GetTestStruct(), + GetTestStruct(), + GetTestStruct(), + }, writeType); + } + finally + { + SerializationTypeTable.Serializers.Remove(typeof(TestStruct)); + SerializationTypeTable.Deserializers.Remove(typeof(TestStruct)); + } + } + else + { + Assert.Fail("No type handler was provided for this type in the test!"); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs.meta new file mode 100644 index 0000000000..f0b683d274 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BaseFastBufferReaderWriterTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 573b1f36caed496a9c6e0eaa788d0c29 +timeCreated: 1629917174 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitCounterTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitCounterTests.cs new file mode 100644 index 0000000000..5e0f4c8a12 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitCounterTests.cs @@ -0,0 +1,71 @@ +using NUnit.Framework; + +namespace Unity.Netcode.EditorTests +{ + public class BitCounterTests + { + [Test] + public void WhenCountingUsedBitsIn64BitValue_ResultMatchesHighBitSetPlusOne([Range(0, 63)] int highBit) + { + if (highBit == 0) + { + ulong value = 0; + // 0 is a special case. All values are considered at least 1 bit. + Assert.AreEqual(1, BitCounter.GetUsedBitCount(value)); + } + else + { + ulong value = 1UL << highBit; + Assert.AreEqual(highBit + 1, BitCounter.GetUsedBitCount(value)); + } + } + + [Test] + public void WhenCountingUsedBitsIn32BitValue_ResultMatchesHighBitSetPlusOne([Range(0, 31)] int highBit) + { + if (highBit == 0) + { + uint value = 0; + // 0 is a special case. All values are considered at least 1 bit. + Assert.AreEqual(1, BitCounter.GetUsedBitCount(value)); + } + else + { + uint value = 1U << highBit; + Assert.AreEqual(highBit + 1, BitCounter.GetUsedBitCount(value)); + } + } + + [Test] + public void WhenCountingUsedBytesIn64BitValue_ResultMatchesHighBitSetOver8PlusOne([Range(0, 63)] int highBit) + { + if (highBit == 0) + { + ulong value = 0; + // 0 is a special case. All values are considered at least 1 byte. + Assert.AreEqual(1, BitCounter.GetUsedByteCount(value)); + } + else + { + ulong value = 1UL << highBit; + Assert.AreEqual(highBit / 8 + 1, BitCounter.GetUsedByteCount(value)); + } + } + + [Test] + public void WhenCountingUsedBytesIn32BitValue_ResultMatchesHighBitSetOver8PlusOne([Range(0, 31)] int highBit) + { + if (highBit == 0) + { + uint value = 0; + // 0 is a special case. All values are considered at least 1 byte. + Assert.AreEqual(1, BitCounter.GetUsedByteCount(value)); + } + else + { + uint value = 1U << highBit; + Assert.AreEqual(highBit / 8 + 1, BitCounter.GetUsedByteCount(value)); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitCounterTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitCounterTests.cs.meta new file mode 100644 index 0000000000..64a137560b --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitCounterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76e459b9c2aeea94ebf448c237061485 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitReaderTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitReaderTests.cs new file mode 100644 index 0000000000..e8c4cf3f29 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitReaderTests.cs @@ -0,0 +1,359 @@ +using System; +using NUnit.Framework; +using Unity.Collections; + +namespace Unity.Netcode.EditorTests +{ + public class BitReaderTests + { + [Test] + public void TestReadingOneBit() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBit(true); + + bitWriter.WriteBit(true); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(false); + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + } + + writer.WriteByte(0b11111111); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(3)); + using (var bitReader = reader.EnterBitwiseContext()) + { + bool b; + bitReader.ReadBit(out b); + Assert.IsTrue(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + } + + reader.ReadByte(out byte lastByte); + Assert.AreEqual(0b11111111, lastByte); + } + } + } + [Test] + public unsafe void TestTryBeginReadBits() + { + var nativeArray = new NativeArray(4, Allocator.Temp); + var reader = new FastBufferReader(nativeArray, Allocator.Temp); + nativeArray.Dispose(); + using (reader) + { + int* asInt = (int*)reader.GetUnsafePtr(); + *asInt = 0b11111111_00001010_10101011; + + using (var bitReader = reader.EnterBitwiseContext()) + { + Assert.Throws(() => reader.TryBeginRead(1)); + Assert.Throws(() => reader.TryBeginReadValue(1)); + Assert.IsTrue(bitReader.TryBeginReadBits(1)); + bitReader.ReadBit(out bool b); + Assert.IsTrue(b); + + // Can't use Assert.Throws() because ref struct BitWriter can't be captured in a lambda + try + { + bitReader.ReadBit(out b); + } + catch (OverflowException e) + { + // Should get called here. + } + catch (Exception e) + { + throw e; + } + Assert.IsTrue(bitReader.TryBeginReadBits(3)); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + bitReader.ReadBit(out b); + Assert.IsFalse(b); + bitReader.ReadBit(out b); + Assert.IsTrue(b); + + byte byteVal; + try + { + bitReader.ReadBits(out byteVal, 4); + } + catch (OverflowException e) + { + // Should get called here. + } + catch (Exception e) + { + throw e; + } + + try + { + bitReader.ReadBits(out byteVal, 1); + } + catch (OverflowException e) + { + // Should get called here. + } + catch (Exception e) + { + throw e; + } + Assert.IsTrue(bitReader.TryBeginReadBits(3)); + + try + { + bitReader.ReadBits(out byteVal, 4); + } + catch (OverflowException e) + { + // Should get called here. + } + catch (Exception e) + { + throw e; + } + Assert.IsTrue(bitReader.TryBeginReadBits(4)); + bitReader.ReadBits(out byteVal, 3); + Assert.AreEqual(0b010, byteVal); + + Assert.IsTrue(bitReader.TryBeginReadBits(5)); + + bitReader.ReadBits(out byteVal, 5); + Assert.AreEqual(0b10101, byteVal); + } + + Assert.AreEqual(2, reader.Position); + + Assert.IsTrue(reader.TryBeginRead(1)); + reader.ReadByte(out byte nextByte); + Assert.AreEqual(0b11111111, nextByte); + + Assert.IsTrue(reader.TryBeginRead(1)); + reader.ReadByte(out nextByte); + Assert.AreEqual(0b00000000, nextByte); + + Assert.IsFalse(reader.TryBeginRead(1)); + using (var bitReader = reader.EnterBitwiseContext()) + { + Assert.IsFalse(bitReader.TryBeginReadBits(1)); + } + } + } + + [Test] + public void TestReadingMultipleBits() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(0b11111111, 1); + bitWriter.WriteBits(0b11111111, 1); + bitWriter.WriteBits(0b11111110, 2); + bitWriter.WriteBits(0b11111000, 4); + bitWriter.WriteBits(0b11111010, 4); + } + writer.WriteByte(0b11111111); + + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(3)); + using (var bitReader = reader.EnterBitwiseContext()) + { + byte b; + bitReader.ReadBits(out b, 1); + Assert.AreEqual(0b1, b); + + bitReader.ReadBits(out b, 1); + Assert.AreEqual(0b1, b); + + bitReader.ReadBits(out b, 2); + Assert.AreEqual(0b10, b); + + bitReader.ReadBits(out b, 4); + Assert.AreEqual(0b1000, b); + + bitReader.ReadBits(out b, 4); + Assert.AreEqual(0b1010, b); + } + + reader.ReadByte(out byte lastByte); + Assert.AreEqual(0b11111111, lastByte); + } + } + } + + [Test] + public void TestReadingMultipleBitsToLongs() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(0b11111111UL, 1); + bitWriter.WriteBits(0b11111111UL, 1); + bitWriter.WriteBits(0b11111110UL, 2); + bitWriter.WriteBits(0b11111000UL, 4); + bitWriter.WriteBits(0b11111010UL, 4); + } + + writer.WriteByte(0b11111111); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(3)); + using (var bitReader = reader.EnterBitwiseContext()) + { + ulong ul; + bitReader.ReadBits(out ul, 1); + Assert.AreEqual(0b1, ul); + + bitReader.ReadBits(out ul, 1); + Assert.AreEqual(0b1, ul); + + bitReader.ReadBits(out ul, 2); + Assert.AreEqual(0b10, ul); + + bitReader.ReadBits(out ul, 4); + Assert.AreEqual(0b1000, ul); + + bitReader.ReadBits(out ul, 4); + Assert.AreEqual(0b1010, ul); + } + + reader.ReadByte(out byte lastByte); + Assert.AreEqual(0b11111111, lastByte); + } + } + } + + [Test] + public unsafe void TestReadingMultipleBytesToLongs([Range(1U, 64U)] uint numBits) + { + ulong value = 0xFFFFFFFFFFFFFFFF; + var reader = new FastBufferReader((byte*)&value, Allocator.Temp, sizeof(ulong)); + using (reader) + { + ulong* asUlong = (ulong*)reader.GetUnsafePtr(); + + Assert.AreEqual(value, *asUlong); + var mask = 0UL; + for (var i = 0; i < numBits; ++i) + { + mask |= (1UL << i); + } + + ulong readValue; + + Assert.IsTrue(reader.TryBeginRead(sizeof(ulong))); + using (var bitReader = reader.EnterBitwiseContext()) + { + bitReader.ReadBits(out readValue, numBits); + } + Assert.AreEqual(value & mask, readValue); + } + } + + [Test] + public unsafe void TestReadingBitsThrowsIfTryBeginReadNotCalled() + { + var nativeArray = new NativeArray(4, Allocator.Temp); + var reader = new FastBufferReader(nativeArray, Allocator.Temp); + nativeArray.Dispose(); + using (reader) + { + int* asInt = (int*)reader.GetUnsafePtr(); + *asInt = 0b11111111_00001010_10101011; + + Assert.Throws(() => + { + using var bitReader = reader.EnterBitwiseContext(); + bitReader.ReadBit(out bool b); + }); + + Assert.Throws(() => + { + using var bitReader = reader.EnterBitwiseContext(); + bitReader.ReadBits(out byte b, 1); + }); + + Assert.Throws(() => + { + using var bitReader = reader.EnterBitwiseContext(); + bitReader.ReadBits(out ulong ul, 1); + }); + + Assert.AreEqual(0, reader.Position); + + Assert.Throws(() => + { + Assert.IsTrue(reader.TryBeginRead(1)); + using var bitReader = reader.EnterBitwiseContext(); + ulong ul; + try + { + bitReader.ReadBits(out ul, 4); + bitReader.ReadBits(out ul, 4); + } + catch (OverflowException e) + { + Assert.Fail("Overflow exception was thrown too early."); + throw; + } + bitReader.ReadBits(out ul, 4); + }); + + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitReaderTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitReaderTests.cs.meta new file mode 100644 index 0000000000..0dc0f36136 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitReaderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 67df11865abcd5843a4e142cf6bbd901 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitWriterTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitWriterTests.cs new file mode 100644 index 0000000000..ab98d06da9 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitWriterTests.cs @@ -0,0 +1,322 @@ +using System; +using NUnit.Framework; +using Unity.Collections; + +namespace Unity.Netcode.EditorTests +{ + public class BitWriterTests + { + [Test] + public unsafe void TestWritingOneBit() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBit(true); + Assert.AreEqual(0b1, *asInt); + + bitWriter.WriteBit(true); + Assert.AreEqual(0b11, *asInt); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + Assert.AreEqual(0b1011, *asInt); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(false); + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + Assert.AreEqual(0b10001011, *asInt); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + Assert.AreEqual(0b1010_10001011, *asInt); + } + + Assert.AreEqual(2, writer.Position); + Assert.AreEqual(0b1010_10001011, *asInt); + + writer.WriteByte(0b11111111); + Assert.AreEqual(0b11111111_00001010_10001011, *asInt); + } + } + [Test] + public unsafe void TestTryBeginWriteBits() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + using (var bitWriter = writer.EnterBitwiseContext()) + { + Assert.Throws(() => writer.TryBeginWrite(1)); + Assert.Throws(() => writer.TryBeginWriteValue(1)); + Assert.IsTrue(bitWriter.TryBeginWriteBits(1)); + bitWriter.WriteBit(true); + Assert.AreEqual(0b1, *asInt); + + // Can't use Assert.Throws() because ref struct BitWriter can't be captured in a lambda + try + { + bitWriter.WriteBit(true); + } + catch (OverflowException e) + { + // Should get called here. + } + catch (Exception e) + { + throw e; + } + Assert.IsTrue(bitWriter.TryBeginWriteBits(3)); + bitWriter.WriteBit(true); + Assert.AreEqual(0b11, *asInt); + + bitWriter.WriteBit(false); + bitWriter.WriteBit(true); + Assert.AreEqual(0b1011, *asInt); + + try + { + bitWriter.WriteBits(0b11111111, 4); + } + catch (OverflowException e) + { + // Should get called here. + } + catch (Exception e) + { + throw e; + } + + try + { + bitWriter.WriteBits(0b11111111, 1); + } + catch (OverflowException e) + { + // Should get called here. + } + catch (Exception e) + { + throw e; + } + Assert.IsTrue(bitWriter.TryBeginWriteBits(3)); + + try + { + bitWriter.WriteBits(0b11111111, 4); + } + catch (OverflowException e) + { + // Should get called here. + } + catch (Exception e) + { + throw e; + } + Assert.IsTrue(bitWriter.TryBeginWriteBits(4)); + + bitWriter.WriteBits(0b11111010, 3); + + Assert.AreEqual(0b00101011, *asInt); + + Assert.IsTrue(bitWriter.TryBeginWriteBits(5)); + + bitWriter.WriteBits(0b11110101, 5); + Assert.AreEqual(0b1010_10101011, *asInt); + } + + Assert.AreEqual(2, writer.Position); + Assert.AreEqual(0b1010_10101011, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(1)); + writer.WriteByte(0b11111111); + Assert.AreEqual(0b11111111_00001010_10101011, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(1)); + writer.WriteByte(0b00000000); + Assert.AreEqual(0b11111111_00001010_10101011, *asInt); + + Assert.IsFalse(writer.TryBeginWrite(1)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + Assert.IsFalse(bitWriter.TryBeginWriteBits(1)); + } + } + } + + [Test] + public unsafe void TestWritingMultipleBits() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(0b11111111, 1); + Assert.AreEqual(0b1, *asInt); + + bitWriter.WriteBits(0b11111111, 1); + Assert.AreEqual(0b11, *asInt); + + bitWriter.WriteBits(0b11111110, 2); + Assert.AreEqual(0b1011, *asInt); + + bitWriter.WriteBits(0b11111000, 4); + Assert.AreEqual(0b10001011, *asInt); + + bitWriter.WriteBits(0b11111010, 4); + Assert.AreEqual(0b1010_10001011, *asInt); + } + + Assert.AreEqual(2, writer.Position); + Assert.AreEqual(0b1010_10001011, *asInt); + + writer.WriteByte(0b11111111); + Assert.AreEqual(0b11111111_00001010_10001011, *asInt); + } + } + + [Test] + public unsafe void TestWritingMultipleBitsFromLongs() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + Assert.IsTrue(writer.TryBeginWrite(3)); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(0b11111111UL, 1); + Assert.AreEqual(0b1, *asInt); + + bitWriter.WriteBits(0b11111111UL, 1); + Assert.AreEqual(0b11, *asInt); + + bitWriter.WriteBits(0b11111110UL, 2); + Assert.AreEqual(0b1011, *asInt); + + bitWriter.WriteBits(0b11111000UL, 4); + Assert.AreEqual(0b10001011, *asInt); + + bitWriter.WriteBits(0b11111010UL, 4); + Assert.AreEqual(0b1010_10001011, *asInt); + } + + Assert.AreEqual(2, writer.Position); + Assert.AreEqual(0b1010_10001011, *asInt); + + writer.WriteByte(0b11111111); + Assert.AreEqual(0b11111111_00001010_10001011, *asInt); + } + } + + [Test] + public unsafe void TestWritingMultipleBytesFromLongs([Range(1U, 64U)] uint numBits) + { + var writer = new FastBufferWriter(sizeof(ulong), Allocator.Temp); + using (writer) + { + ulong* asUlong = (ulong*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asUlong); + var mask = 0UL; + for (var i = 0; i < numBits; ++i) + { + mask |= (1UL << i); + } + + ulong value = 0xFFFFFFFFFFFFFFFF; + + Assert.IsTrue(writer.TryBeginWrite(sizeof(ulong))); + using (var bitWriter = writer.EnterBitwiseContext()) + { + bitWriter.WriteBits(value, numBits); + } + Assert.AreEqual(value & mask, *asUlong); + } + } + + [Test] + public unsafe void TestWritingBitsThrowsIfTryBeginWriteNotCalled() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + int* asInt = (int*)writer.GetUnsafePtr(); + + Assert.AreEqual(0, *asInt); + + Assert.Throws(() => + { + using var bitWriter = writer.EnterBitwiseContext(); + bitWriter.WriteBit(true); + }); + + Assert.Throws(() => + { + using var bitWriter = writer.EnterBitwiseContext(); + bitWriter.WriteBit(false); + }); + + Assert.Throws(() => + { + using var bitWriter = writer.EnterBitwiseContext(); + bitWriter.WriteBits(0b11111111, 1); + }); + + Assert.Throws(() => + { + using var bitWriter = writer.EnterBitwiseContext(); + bitWriter.WriteBits(0b11111111UL, 1); + }); + + Assert.AreEqual(0, writer.Position); + Assert.AreEqual(0, *asInt); + + writer.WriteByteSafe(0b11111111); + Assert.AreEqual(0b11111111, *asInt); + + + Assert.Throws(() => + { + Assert.IsTrue(writer.TryBeginWrite(1)); + using var bitWriter = writer.EnterBitwiseContext(); + try + { + bitWriter.WriteBits(0b11111111UL, 4); + bitWriter.WriteBits(0b11111111UL, 4); + } + catch (OverflowException e) + { + Assert.Fail("Overflow exception was thrown too early."); + throw; + } + bitWriter.WriteBits(0b11111111UL, 1); + }); + + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitWriterTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitWriterTests.cs.meta new file mode 100644 index 0000000000..3a8da0e2cc --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BitWriterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fed657e0516a72f469fbf886e3e5149a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BufferSerializerTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BufferSerializerTests.cs new file mode 100644 index 0000000000..1847dd0954 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BufferSerializerTests.cs @@ -0,0 +1,644 @@ +using System; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class BufferSerializerTests + { + [Test] + public void TestIsReaderIsWriter() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + Assert.IsFalse(serializer.IsReader); + Assert.IsTrue(serializer.IsWriter); + } + byte[] readBuffer = new byte[4]; + var reader = new FastBufferReader(readBuffer, Allocator.Temp); + using (reader) + { + var serializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + Assert.IsTrue(serializer.IsReader); + Assert.IsFalse(serializer.IsWriter); + } + } + [Test] + public unsafe void TestGetUnderlyingStructs() + { + var writer = new FastBufferWriter(4, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + ref FastBufferWriter underlyingWriter = ref serializer.GetFastBufferWriter(); + fixed (FastBufferWriter* ptr = &underlyingWriter) + { + Assert.IsTrue(ptr == &writer); + } + // Can't use Assert.Throws() because ref structs can't be passed into lambdas. + try + { + serializer.GetFastBufferReader(); + } + catch (InvalidOperationException) + { + // pass + } + + } + byte[] readBuffer = new byte[4]; + var reader = new FastBufferReader(readBuffer, Allocator.Temp); + using (reader) + { + var serializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + ref FastBufferReader underlyingReader = ref serializer.GetFastBufferReader(); + fixed (FastBufferReader* ptr = &underlyingReader) + { + Assert.IsTrue(ptr == &reader); + } + // Can't use Assert.Throws() because ref structs can't be passed into lambdas. + try + { + serializer.GetFastBufferWriter(); + } + catch (InvalidOperationException) + { + // pass + } + } + } + + // Not reimplementing the entire suite of all value tests for BufferSerializer since they're already tested + // for the underlying structures. These are just basic tests to make sure the correct underlying functions + // are being called. + [Test] + public void TestSerializingObjects() + { + var random = new Random(); + int value = random.Next(); + object asObj = value; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + serializer.SerializeValue(ref asObj, typeof(int)); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + object readValue = 0; + deserializer.SerializeValue(ref readValue, typeof(int)); + + Assert.AreEqual(value, readValue); + } + } + } + + [Test] + public void TestSerializingValues() + { + var random = new Random(); + int value = random.Next(); + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + serializer.SerializeValue(ref value); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + int readValue = 0; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingBytes() + { + var random = new Random(); + byte value = (byte)random.Next(); + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + serializer.SerializeValue(ref value); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + byte readValue = 0; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingArrays() + { + var random = new Random(); + int[] value = { random.Next(), random.Next(), random.Next() }; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + serializer.SerializeValue(ref value); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + int[] readValue = null; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingStrings([Values] bool oneBytChars) + { + string value = "I am a test string"; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + serializer.SerializeValue(ref value, oneBytChars); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + string readValue = null; + deserializer.SerializeValue(ref readValue, oneBytChars); + + Assert.AreEqual(value, readValue); + } + } + } + + + [Test] + public void TestSerializingValuesPreChecked() + { + var random = new Random(); + int value = random.Next(); + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + try + { + serializer.SerializeValuePreChecked(ref value); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + serializer.SerializeValuePreChecked(ref value); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + int readValue = 0; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingBytesPreChecked() + { + var random = new Random(); + byte value = (byte)random.Next(); + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + try + { + serializer.SerializeValuePreChecked(ref value); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + serializer.SerializeValuePreChecked(ref value); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + byte readValue = 0; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingArraysPreChecked() + { + var random = new Random(); + int[] value = { random.Next(), random.Next(), random.Next() }; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + try + { + serializer.SerializeValuePreChecked(ref value); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + serializer.SerializeValuePreChecked(ref value); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + int[] readValue = null; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriter.GetWriteSize(value))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(value, readValue); + } + } + } + [Test] + public void TestSerializingStringsPreChecked([Values] bool oneBytChars) + { + string value = "I am a test string"; + + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + try + { + serializer.SerializeValuePreChecked(ref value, oneBytChars); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriter.GetWriteSize(value, oneBytChars))); + serializer.SerializeValuePreChecked(ref value, oneBytChars); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + string readValue = null; + try + { + deserializer.SerializeValuePreChecked(ref readValue, oneBytChars); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriter.GetWriteSize(value, oneBytChars))); + deserializer.SerializeValuePreChecked(ref readValue, oneBytChars); + + Assert.AreEqual(value, readValue); + } + } + } + + private delegate void GameObjectTestDelegate(GameObject obj, NetworkBehaviour networkBehaviour, + NetworkObject networkObject); + private void RunGameObjectTest(GameObjectTestDelegate testCode) + { + var obj = new GameObject("Object"); + var networkBehaviour = obj.AddComponent(); + var networkObject = obj.AddComponent(); + // Create networkManager component + var networkManager = obj.AddComponent(); + networkManager.SetSingleton(); + networkObject.NetworkManagerOwner = networkManager; + + // Set the NetworkConfig + networkManager.NetworkConfig = new NetworkConfig() + { + // Set transport + NetworkTransport = obj.AddComponent() + }; + + networkManager.StartServer(); + + try + { + testCode(obj, networkBehaviour, networkObject); + } + finally + { + UnityEngine.Object.DestroyImmediate(obj); + networkManager.Shutdown(); + } + } + + [Test] + public void TestSerializingGameObjects() + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + serializer.SerializeValue(ref obj); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + GameObject readValue = null; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(obj, readValue); + } + } + } + ); + } + + [Test] + public void TestSerializingNetworkObjects() + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + serializer.SerializeValue(ref networkObject); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + NetworkObject readValue = null; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(networkObject, readValue); + } + } + } + ); + } + + [Test] + public void TestSerializingNetworkBehaviours() + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + serializer.SerializeValue(ref networkBehaviour); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + NetworkBehaviour readValue = null; + deserializer.SerializeValue(ref readValue); + + Assert.AreEqual(networkBehaviour, readValue); + } + } + } + ); + } + + [Test] + public void TestSerializingGameObjectsPreChecked() + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + try + { + serializer.SerializeValuePreChecked(ref obj); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriterExtensions.GetWriteSize(obj))); + serializer.SerializeValuePreChecked(ref obj); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + GameObject readValue = null; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriterExtensions.GetWriteSize(readValue))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(obj, readValue); + } + } + } + ); + } + + [Test] + public void TestSerializingNetworkObjectsPreChecked() + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + try + { + serializer.SerializeValuePreChecked(ref networkObject); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriterExtensions.GetWriteSize(networkObject))); + serializer.SerializeValuePreChecked(ref networkObject); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + NetworkObject readValue = null; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriterExtensions.GetWriteSize(readValue))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(networkObject, readValue); + } + } + } + ); + } + + [Test] + public void TestSerializingNetworkBehavioursPreChecked() + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + var serializer = + new BufferSerializer(new BufferSerializerWriter(ref writer)); + try + { + serializer.SerializeValuePreChecked(ref networkBehaviour); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(serializer.PreCheck(FastBufferWriterExtensions.GetWriteSize(networkBehaviour))); + serializer.SerializeValuePreChecked(ref networkBehaviour); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var deserializer = + new BufferSerializer(new BufferSerializerReader(ref reader)); + NetworkBehaviour readValue = null; + try + { + deserializer.SerializeValuePreChecked(ref readValue); + } + catch (OverflowException e) + { + // Pass + } + + Assert.IsTrue(deserializer.PreCheck(FastBufferWriterExtensions.GetWriteSize(readValue))); + deserializer.SerializeValuePreChecked(ref readValue); + + Assert.AreEqual(networkBehaviour, readValue); + } + } + } + ); + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BufferSerializerTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BufferSerializerTests.cs.meta new file mode 100644 index 0000000000..cb62c7e2d2 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BufferSerializerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0cf899c97866c76498b71585a61a8142 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BytePackerTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BytePackerTests.cs new file mode 100644 index 0000000000..1c2ff159e9 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BytePackerTests.cs @@ -0,0 +1,1268 @@ +using System; +using System.Linq; +using System.Reflection; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class BytePackerTests + { + #region Test Types + + private enum ByteEnum : byte + { + A, + B, + C + } + + private enum SByteEnum : sbyte + { + A, + B, + C + } + + private enum ShortEnum : short + { + A, + B, + C + } + + private enum UShortEnum : ushort + { + A, + B, + C + } + + private enum IntEnum + { + A, + B, + C + } + + private enum UIntEnum : uint + { + A, + B, + C + } + + private enum LongEnum : long + { + A, + B, + C + } + + private enum ULongEnum : ulong + { + A, + B, + C + } + + public enum WriteType + { + WriteDirect, + WriteAsObject + } + + #endregion + + private void CheckUnsignedPackedSize64(ref FastBufferWriter writer, ulong value) + { + + if (value <= 240) + { + Assert.AreEqual(1, writer.Position); + } + else if (value <= 2287) + { + Assert.AreEqual(2, writer.Position); + } + else + { + Assert.AreEqual(BitCounter.GetUsedByteCount(value) + 1, writer.Position); + } + } + + private void CheckUnsignedPackedValue64(ref FastBufferWriter writer, ulong value) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(ref reader, out ulong readValue); + Assert.AreEqual(readValue, value); + } + } + + private void CheckUnsignedPackedSize32(ref FastBufferWriter writer, uint value) + { + + if (value <= 240) + { + Assert.AreEqual(1, writer.Position); + } + else if (value <= 2287) + { + Assert.AreEqual(2, writer.Position); + } + else + { + Assert.AreEqual(BitCounter.GetUsedByteCount(value) + 1, writer.Position); + } + } + + private void CheckUnsignedPackedValue32(ref FastBufferWriter writer, uint value) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(ref reader, out uint readValue); + Assert.AreEqual(readValue, value); + } + } + + private void CheckSignedPackedSize64(ref FastBufferWriter writer, long value) + { + ulong asUlong = Arithmetic.ZigZagEncode(value); + + if (asUlong <= 240) + { + Assert.AreEqual(1, writer.Position); + } + else if (asUlong <= 2287) + { + Assert.AreEqual(2, writer.Position); + } + else + { + Assert.AreEqual(BitCounter.GetUsedByteCount(asUlong) + 1, writer.Position); + } + } + + private void CheckSignedPackedValue64(ref FastBufferWriter writer, long value) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(ref reader, out long readValue); + Assert.AreEqual(readValue, value); + } + } + + private void CheckSignedPackedSize32(ref FastBufferWriter writer, int value) + { + ulong asUlong = Arithmetic.ZigZagEncode(value); + + if (asUlong <= 240) + { + Assert.AreEqual(1, writer.Position); + } + else if (asUlong <= 2287) + { + Assert.AreEqual(2, writer.Position); + } + else + { + Assert.AreEqual(BitCounter.GetUsedByteCount(asUlong) + 1, writer.Position); + } + } + + private void CheckSignedPackedValue32(ref FastBufferWriter writer, int value) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(ref reader, out int readValue); + Assert.AreEqual(readValue, value); + } + } + + private unsafe void VerifyBytewiseEquality(T value, T otherValue) where T : unmanaged + { + byte* asBytePointer = (byte*)&value; + byte* otherBytePointer = (byte*)&otherValue; + for (var i = 0; i < sizeof(T); ++i) + { + Assert.AreEqual(asBytePointer[i], otherBytePointer[i]); + } + } + + private unsafe void RunTypeTest(T value) where T : unmanaged + { + var writer = new FastBufferWriter(sizeof(T) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteValuePacked(ref writer, (dynamic)value); + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + + var outVal = new T(); + MethodInfo method; + if (value is Enum) + { + method = typeof(ByteUnpacker).GetMethods().Single(x => + x.Name == "ReadValuePacked" && x.IsGenericMethodDefinition) + .MakeGenericMethod(typeof(T)); + } + else + { + method = typeof(ByteUnpacker).GetMethod("ReadValuePacked", + new[] { typeof(FastBufferReader).MakeByRefType(), typeof(T).MakeByRefType() }); + } + + object[] args = { reader, outVal }; + method.Invoke(null, args); + outVal = (T)args[1]; + Assert.AreEqual(value, outVal); + VerifyBytewiseEquality(value, outVal); + } + } + } + + private unsafe void RunObjectTypeTest(T value) where T : unmanaged + { + var writer = new FastBufferWriter(sizeof(T) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteObjectPacked(ref writer, value); + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + + ByteUnpacker.ReadObjectPacked(ref reader, out object outVal, typeof(T)); + Assert.AreEqual(value, outVal); + VerifyBytewiseEquality(value, (T)outVal); + } + } + } + + + + [Test] + public void TestPacking64BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(9); + ulong value = 0; + BytePacker.WriteValuePacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + + for (var i = 0; i < 64; ++i) + { + value = 1UL << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, value); + CheckUnsignedPackedSize64(ref writer, value); + CheckUnsignedPackedValue64(ref writer, value); + for (var j = 0; j < 8; ++j) + { + value = (1UL << i) | (1UL << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, value); + CheckUnsignedPackedSize64(ref writer, value); + CheckUnsignedPackedValue64(ref writer, value); + } + } + } + } + + [Test] + public void TestPacking32BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(9); + uint value = 0; + BytePacker.WriteValuePacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + + for (var i = 0; i < 64; ++i) + { + value = 1U << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, value); + CheckUnsignedPackedSize32(ref writer, value); + CheckUnsignedPackedValue32(ref writer, value); + for (var j = 0; j < 8; ++j) + { + value = (1U << i) | (1U << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, value); + CheckUnsignedPackedSize32(ref writer, value); + CheckUnsignedPackedValue32(ref writer, value); + } + } + } + } + + [Test] + public void TestPacking64BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(9); + long value = 0; + BytePacker.WriteValuePacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + + for (var i = 0; i < 64; ++i) + { + value = 1L << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, value); + CheckSignedPackedSize64(ref writer, value); + CheckSignedPackedValue64(ref writer, value); + + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, -value); + CheckSignedPackedSize64(ref writer, -value); + CheckSignedPackedValue64(ref writer, -value); + for (var j = 0; j < 8; ++j) + { + value = (1L << i) | (1L << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, value); + CheckSignedPackedSize64(ref writer, value); + CheckSignedPackedValue64(ref writer, value); + + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, -value); + CheckSignedPackedSize64(ref writer, -value); + CheckSignedPackedValue64(ref writer, -value); + } + } + } + } + + [Test] + public void TestPacking32BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(5); + int value = 0; + BytePacker.WriteValuePacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + + for (var i = 0; i < 64; ++i) + { + value = 1 << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, value); + CheckSignedPackedSize32(ref writer, value); + CheckSignedPackedValue32(ref writer, value); + + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, -value); + CheckSignedPackedSize32(ref writer, -value); + CheckSignedPackedValue32(ref writer, -value); + for (var j = 0; j < 8; ++j) + { + value = (1 << i) | (1 << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, value); + CheckSignedPackedSize32(ref writer, value); + CheckSignedPackedValue32(ref writer, value); + + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValuePacked(ref writer, -value); + CheckSignedPackedSize32(ref writer, -value); + CheckSignedPackedValue32(ref writer, -value); + } + } + } + } + + private int GetByteCount61Bits(ulong value) + { + + if (value <= 0b0001_1111) + { + return 1; + } + + if (value <= 0b0001_1111_1111_1111) + { + return 2; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111) + { + return 3; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111_1111_1111) + { + return 4; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111_1111_1111_1111_1111) + { + return 5; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + { + return 6; + } + + if (value <= 0b0001_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111) + { + return 7; + } + + return 8; + } + + private int GetByteCount30Bits(uint value) + { + + if (value <= 0b0011_1111) + { + return 1; + } + + if (value <= 0b0011_1111_1111_1111) + { + return 2; + } + + if (value <= 0b0011_1111_1111_1111_1111_1111) + { + return 3; + } + + return 4; + } + + private int GetByteCount15Bits(ushort value) + { + + if (value <= 0b0111_1111) + { + return 1; + } + + return 2; + } + + private ulong Get61BitEncodedValue(ref FastBufferWriter writer) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(ref reader, out ulong value); + return value; + } + } + + private long Get60BitSignedEncodedValue(ref FastBufferWriter writer) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(ref reader, out long value); + return value; + } + } + + private uint Get30BitEncodedValue(ref FastBufferWriter writer) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(ref reader, out uint value); + return value; + } + } + + private int Get29BitSignedEncodedValue(ref FastBufferWriter writer) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(ref reader, out int value); + return value; + } + } + + private ushort Get15BitEncodedValue(ref FastBufferWriter writer) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(ref reader, out ushort value); + return value; + } + } + + private short Get14BitSignedEncodedValue(ref FastBufferWriter writer) + { + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValueBitPacked(ref reader, out short value); + return value; + } + } + + [Test] + public void TestBitPacking61BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(8); + ulong value = 0; + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b111); + Assert.AreEqual(value, Get61BitEncodedValue(ref writer)); + + for (var i = 0; i < 61; ++i) + { + value = 1UL << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount61Bits(value), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount61Bits(value) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get61BitEncodedValue(ref writer)); + + for (var j = 0; j < 8; ++j) + { + value = (1UL << i) | (1UL << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount61Bits(value), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount61Bits(value) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get61BitEncodedValue(ref writer)); + } + } + + Assert.Throws(() => { BytePacker.WriteValueBitPacked(ref writer, 1UL << 61); }); + } + } + + [Test] + public void TestBitPacking60BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(8); + long value = 0; + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b111); + Assert.AreEqual(value, Get60BitSignedEncodedValue(ref writer)); + + for (var i = 0; i < 61; ++i) + { + value = 1U << i; + ulong zzvalue = Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount61Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount61Bits(zzvalue) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get60BitSignedEncodedValue(ref writer)); + + value = -value; + zzvalue = Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount61Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount61Bits(zzvalue) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get60BitSignedEncodedValue(ref writer)); + + for (var j = 0; j < 8; ++j) + { + value = (1U << i) | (1U << j); + zzvalue = Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount61Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount61Bits(zzvalue) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get60BitSignedEncodedValue(ref writer)); + + value = -value; + zzvalue = Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount61Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount61Bits(zzvalue) - 1, writer.ToArray()[0] & 0b111, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get60BitSignedEncodedValue(ref writer)); + } + } + + Assert.Throws(() => { BytePacker.WriteValueBitPacked(ref writer, 1UL << 61); }); + } + } + + [Test] + public void TestBitPacking30BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(4); + uint value = 0; + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b11); + Assert.AreEqual(value, Get30BitEncodedValue(ref writer)); + + for (var i = 0; i < 30; ++i) + { + value = 1U << i; + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount30Bits(value), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount30Bits(value) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get30BitEncodedValue(ref writer)); + + for (var j = 0; j < 8; ++j) + { + value = (1U << i) | (1U << j); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount30Bits(value), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount30Bits(value) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get30BitEncodedValue(ref writer)); + } + } + + Assert.Throws(() => { BytePacker.WriteValueBitPacked(ref writer, 1U << 30); }); + } + } + + [Test] + public void TestBitPacking29BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(4); + int value = 0; + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b11); + Assert.AreEqual(value, Get30BitEncodedValue(ref writer)); + + for (var i = 0; i < 29; ++i) + { + value = 1 << i; + uint zzvalue = (uint)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount30Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount30Bits(zzvalue) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get29BitSignedEncodedValue(ref writer)); + + value = -value; + zzvalue = (uint)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount30Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount30Bits(zzvalue) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get29BitSignedEncodedValue(ref writer)); + + for (var j = 0; j < 8; ++j) + { + value = (1 << i) | (1 << j); + zzvalue = (uint)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount30Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount30Bits(zzvalue) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get29BitSignedEncodedValue(ref writer)); + + value = -value; + zzvalue = (uint)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount30Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount30Bits(zzvalue) - 1, writer.ToArray()[0] & 0b11, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get29BitSignedEncodedValue(ref writer)); + } + } + } + } + + [Test] + public void TestBitPacking15BitsUnsigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(2); + ushort value = 0; + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b1); + Assert.AreEqual(value, Get15BitEncodedValue(ref writer)); + + for (var i = 0; i < 15; ++i) + { + value = (ushort)(1U << i); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount15Bits(value), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount15Bits(value) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get15BitEncodedValue(ref writer)); + + for (var j = 0; j < 8; ++j) + { + value = (ushort)((1U << i) | (1U << j)); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount15Bits(value), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount15Bits(value) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get15BitEncodedValue(ref writer)); + } + } + + Assert.Throws(() => { BytePacker.WriteValueBitPacked(ref writer, (ushort)(1U << 15)); }); + } + } + [Test] + public void TestBitPacking14BitsSigned() + { + var writer = new FastBufferWriter(9, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(2); + short value = 0; + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(1, writer.Position); + Assert.AreEqual(0, writer.ToArray()[0] & 0b1); + Assert.AreEqual(value, Get15BitEncodedValue(ref writer)); + + for (var i = 0; i < 14; ++i) + { + value = (short)(1 << i); + ushort zzvalue = (ushort)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount15Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount15Bits(zzvalue) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get14BitSignedEncodedValue(ref writer)); + + value = (short)-value; + zzvalue = (ushort)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount15Bits(zzvalue), writer.Position, $"Failed on {value} ({i})"); + Assert.AreEqual(GetByteCount15Bits(zzvalue) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i})"); + Assert.AreEqual(value, Get14BitSignedEncodedValue(ref writer)); + + for (var j = 0; j < 8; ++j) + { + value = (short)((1 << i) | (1 << j)); + zzvalue = (ushort)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount15Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount15Bits(zzvalue) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get14BitSignedEncodedValue(ref writer)); + + value = (short)-value; + zzvalue = (ushort)Arithmetic.ZigZagEncode(value); + writer.Seek(0); + writer.Truncate(); + BytePacker.WriteValueBitPacked(ref writer, value); + Assert.AreEqual(GetByteCount15Bits(zzvalue), writer.Position, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(GetByteCount15Bits(zzvalue) - 1, writer.ToArray()[0] & 0b1, $"Failed on {value} ({i}, {j})"); + Assert.AreEqual(value, Get14BitSignedEncodedValue(ref writer)); + } + } + } + } + + [Test] + public void TestPackingBasicTypes( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D))] + Type testType, + [Values] WriteType writeType) + { + var random = new Random(); + + if (testType == typeof(byte)) + { + byte b = (byte)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(b); + } + else + { + RunObjectTypeTest(b); + } + } + else if (testType == typeof(sbyte)) + { + sbyte sb = (sbyte)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(sb); + } + else + { + RunObjectTypeTest(sb); + } + } + else if (testType == typeof(short)) + { + short s = (short)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(s); + } + else + { + RunObjectTypeTest(s); + } + } + else if (testType == typeof(ushort)) + { + ushort us = (ushort)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(us); + } + else + { + RunObjectTypeTest(us); + } + } + else if (testType == typeof(int)) + { + int i = random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(i); + } + else + { + RunObjectTypeTest(i); + } + } + else if (testType == typeof(uint)) + { + uint ui = (uint)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(ui); + } + else + { + RunObjectTypeTest(ui); + } + } + else if (testType == typeof(long)) + { + long l = ((long)random.Next() << 32) + random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(l); + } + else + { + RunObjectTypeTest(l); + } + } + else if (testType == typeof(ulong)) + { + ulong ul = ((ulong)random.Next() << 32) + (ulong)random.Next(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(ul); + } + else + { + RunObjectTypeTest(ul); + } + } + else if (testType == typeof(bool)) + { + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(true); + } + else + { + RunObjectTypeTest(true); + } + } + else if (testType == typeof(char)) + { + char c = 'a'; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(c); + } + else + { + RunObjectTypeTest(c); + } + + c = '\u263a'; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(c); + } + else + { + RunObjectTypeTest(c); + } + } + else if (testType == typeof(float)) + { + float f = (float)random.NextDouble(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(f); + } + else + { + RunObjectTypeTest(f); + } + } + else if (testType == typeof(double)) + { + double d = random.NextDouble(); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(d); + } + else + { + RunObjectTypeTest(d); + } + } + else if (testType == typeof(ByteEnum)) + { + ByteEnum e = ByteEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + else + { + RunObjectTypeTest(e); + } + } + else if (testType == typeof(SByteEnum)) + { + SByteEnum e = SByteEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + else + { + RunObjectTypeTest(e); + } + } + else if (testType == typeof(ShortEnum)) + { + ShortEnum e = ShortEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + else + { + RunObjectTypeTest(e); + } + } + else if (testType == typeof(UShortEnum)) + { + UShortEnum e = UShortEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + else + { + RunObjectTypeTest(e); + } + } + else if (testType == typeof(IntEnum)) + { + IntEnum e = IntEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + else + { + RunObjectTypeTest(e); + } + } + else if (testType == typeof(UIntEnum)) + { + UIntEnum e = UIntEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + else + { + RunObjectTypeTest(e); + } + } + else if (testType == typeof(LongEnum)) + { + LongEnum e = LongEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + else + { + RunObjectTypeTest(e); + } + } + else if (testType == typeof(ULongEnum)) + { + ULongEnum e = ULongEnum.C; + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(e); + } + else + { + RunObjectTypeTest(e); + } + } + else if (testType == typeof(Vector2)) + { + var v = new Vector2((float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + else + { + RunObjectTypeTest(v); + } + } + else if (testType == typeof(Vector3)) + { + var v = new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + else + { + RunObjectTypeTest(v); + } + } + else if (testType == typeof(Vector4)) + { + var v = new Vector4((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + else + { + RunObjectTypeTest(v); + } + } + else if (testType == typeof(Quaternion)) + { + var v = new Quaternion((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + else + { + RunObjectTypeTest(v); + } + } + else if (testType == typeof(Color)) + { + var v = new Color((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + else + { + RunObjectTypeTest(v); + } + } + else if (testType == typeof(Color32)) + { + var v = new Color32((byte)random.Next(), (byte)random.Next(), (byte)random.Next(), (byte)random.Next()); + if (writeType == WriteType.WriteDirect) + { + RunTypeTest(v); + } + else + { + RunObjectTypeTest(v); + } + } + else if (testType == typeof(Ray)) + { + // Rays need special handling on the equality checks because the constructor normalizes direction + // Which can cause slight variations in the result + var v = new Ray( + new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble()), + new Vector3((float)random.NextDouble(), (float)random.NextDouble(), (float)random.NextDouble())); + if (writeType == WriteType.WriteDirect) + { + unsafe + { + var writer = new FastBufferWriter(sizeof(Ray) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteValuePacked(ref writer, v); + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(ref reader, out Ray outVal); + Assert.AreEqual(v.origin, outVal.origin); + Assert.AreEqual(v.direction.x, outVal.direction.x, 0.00001); + Assert.AreEqual(v.direction.y, outVal.direction.y, 0.00001); + Assert.AreEqual(v.direction.z, outVal.direction.z, 0.00001); + } + } + } + } + else + { + unsafe + { + var writer = new FastBufferWriter(sizeof(Ray) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteObjectPacked(ref writer, v); + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadObjectPacked(ref reader, out object outVal, typeof(Ray)); + Assert.AreEqual(v.origin, ((Ray)outVal).origin); + Assert.AreEqual(v.direction.x, ((Ray)outVal).direction.x, 0.00001); + Assert.AreEqual(v.direction.y, ((Ray)outVal).direction.y, 0.00001); + Assert.AreEqual(v.direction.z, ((Ray)outVal).direction.z, 0.00001); + } + } + } + } + } + else if (testType == typeof(Ray2D)) + { + // Rays need special handling on the equality checks because the constructor normalizes direction + // Which can cause slight variations in the result + var v = new Ray2D( + new Vector2((float)random.NextDouble(), (float)random.NextDouble()), + new Vector2((float)random.NextDouble(), (float)random.NextDouble())); + if (writeType == WriteType.WriteDirect) + { + unsafe + { + var writer = new FastBufferWriter(sizeof(Ray2D) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteValuePacked(ref writer, v); + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadValuePacked(ref reader, out Ray2D outVal); + Assert.AreEqual(v.origin, outVal.origin); + Assert.AreEqual(v.direction.x, outVal.direction.x, 0.00001); + Assert.AreEqual(v.direction.y, outVal.direction.y, 0.00001); + } + } + } + } + else + { + unsafe + { + var writer = new FastBufferWriter(sizeof(Ray2D) * 2, Allocator.Temp); + using (writer) + { + BytePacker.WriteObjectPacked(ref writer, v); + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + ByteUnpacker.ReadObjectPacked(ref reader, out object outVal, typeof(Ray2D)); + Assert.AreEqual(v.origin, ((Ray2D)outVal).origin); + Assert.AreEqual(v.direction.x, ((Ray2D)outVal).direction.x, 0.00001); + Assert.AreEqual(v.direction.y, ((Ray2D)outVal).direction.y, 0.00001); + } + } + } + } + } + else + { + Assert.Fail("No type handler was provided for this type in the test!"); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BytePackerTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BytePackerTests.cs.meta new file mode 100644 index 0000000000..4c07179b82 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/BytePackerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b50db056cd7443b4eb2e00b603d4c15c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs new file mode 100644 index 0000000000..0f3e36e2e8 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs @@ -0,0 +1,1027 @@ +using System; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class FastBufferReaderTests : BaseFastBufferReaderWriterTest + { + #region Common Checks + private void WriteCheckBytes(ref FastBufferWriter writer, int writeSize, string failMessage = "") + { + Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission"); + writer.WriteValue((byte)0x80); + Assert.AreEqual(writeSize + 1, writer.Position, failMessage); + Assert.AreEqual(writeSize + 1, writer.Length, failMessage); + writer.WriteValue((byte)0xFF); + Assert.AreEqual(writeSize + 2, writer.Position, failMessage); + Assert.AreEqual(writeSize + 2, writer.Length, failMessage); + } + + private void VerifyCheckBytes(ref FastBufferReader reader, int checkPosition, string failMessage = "") + { + reader.Seek(checkPosition); + reader.TryBeginRead(2); + + reader.ReadByte(out byte value); + Assert.AreEqual(0x80, value, failMessage); + reader.ReadByte(out value); + Assert.AreEqual(0xFF, value, failMessage); + } + + private void VerifyPositionAndLength(ref FastBufferReader reader, int length, string failMessage = "") + { + Assert.AreEqual(0, reader.Position, failMessage); + Assert.AreEqual(length, reader.Length, failMessage); + } + + private FastBufferReader CommonChecks(ref FastBufferWriter writer, T valueToTest, int writeSize, string failMessage = "") where T : unmanaged + { + WriteCheckBytes(ref writer, writeSize, failMessage); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + + VerifyPositionAndLength(ref reader, writer.Length, failMessage); + + VerifyCheckBytes(ref reader, writeSize, failMessage); + + reader.Seek(0); + + return reader; + } + #endregion + + #region Generic Checks + protected override unsafe void RunTypeTest(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + Assert.AreEqual(sizeof(T), writeSize); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); + + var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; + + writer.WriteValue(valueToTest); + + var reader = CommonChecks(ref writer, valueToTest, writeSize, failMessage); + + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(FastBufferWriter.GetWriteSize())); + reader.ReadValue(out T result); + Assert.AreEqual(valueToTest, result); + } + } + } + protected override unsafe void RunTypeTestSafe(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + Assert.AreEqual(sizeof(T), writeSize); + + var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; + + writer.WriteValueSafe(valueToTest); + + + var reader = CommonChecks(ref writer, valueToTest, writeSize, failMessage); + + using (reader) + { + reader.ReadValueSafe(out T result); + Assert.AreEqual(valueToTest, result); + } + } + } + + protected override unsafe void RunObjectTypeTest(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + Assert.AreEqual(sizeof(T), writeSize); + + var failMessage = $"RunObjectTypeTest failed with type {typeof(T)} and value {valueToTest}"; + writer.WriteObject(valueToTest); + + var reader = CommonChecks(ref writer, valueToTest, writeSize, failMessage); + + using (reader) + { + reader.ReadObject(out object result, typeof(T)); + Assert.AreEqual(valueToTest, result); + } + } + } + + private void VerifyArrayEquality(T[] value, T[] compareValue, int offset) where T : unmanaged + { + Assert.AreEqual(value.Length, compareValue.Length); + + for (var i = 0; i < value.Length; ++i) + { + Assert.AreEqual(value[i], compareValue[i]); + } + } + + protected override unsafe void RunTypeArrayTest(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + using (writer) + { + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); + + writer.WriteValue(valueToTest); + + WriteCheckBytes(ref writer, writeSize); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(ref reader, writer.Length); + + Assert.IsTrue(reader.TryBeginRead(writeSize)); + reader.ReadValue(out T[] result); + VerifyArrayEquality(valueToTest, result, 0); + + VerifyCheckBytes(ref reader, writeSize); + } + } + } + + protected override unsafe void RunTypeArrayTestSafe(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + using (writer) + { + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + + writer.WriteValueSafe(valueToTest); + + WriteCheckBytes(ref writer, writeSize); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(ref reader, writer.Length); + + reader.ReadValueSafe(out T[] result); + VerifyArrayEquality(valueToTest, result, 0); + + VerifyCheckBytes(ref reader, writeSize); + } + } + } + + protected override unsafe void RunObjectTypeArrayTest(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + // Extra byte for WriteObject adding isNull flag + var writer = new FastBufferWriter(writeSize + 3, Allocator.Temp); + using (writer) + { + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + + writer.WriteObject(valueToTest); + + WriteCheckBytes(ref writer, writeSize + 1); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(ref reader, writer.Length); + + reader.ReadObject(out object result, typeof(T[])); + VerifyArrayEquality(valueToTest, (T[])result, 0); + + VerifyCheckBytes(ref reader, writeSize + 1); + } + } + } + #endregion + + #region Tests + [Test] + public void GivenFastBufferWriterContainingValue_WhenReadingUnmanagedType_ValueMatchesWhatWasWritten( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), typeof(TestStruct))] + Type testType, + [Values] WriteType writeType) + { + BaseTypeTest(testType, writeType); + } + + [Test] + public void GivenFastBufferWriterContainingValue_WhenReadingArrayOfUnmanagedElementType_ValueMatchesWhatWasWritten( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), typeof(TestStruct))] + Type testType, + [Values] WriteType writeType) + { + BaseArrayTypeTest(testType, writeType); + } + + [TestCase(false, WriteType.WriteDirect)] + [TestCase(false, WriteType.WriteSafe)] + [TestCase(false, WriteType.WriteAsObject)] + [TestCase(true, WriteType.WriteDirect)] + [TestCase(true, WriteType.WriteSafe)] + public void GivenFastBufferWriterContainingValue_WhenReadingString_ValueMatchesWhatWasWritten(bool oneByteChars, WriteType writeType) + { + string valueToTest = "Hello, I am a test string!"; + + var serializedValueSize = FastBufferWriter.GetWriteSize(valueToTest, oneByteChars); + + var writer = new FastBufferWriter(serializedValueSize + 3, Allocator.Temp); + using (writer) + { + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(writer.TryBeginWrite(serializedValueSize + 2), "Writer denied write permission"); + writer.WriteValue(valueToTest, oneByteChars); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(valueToTest, oneByteChars); + break; + case WriteType.WriteAsObject: + writer.WriteObject(valueToTest); + serializedValueSize += 1; + break; + } + + WriteCheckBytes(ref writer, serializedValueSize); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(ref reader, writer.Length); + + string result = null; + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(reader.TryBeginRead(serializedValueSize + 2), "Reader denied read permission"); + reader.ReadValue(out result, oneByteChars); + break; + case WriteType.WriteSafe: + reader.ReadValueSafe(out result, oneByteChars); + break; + case WriteType.WriteAsObject: + reader.ReadObject(out object resultObj, typeof(string), oneByteChars); + result = (string)resultObj; + break; + } + Assert.AreEqual(valueToTest, result); + + VerifyCheckBytes(ref reader, serializedValueSize); + } + } + } + + + [TestCase(1, 0)] + [TestCase(2, 0)] + [TestCase(3, 0)] + [TestCase(4, 0)] + [TestCase(5, 0)] + [TestCase(6, 0)] + [TestCase(7, 0)] + [TestCase(8, 0)] + + [TestCase(1, 1)] + [TestCase(2, 1)] + [TestCase(3, 1)] + [TestCase(4, 1)] + [TestCase(5, 1)] + [TestCase(6, 1)] + [TestCase(7, 1)] + + [TestCase(1, 2)] + [TestCase(2, 2)] + [TestCase(3, 2)] + [TestCase(4, 2)] + [TestCase(5, 2)] + [TestCase(6, 2)] + + [TestCase(1, 3)] + [TestCase(2, 3)] + [TestCase(3, 3)] + [TestCase(4, 3)] + [TestCase(5, 3)] + + [TestCase(1, 4)] + [TestCase(2, 4)] + [TestCase(3, 4)] + [TestCase(4, 4)] + + [TestCase(1, 5)] + [TestCase(2, 5)] + [TestCase(3, 5)] + + [TestCase(1, 6)] + [TestCase(2, 6)] + + [TestCase(1, 7)] + public void GivenFastBufferWriterContainingValue_WhenReadingPartialValue_ValueMatchesWhatWasWritten(int count, int offset) + { + var random = new Random(); + var valueToTest = ((ulong)random.Next() << 32) + (ulong)random.Next(); + var writer = new FastBufferWriter(sizeof(ulong) + 2, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(count + 2), "Writer denied write permission"); + writer.WritePartialValue(valueToTest, count, offset); + + var failMessage = $"TestReadingPartialValues failed with value {valueToTest}"; + WriteCheckBytes(ref writer, count, failMessage); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + VerifyPositionAndLength(ref reader, writer.Length, failMessage); + Assert.IsTrue(reader.TryBeginRead(count + 2), "Reader denied read permission"); + + ulong mask = 0; + for (var i = 0; i < count; ++i) + { + mask = (mask << 8) | 0b11111111; + } + + mask <<= (offset * 8); + + reader.ReadPartialValue(out ulong result, count, offset); + Assert.AreEqual(valueToTest & mask, result & mask, failMessage); + VerifyCheckBytes(ref reader, count, failMessage); + } + } + } + + + [Test] + public unsafe void GivenFastBufferReaderInitializedFromFastBufferWriterContainingValue_WhenCallingToArray_ReturnedArrayMatchesContentOfWriter() + { + var testStruct = GetTestStruct(); + var requiredSize = FastBufferWriter.GetWriteSize(testStruct); + var writer = new FastBufferWriter(requiredSize, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(requiredSize); + writer.WriteValue(testStruct); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + var array = reader.ToArray(); + var underlyingArray = writer.GetUnsafePtr(); + for (var i = 0; i < array.Length; ++i) + { + Assert.AreEqual(array[i], underlyingArray[i]); + } + } + } + } + + + [Test] + public void WhenCallingReadByteWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadByte(out byte b); }); + } + } + + [Test] + public void WhenCallingReadBytesWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + byte[] b = { 0, 1, 2 }; + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadBytes(ref b, 3); }); + } + } + + [Test] + public void WhenCallingReadValueWithUnmanagedTypeWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadValue(out int i); }); + } + } + + [Test] + public void WhenCallingReadValueWithByteArrayWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadValue(out byte[] b); }); + } + } + + [Test] + public void WhenCallingReadValueWithStringWithoutCallingTryBeingReadFirst_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + Assert.Throws(() => { emptyReader.ReadValue(out string s); }); + } + } + + [Test] + public void WhenCallingReadValueAfterCallingTryBeginWriteWithTooFewBytes_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(sizeof(int) - 1); + Assert.Throws(() => { emptyReader.ReadValue(out int i); }); + } + } + + [Test] + public void WhenCallingReadBytePastBoundaryMarkedByTryBeginWrite_OverflowExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(sizeof(int) - 1); + emptyReader.ReadByte(out byte b); + emptyReader.ReadByte(out b); + emptyReader.ReadByte(out b); + Assert.Throws(() => { emptyReader.ReadByte(out b); }); + } + } + + [Test] + public void WhenCallingReadByteDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadByte(out byte b); }); + } + } + + [Test] + public void WhenCallingReadBytesDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + byte[] b = { 0, 1, 2 }; + Assert.Throws(() => { emptyReader.ReadBytes(ref b, 3); }); + } + } + + [Test] + public void WhenCallingReadValueWithUnmanagedTypeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValue(out int i); }); + } + } + + [Test] + public void WhenCallingReadValueWithByteArrayDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValue(out byte[] b); }); + } + } + + [Test] + public void WhenCallingReadValueWithStringDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValue(out string s); }); + } + } + + [Test] + public void WhenCallingReadByteSafeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadByteSafe(out byte b); }); + } + } + + [Test] + public void WhenCallingReadBytesSafeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + byte[] b = { 0, 1, 2 }; + Assert.Throws(() => { emptyReader.ReadBytesSafe(ref b, 3); }); + } + } + + [Test] + public void WhenCallingReadValueSafeWithUnmanagedTypeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValueSafe(out int i); }); + } + } + + [Test] + public void WhenCallingReadValueSafeWithByteArrayDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValueSafe(out byte[] b); }); + } + } + + [Test] + public void WhenCallingReadValueSafeWithStringDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + using var context = emptyReader.EnterBitwiseContext(); + Assert.Throws(() => { emptyReader.ReadValueSafe(out string s); }); + } + } + + [Test] + public void WhenCallingReadByteAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadByte(out byte theByte); + } + } + + [Test] + public void WhenCallingReadBytesAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + + byte[] theBytes = { 0, 1, 2 }; + emptyReader.ReadBytes(ref theBytes, 3); + } + } + + [Test] + public void WhenCallingReadValueWithUnmanagedTypeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValue(out int i); + } + } + + [Test] + public void WhenCallingReadValueWithByteArrayAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValue(out byte[] theBytes); + } + } + + [Test] + public void WhenCallingReadValueWithStringAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValue(out string s); + } + } + + [Test] + public void WhenCallingReadByteSafeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadByteSafe(out byte theByte); + } + } + + [Test] + public void WhenCallingReadBytesSafeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + + byte[] theBytes = { 0, 1, 2 }; + emptyReader.ReadBytesSafe(ref theBytes, 3); + } + } + + [Test] + public void WhenCallingReadValueSafeWithUnmanagedTypeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValueSafe(out int i); + } + } + + [Test] + public void WhenCallingReadValueSafeWithByteArrayAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValueSafe(out byte[] theBytes); + } + } + + [Test] + public void WhenCallingReadValueSafeWithStringAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + using (var context = emptyReader.EnterBitwiseContext()) + { + context.ReadBit(out bool theBit); + } + emptyReader.ReadValueSafe(out string s); + } + } + + [Test] + public void WhenCallingTryBeginRead_TheAllowedReadPositionIsMarkedRelativeToCurrentPosition() + { + var nativeArray = new NativeArray(100, Allocator.Temp); + var emptyReader = new FastBufferReader(nativeArray, Allocator.Temp); + + using (emptyReader) + { + emptyReader.TryBeginRead(100); + emptyReader.ReadByte(out byte b); + emptyReader.TryBeginRead(1); + emptyReader.ReadByte(out b); + Assert.Throws(() => { emptyReader.ReadByte(out byte b); }); + } + } + + [Test] + public void WhenReadingAfterSeeking_TheNewReadComesFromTheCorrectPosition() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.WriteByteSafe(1); + writer.WriteByteSafe(3); + writer.WriteByteSafe(2); + writer.WriteByteSafe(5); + writer.WriteByteSafe(4); + writer.WriteByteSafe(0); + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + reader.Seek(5); + reader.ReadByteSafe(out byte b); + Assert.AreEqual(reader.Position, 6); + Assert.AreEqual(reader.Length, writer.Length); + Assert.AreEqual(0, b); + + reader.Seek(0); + reader.ReadByteSafe(out b); + Assert.AreEqual(reader.Position, 1); + Assert.AreEqual(reader.Length, writer.Length); + Assert.AreEqual(1, b); + + reader.Seek(10); + Assert.AreEqual(reader.Position, writer.Length); + Assert.AreEqual(reader.Length, writer.Length); + + reader.Seek(2); + reader.ReadByteSafe(out b); + Assert.AreEqual(2, b); + + reader.Seek(1); + reader.ReadByteSafe(out b); + Assert.AreEqual(3, b); + + reader.Seek(4); + reader.ReadByteSafe(out b); + Assert.AreEqual(4, b); + + reader.Seek(3); + reader.ReadByteSafe(out b); + Assert.AreEqual(5, b); + + Assert.AreEqual(reader.Position, 4); + Assert.AreEqual(reader.Length, writer.Length); + } + } + } + + [Test] + public void GivenFastBufferWriterWithNetworkObjectWritten_WhenReadingNetworkObject_TheSameObjectIsRetrieved([Values] WriteType writeType) + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(FastBufferWriterExtensions.GetWriteSize(networkObject) + 1, Allocator.Temp); + using (writer) + { + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(writer.TryBeginWrite(FastBufferWriterExtensions.GetWriteSize(networkObject))); + writer.WriteValue(networkObject); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(networkObject); + break; + case WriteType.WriteAsObject: + writer.WriteObject(networkObject); + break; + } + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(FastBufferWriterExtensions.GetNetworkObjectWriteSize())); + NetworkObject result = null; + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(reader.TryBeginRead(FastBufferWriterExtensions.GetWriteSize(networkObject))); + reader.ReadValue(out result); + break; + case WriteType.WriteSafe: + reader.ReadValueSafe(out result); + break; + case WriteType.WriteAsObject: + reader.ReadObject(out object resultObj, typeof(NetworkObject)); + result = (NetworkObject)resultObj; + break; + } + Assert.AreSame(result, networkObject); + } + } + }); + } + + [Test] + public void GivenFastBufferWriterWithGameObjectWritten_WhenReadingGameObject_TheSameObjectIsRetrieved([Values] WriteType writeType) + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(FastBufferWriterExtensions.GetWriteSize(obj) + 1, Allocator.Temp); + using (writer) + { + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(writer.TryBeginWrite(FastBufferWriterExtensions.GetWriteSize(obj))); + writer.WriteValue(obj); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(obj); + break; + case WriteType.WriteAsObject: + writer.WriteObject(obj); + break; + } + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(FastBufferWriterExtensions.GetGameObjectWriteSize())); + GameObject result = null; + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(reader.TryBeginRead(FastBufferWriterExtensions.GetWriteSize(obj))); + reader.ReadValue(out result); + break; + case WriteType.WriteSafe: + reader.ReadValueSafe(out result); + break; + case WriteType.WriteAsObject: + reader.ReadObject(out object resultObj, typeof(GameObject)); + result = (GameObject)resultObj; + break; + } + Assert.AreSame(result, obj); + } + } + }); + } + + [Test] + public void GivenFastBufferWriterWithNetworkBehaviourWritten_WhenReadingNetworkBehaviour_TheSameObjectIsRetrieved([Values] WriteType writeType) + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(FastBufferWriterExtensions.GetWriteSize(networkBehaviour) + 1, Allocator.Temp); + using (writer) + { + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(writer.TryBeginWrite(FastBufferWriterExtensions.GetWriteSize(networkBehaviour))); + writer.WriteValue(networkBehaviour); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(networkBehaviour); + break; + case WriteType.WriteAsObject: + writer.WriteObject(networkBehaviour); + break; + } + + var reader = new FastBufferReader(ref writer, Allocator.Temp); + using (reader) + { + Assert.IsTrue(reader.TryBeginRead(FastBufferWriterExtensions.GetNetworkBehaviourWriteSize())); + NetworkBehaviour result = null; + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(reader.TryBeginRead(FastBufferWriterExtensions.GetWriteSize(networkBehaviour))); + reader.ReadValue(out result); + break; + case WriteType.WriteSafe: + reader.ReadValueSafe(out result); + break; + case WriteType.WriteAsObject: + reader.ReadObject(out object resultObj, typeof(NetworkBehaviour)); + result = (NetworkBehaviour)resultObj; + break; + } + Assert.AreSame(result, networkBehaviour); + } + } + }); + } + + [Test] + public void WhenCallingTryBeginReadInternal_AllowedReadPositionDoesNotMoveBackward() + { + var reader = new FastBufferReader(new NativeArray(100, Allocator.Temp), Allocator.Temp); + using (reader) + { + reader.TryBeginRead(25); + reader.TryBeginReadInternal(5); + Assert.AreEqual(reader.AllowedReadMark, 25); + } + } + + #endregion + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs.meta new file mode 100644 index 0000000000..52af796039 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2881f8138b479c34389b76687e5307ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferWriterTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferWriterTests.cs new file mode 100644 index 0000000000..f1d3897737 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferWriterTests.cs @@ -0,0 +1,1215 @@ +using System; +using NUnit.Framework; +using Unity.Collections; +using UnityEngine; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class FastBufferWriterTests : BaseFastBufferReaderWriterTest + { + + #region Common Checks + private void WriteCheckBytes(ref FastBufferWriter writer, int writeSize, string failMessage = "") + { + Assert.IsTrue(writer.TryBeginWrite(2), "Writer denied write permission"); + writer.WriteValue((byte)0x80); + Assert.AreEqual(writeSize + 1, writer.Position, failMessage); + Assert.AreEqual(writeSize + 1, writer.Length, failMessage); + writer.WriteValue((byte)0xFF); + Assert.AreEqual(writeSize + 2, writer.Position, failMessage); + Assert.AreEqual(writeSize + 2, writer.Length, failMessage); + } + + private void VerifyCheckBytes(byte[] underlyingArray, int writeSize, string failMessage = "") + { + Assert.AreEqual(0x80, underlyingArray[writeSize], failMessage); + Assert.AreEqual(0xFF, underlyingArray[writeSize + 1], failMessage); + } + + private unsafe void VerifyBytewiseEquality(T value, byte[] underlyingArray, int valueOffset, int bufferOffset, int size, string failMessage = "") where T : unmanaged + { + byte* asBytePointer = (byte*)&value; + for (var i = 0; i < size; ++i) + { + Assert.AreEqual(asBytePointer[i + valueOffset], underlyingArray[i + bufferOffset], failMessage); + } + } + + private unsafe void VerifyTypedEquality(T value, byte* unsafePtr) where T : unmanaged + { + var checkValue = (T*)unsafePtr; + Assert.AreEqual(value, *checkValue); + } + + private void VerifyPositionAndLength(ref FastBufferWriter writer, int position, string failMessage = "") + { + Assert.AreEqual(position, writer.Position, failMessage); + Assert.AreEqual(position, writer.Length, failMessage); + } + + private unsafe void CommonChecks(ref FastBufferWriter writer, T valueToTest, int writeSize, string failMessage = "") where T : unmanaged + { + + VerifyPositionAndLength(ref writer, writeSize, failMessage); + + WriteCheckBytes(ref writer, writeSize, failMessage); + + var underlyingArray = writer.ToArray(); + + VerifyBytewiseEquality(valueToTest, underlyingArray, 0, 0, writeSize, failMessage); + + VerifyCheckBytes(underlyingArray, writeSize, failMessage); + + VerifyTypedEquality(valueToTest, writer.GetUnsafePtr()); + } + #endregion + + #region Generic Checks + protected override unsafe void RunTypeTest(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var alternateWriteSize = FastBufferWriter.GetWriteSize(); + Assert.AreEqual(sizeof(T), writeSize); + Assert.AreEqual(sizeof(T), alternateWriteSize); + + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + + Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); + + var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; + + writer.WriteValue(valueToTest); + + CommonChecks(ref writer, valueToTest, writeSize, failMessage); + } + } + protected override unsafe void RunTypeTestSafe(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + Assert.AreEqual(sizeof(T), writeSize); + + var failMessage = $"RunTypeTest failed with type {typeof(T)} and value {valueToTest}"; + + writer.WriteValueSafe(valueToTest); + + CommonChecks(ref writer, valueToTest, writeSize, failMessage); + } + } + + protected override unsafe void RunObjectTypeTest(T valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + + using (writer) + { + Assert.AreEqual(sizeof(T), writeSize); + + var failMessage = $"RunObjectTypeTest failed with type {typeof(T)} and value {valueToTest}"; + writer.WriteObject(valueToTest); + + CommonChecks(ref writer, valueToTest, writeSize, failMessage); + } + } + + private unsafe void VerifyArrayEquality(T[] value, byte* unsafePtr, int offset) where T : unmanaged + { + int* sizeValue = (int*)(unsafePtr + offset); + Assert.AreEqual(value.Length, *sizeValue); + + fixed (T* asTPointer = value) + { + var underlyingTArray = (T*)(unsafePtr + sizeof(int) + offset); + for (var i = 0; i < value.Length; ++i) + { + Assert.AreEqual(asTPointer[i], underlyingTArray[i]); + } + } + } + + protected override unsafe void RunTypeArrayTest(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + using (writer) + { + + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + Assert.IsTrue(writer.TryBeginWrite(writeSize + 2), "Writer denied write permission"); + + writer.WriteValue(valueToTest); + VerifyPositionAndLength(ref writer, writeSize); + + WriteCheckBytes(ref writer, writeSize); + + VerifyArrayEquality(valueToTest, writer.GetUnsafePtr(), 0); + + var underlyingArray = writer.ToArray(); + VerifyCheckBytes(underlyingArray, writeSize); + } + } + + protected override unsafe void RunTypeArrayTestSafe(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + var writer = new FastBufferWriter(writeSize + 2, Allocator.Temp); + using (writer) + { + + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + + writer.WriteValueSafe(valueToTest); + VerifyPositionAndLength(ref writer, writeSize); + + WriteCheckBytes(ref writer, writeSize); + + VerifyArrayEquality(valueToTest, writer.GetUnsafePtr(), 0); + + var underlyingArray = writer.ToArray(); + VerifyCheckBytes(underlyingArray, writeSize); + } + } + + protected override unsafe void RunObjectTypeArrayTest(T[] valueToTest) + { + var writeSize = FastBufferWriter.GetWriteSize(valueToTest); + // Extra byte for WriteObject adding isNull flag + var writer = new FastBufferWriter(writeSize + 3, Allocator.Temp); + using (writer) + { + + Assert.AreEqual(sizeof(int) + sizeof(T) * valueToTest.Length, writeSize); + + writer.WriteObject(valueToTest); + Assert.AreEqual(0, writer.ToArray()[0]); + VerifyPositionAndLength(ref writer, writeSize + sizeof(byte)); + + WriteCheckBytes(ref writer, writeSize + sizeof(byte)); + + VerifyArrayEquality(valueToTest, writer.GetUnsafePtr(), sizeof(byte)); + + var underlyingArray = writer.ToArray(); + VerifyCheckBytes(underlyingArray, writeSize + sizeof(byte)); + } + } + #endregion + + + #region Tests + [Test, Description("Tests ")] + public void WhenWritingUnmanagedType_ValueIsWrittenCorrectly( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), typeof(TestStruct))] + Type testType, + [Values] WriteType writeType) + { + BaseTypeTest(testType, writeType); + } + + [Test] + public void WhenWritingArrayOfUnmanagedElementType_ArrayIsWrittenCorrectly( + [Values(typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), + typeof(long), typeof(ulong), typeof(bool), typeof(char), typeof(float), typeof(double), + typeof(ByteEnum), typeof(SByteEnum), typeof(ShortEnum), typeof(UShortEnum), typeof(IntEnum), + typeof(UIntEnum), typeof(LongEnum), typeof(ULongEnum), typeof(Vector2), typeof(Vector3), typeof(Vector4), + typeof(Quaternion), typeof(Color), typeof(Color32), typeof(Ray), typeof(Ray2D), typeof(TestStruct))] + Type testType, + [Values] WriteType writeType) + { + BaseArrayTypeTest(testType, writeType); + } + + [TestCase(false, WriteType.WriteDirect)] + [TestCase(false, WriteType.WriteSafe)] + [TestCase(false, WriteType.WriteAsObject)] + [TestCase(true, WriteType.WriteDirect)] + [TestCase(true, WriteType.WriteSafe)] + public unsafe void WhenWritingString_ValueIsWrittenCorrectly(bool oneByteChars, WriteType writeType) + { + string valueToTest = "Hello, I am a test string!"; + + var serializedValueSize = FastBufferWriter.GetWriteSize(valueToTest, oneByteChars); + + var writer = new FastBufferWriter(serializedValueSize + 3, Allocator.Temp); + using (writer) + { + var offset = 0; + switch (writeType) + { + case WriteType.WriteDirect: + Assert.IsTrue(writer.TryBeginWrite(serializedValueSize + 2), "Writer denied write permission"); + writer.WriteValue(valueToTest, oneByteChars); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(valueToTest, oneByteChars); + break; + case WriteType.WriteAsObject: + writer.WriteObject(valueToTest); + // account for isNull byte + offset = sizeof(byte); + break; + + } + + VerifyPositionAndLength(ref writer, serializedValueSize + offset); + WriteCheckBytes(ref writer, serializedValueSize + offset); + + int* sizeValue = (int*)(writer.GetUnsafePtr() + offset); + Assert.AreEqual(valueToTest.Length, *sizeValue); + + fixed (char* asCharPointer = valueToTest) + { + if (oneByteChars) + { + byte* underlyingByteArray = writer.GetUnsafePtr() + sizeof(int) + offset; + for (var i = 0; i < valueToTest.Length; ++i) + { + Assert.AreEqual((byte)asCharPointer[i], underlyingByteArray[i]); + } + + } + else + { + char* underlyingCharArray = (char*)(writer.GetUnsafePtr() + sizeof(int) + offset); + for (var i = 0; i < valueToTest.Length; ++i) + { + Assert.AreEqual(asCharPointer[i], underlyingCharArray[i]); + } + } + } + + var underlyingArray = writer.ToArray(); + VerifyCheckBytes(underlyingArray, serializedValueSize + offset); + } + } + + [TestCase(1, 0)] + [TestCase(2, 0)] + [TestCase(3, 0)] + [TestCase(4, 0)] + [TestCase(5, 0)] + [TestCase(6, 0)] + [TestCase(7, 0)] + [TestCase(8, 0)] + + [TestCase(1, 1)] + [TestCase(2, 1)] + [TestCase(3, 1)] + [TestCase(4, 1)] + [TestCase(5, 1)] + [TestCase(6, 1)] + [TestCase(7, 1)] + + [TestCase(1, 2)] + [TestCase(2, 2)] + [TestCase(3, 2)] + [TestCase(4, 2)] + [TestCase(5, 2)] + [TestCase(6, 2)] + + [TestCase(1, 3)] + [TestCase(2, 3)] + [TestCase(3, 3)] + [TestCase(4, 3)] + [TestCase(5, 3)] + + [TestCase(1, 4)] + [TestCase(2, 4)] + [TestCase(3, 4)] + [TestCase(4, 4)] + + [TestCase(1, 5)] + [TestCase(2, 5)] + [TestCase(3, 5)] + + [TestCase(1, 6)] + [TestCase(2, 6)] + + [TestCase(1, 7)] + public unsafe void WhenWritingPartialValueWithCountAndOffset_ValueIsWrittenCorrectly(int count, int offset) + { + var random = new Random(); + var valueToTest = ((ulong)random.Next() << 32) + (ulong)random.Next(); + var writer = new FastBufferWriter(sizeof(ulong) + 2, Allocator.Temp); + using (writer) + { + + Assert.IsTrue(writer.TryBeginWrite(count + 2), "Writer denied write permission"); + writer.WritePartialValue(valueToTest, count, offset); + + var failMessage = $"TestWritingPartialValues failed with value {valueToTest}"; + VerifyPositionAndLength(ref writer, count, failMessage); + WriteCheckBytes(ref writer, count, failMessage); + var underlyingArray = writer.ToArray(); + VerifyBytewiseEquality(valueToTest, underlyingArray, offset, 0, count, failMessage); + VerifyCheckBytes(underlyingArray, count, failMessage); + + ulong mask = 0; + for (var i = 0; i < count; ++i) + { + mask = (mask << 8) | 0b11111111; + } + + ulong* checkValue = (ulong*)writer.GetUnsafePtr(); + Assert.AreEqual((valueToTest >> (offset * 8)) & mask, *checkValue & mask); + } + } + + [Test] + public void WhenCallingToArray_ReturnedArrayContainsCorrectData() + { + var testStruct = GetTestStruct(); + var requiredSize = FastBufferWriter.GetWriteSize(testStruct); + var writer = new FastBufferWriter(requiredSize, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(requiredSize); + writer.WriteValue(testStruct); + var array = writer.ToArray(); + var underlyingArray = writer.ToArray(); + for (var i = 0; i < array.Length; ++i) + { + Assert.AreEqual(array[i], underlyingArray[i]); + } + } + } + + [Test] + public void WhenCallingWriteByteWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + Assert.Throws(() => { writer.WriteByte(1); }); + } + } + + [Test] + public void WhenCallingWriteBytesWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + var bytes = new byte[] { 0, 1, 2 }; + Assert.Throws(() => { writer.WriteBytes(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueWithUnmanagedTypeWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + int i = 1; + Assert.Throws(() => { writer.WriteValue(i); }); + } + } + + [Test] + public void WhenCallingWriteValueWithByteArrayWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + var bytes = new byte[] { 0, 1, 2 }; + Assert.Throws(() => { writer.WriteValue(bytes); }); + } + } + + [Test] + public void WhenCallingWriteValueWithStringWithoutCallingTryBeingWriteFirst_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + Assert.Throws(() => { writer.WriteValue(""); }); + } + } + + [Test] + public void WhenCallingWriteValueAfterCallingTryBeginWriteWithTooFewBytes_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + int i = 0; + writer.TryBeginWrite(sizeof(int) - 1); + Assert.Throws(() => { writer.WriteValue(i); }); + } + } + + [Test] + public void WhenCallingWriteBytePastBoundaryMarkedByTryBeginWrite_OverflowExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(sizeof(int) - 1); + writer.WriteByte(1); + writer.WriteByte(2); + writer.WriteByte(3); + Assert.Throws(() => { writer.WriteByte(4); }); + } + } + + [Test] + public void WhenCallingWriteByteDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteByte(1); }); + } + } + + [Test] + public void WhenCallingWriteBytesDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteBytes(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueWithUnmanagedTypeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + int i = 1; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteValue(i); }); + } + } + + [Test] + public void WhenCallingWriteValueWithByteArrayDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteBytes(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueWithStringDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + int i = 1; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteValue(""); }); + } + } + + [Test] + public void WhenCallingWriteByteSafeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteByteSafe(1); }); + } + } + + [Test] + public void WhenCallingWriteBytesSafeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteBytesSafe(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithUnmanagedTypeDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + int i = 1; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteValueSafe(i); }); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithByteArrayDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteBytesSafe(bytes, bytes.Length); }); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithStringDuringBitwiseContext_InvalidOperationExceptionIsThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using var context = writer.EnterBitwiseContext(); + Assert.Throws(() => { writer.WriteValueSafe(""); }); + } + } + + [Test] + public void WhenCallingWriteByteAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteByte(1); + } + } + + [Test] + public void WhenCallingWriteBytesAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteBytes(bytes, bytes.Length); + } + } + + [Test] + public void WhenCallingWriteValueWithUnmanagedTypeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + int i = 1; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValue(i); + } + } + + [Test] + public void WhenCallingWriteValueWithByteArrayAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + int i = 1; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValue(bytes); + } + } + + [Test] + public void WhenCallingWriteValueWithStringAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValue(""); + } + } + + [Test] + public void WhenCallingWriteByteSafeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteByteSafe(1); + } + } + + [Test] + public void WhenCallingWriteBytesSafeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteBytesSafe(bytes, bytes.Length); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithUnmanagedTypeAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + int i = 1; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValueSafe(i); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithByteArrayAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + var bytes = new byte[] { 0, 1, 2 }; + int i = 1; + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValueSafe(bytes); + } + } + + [Test] + public void WhenCallingWriteValueSafeWithStringAfterExitingBitwiseContext_InvalidOperationExceptionIsNotThrown() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + + using (writer) + { + writer.TryBeginWrite(100); + using (var context = writer.EnterBitwiseContext()) + { + context.WriteBit(true); + } + writer.WriteValueSafe(""); + } + } + + [Test] + public void WhenCallingTryBeginWrite_TheAllowedWritePositionIsMarkedRelativeToCurrentPosition() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(100); + writer.WriteByte(1); + writer.TryBeginWrite(1); + writer.WriteByte(1); + Assert.Throws(() => { writer.WriteByte(1); }); + } + } + + [Test] + public void WhenWritingAfterSeeking_TheNewWriteGoesToTheCorrectPosition() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.Seek(5); + writer.WriteByteSafe(0); + Assert.AreEqual(writer.Position, 6); + + writer.Seek(0); + writer.WriteByteSafe(1); + Assert.AreEqual(writer.Position, 1); + + writer.Seek(10); + Assert.AreEqual(writer.Position, 10); + + writer.Seek(2); + writer.WriteByteSafe(2); + + writer.Seek(1); + writer.WriteByteSafe(3); + + writer.Seek(4); + writer.WriteByteSafe(4); + + writer.Seek(3); + writer.WriteByteSafe(5); + + Assert.AreEqual(writer.Position, 4); + + var expected = new byte[] { 1, 3, 2, 5, 4, 0 }; + var underlyingArray = writer.ToArray(); + for (var i = 0; i < expected.Length; ++i) + { + Assert.AreEqual(expected[i], underlyingArray[i]); + } + } + } + + [Test] + public void WhenSeekingForward_LengthUpdatesToNewPosition() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + Assert.AreEqual(writer.Length, 0); + writer.Seek(5); + Assert.AreEqual(writer.Length, 5); + writer.Seek(10); + Assert.AreEqual(writer.Length, 10); + } + } + + [Test] + public void WhenSeekingBackward_LengthDoesNotChange() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + Assert.AreEqual(writer.Length, 0); + writer.Seek(5); + Assert.AreEqual(writer.Length, 5); + writer.Seek(0); + Assert.AreEqual(writer.Length, 5); + } + } + + [Test] + public void WhenTruncatingToSpecificPositionAheadOfWritePosition_LengthIsUpdatedAndPositionIsNot() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.Seek(10); + Assert.AreEqual(writer.Position, 10); + Assert.AreEqual(writer.Length, 10); + + writer.Seek(5); + Assert.AreEqual(writer.Position, 5); + Assert.AreEqual(writer.Length, 10); + + writer.Truncate(8); + Assert.AreEqual(writer.Position, 5); + Assert.AreEqual(writer.Length, 8); + } + } + + [Test] + public void WhenTruncatingToSpecificPositionBehindWritePosition_BothLengthAndPositionAreUpdated() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.Seek(10); + Assert.AreEqual(writer.Position, 10); + Assert.AreEqual(writer.Length, 10); + + writer.Truncate(8); + Assert.AreEqual(writer.Position, 8); + Assert.AreEqual(writer.Length, 8); + } + } + + [Test] + public void WhenTruncatingToCurrentPosition_LengthIsUpdated() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.Seek(10); + Assert.AreEqual(writer.Position, 10); + Assert.AreEqual(writer.Length, 10); + + writer.Seek(5); + writer.Truncate(); + Assert.AreEqual(writer.Position, 5); + Assert.AreEqual(writer.Length, 5); + } + } + + [Test] + public void WhenCreatingNewFastBufferWriter_CapacityIsCorrect() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + Assert.AreEqual(100, writer.Capacity); + writer.Dispose(); + + writer = new FastBufferWriter(200, Allocator.Temp); + Assert.AreEqual(200, writer.Capacity); + writer.Dispose(); + } + + [Test] + public void WhenCreatingNewFastBufferWriter_MaxCapacityIsCorrect() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + Assert.AreEqual(100, writer.MaxCapacity); + writer.Dispose(); + + writer = new FastBufferWriter(100, Allocator.Temp, 200); + Assert.AreEqual(200, writer.MaxCapacity); + writer.Dispose(); + } + + [Test] + public void WhenRequestingWritePastBoundsForNonGrowingWriter_TryBeginWriteReturnsFalse() + { + var writer = new FastBufferWriter(150, Allocator.Temp); + using (writer) + { + var testStruct = GetTestStruct(); + writer.TryBeginWriteValue(testStruct); + writer.WriteValue(testStruct); + + // Seek to exactly where the write would cross the buffer boundary + writer.Seek(150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + + // Writer isn't allowed to grow because it didn't specify a maxSize + Assert.IsFalse(writer.TryBeginWriteValue(testStruct)); + } + } + + [Test] + public void WhenTryBeginWriteReturnsFalse_WritingThrowsOverflowException() + { + var writer = new FastBufferWriter(150, Allocator.Temp); + using (writer) + { + var testStruct = GetTestStruct(); + writer.TryBeginWriteValue(testStruct); + writer.WriteValue(testStruct); + + // Seek to exactly where the write would cross the buffer boundary + writer.Seek(150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + + // Writer isn't allowed to grow because it didn't specify a maxSize + Assert.IsFalse(writer.TryBeginWriteValue(testStruct)); + Assert.Throws(() => writer.WriteValue(testStruct)); + } + } + + [Test] + public void WhenTryBeginWriteReturnsFalseAndOverflowExceptionIsThrown_DataIsNotAffected() + { + var writer = new FastBufferWriter(150, Allocator.Temp); + using (writer) + { + var testStruct = GetTestStruct(); + writer.TryBeginWriteValue(testStruct); + writer.WriteValue(testStruct); + + // Seek to exactly where the write would cross the buffer boundary + writer.Seek(150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + + // Writer isn't allowed to grow because it didn't specify a maxSize + Assert.IsFalse(writer.TryBeginWriteValue(testStruct)); + Assert.Throws(() => writer.WriteValue(testStruct)); + VerifyBytewiseEquality(testStruct, writer.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenRequestingWritePastBoundsForGrowingWriter_BufferGrowsWithoutLosingData() + { + var growingWriter = new FastBufferWriter(150, Allocator.Temp, 500); + using (growingWriter) + { + var testStruct = GetTestStruct(); + growingWriter.TryBeginWriteValue(testStruct); + growingWriter.WriteValue(testStruct); + + // Seek to exactly where the write would cross the buffer boundary + growingWriter.Seek(150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + + Assert.IsTrue(growingWriter.TryBeginWriteValue(testStruct)); + + // Growth doubles the size + Assert.AreEqual(300, growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, 150 - FastBufferWriter.GetWriteSize(testStruct) + 1); + growingWriter.WriteValue(testStruct); + + // Verify the growth properly copied the existing data + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 150 - FastBufferWriter.GetWriteSize(testStruct) + 1, FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenRequestingWriteExactlyAtBoundsForGrowingWriter_BufferDoesntGrow() + { + var growingWriter = new FastBufferWriter(300, Allocator.Temp, 500); + using (growingWriter) + { + var testStruct = GetTestStruct(); + growingWriter.TryBeginWriteValue(testStruct); + growingWriter.WriteValue(testStruct); + + growingWriter.Seek(300 - FastBufferWriter.GetWriteSize(testStruct)); + Assert.IsTrue(growingWriter.TryBeginWriteValue(testStruct)); + Assert.AreEqual(300, growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, growingWriter.ToArray().Length); + growingWriter.WriteValue(testStruct); + Assert.AreEqual(300, growingWriter.Position); + + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 300 - FastBufferWriter.GetWriteSize(testStruct), FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenBufferGrows_MaxCapacityIsNotExceeded() + { + var growingWriter = new FastBufferWriter(300, Allocator.Temp, 500); + using (growingWriter) + { + var testStruct = GetTestStruct(); + growingWriter.TryBeginWriteValue(testStruct); + growingWriter.WriteValue(testStruct); + + growingWriter.Seek(300); + Assert.IsTrue(growingWriter.TryBeginWriteValue(testStruct)); + + Assert.AreEqual(500, growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, 300); + + growingWriter.WriteValue(testStruct); + + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 300, FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenBufferGrowthRequiredIsMoreThanDouble_BufferGrowsEnoughToContainRequestedValue() + { + var growingWriter = new FastBufferWriter(1, Allocator.Temp, 500); + using (growingWriter) + { + var testStruct = GetTestStruct(); + Assert.IsTrue(growingWriter.TryBeginWriteValue(testStruct)); + + // Buffer size doubles with each growth, so since we're starting with a size of 1, that means + // the resulting size should be the next power of 2 above the size of testStruct. + Assert.AreEqual(Math.Pow(2, Math.Ceiling(Mathf.Log(FastBufferWriter.GetWriteSize(testStruct), 2))), + growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, 0); + + growingWriter.WriteValue(testStruct); + + VerifyBytewiseEquality(testStruct, growingWriter.ToArray(), 0, 0, FastBufferWriter.GetWriteSize(testStruct)); + } + } + + [Test] + public void WhenTryingToWritePastMaxCapacity_GrowthDoesNotOccurAndTryBeginWriteReturnsFalse() + { + var growingWriter = new FastBufferWriter(300, Allocator.Temp, 500); + using (growingWriter) + { + Assert.IsFalse(growingWriter.TryBeginWrite(501)); + + Assert.AreEqual(300, growingWriter.Capacity); + Assert.AreEqual(growingWriter.Position, 0); + } + } + + [Test] + public void WhenWritingNetworkBehaviour_ObjectIdAndBehaviourIdAreWritten([Values] WriteType writeType) + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(FastBufferWriterExtensions.GetWriteSize(networkBehaviour) + 1, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(FastBufferWriterExtensions.GetWriteSize(networkBehaviour))); + + var offset = 0; + switch (writeType) + { + case WriteType.WriteDirect: + writer.WriteValue(networkBehaviour); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(networkBehaviour); + break; + case WriteType.WriteAsObject: + writer.WriteObject(networkBehaviour); + // account for isNull byte + offset = 1; + break; + } + + Assert.AreEqual(FastBufferWriterExtensions.GetWriteSize(networkBehaviour) + offset, writer.Position); + VerifyBytewiseEquality(networkObject.NetworkObjectId, writer.ToArray(), 0, offset, sizeof(ulong)); + VerifyBytewiseEquality(networkBehaviour.NetworkBehaviourId, writer.ToArray(), 0, + sizeof(ulong) + offset, sizeof(ushort)); + } + }); + } + + [Test] + public void WhenWritingNetworkObject_NetworkObjectIdIsWritten([Values] WriteType writeType) + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(FastBufferWriterExtensions.GetWriteSize(networkObject) + 1, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(FastBufferWriterExtensions.GetWriteSize(networkObject))); + + var offset = 0; + switch (writeType) + { + case WriteType.WriteDirect: + writer.WriteValue(networkObject); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(networkObject); + break; + case WriteType.WriteAsObject: + writer.WriteObject(networkObject); + // account for isNull byte + offset = 1; + break; + } + + Assert.AreEqual(FastBufferWriterExtensions.GetWriteSize(networkObject) + offset, writer.Position); + VerifyBytewiseEquality(networkObject.NetworkObjectId, writer.ToArray(), 0, offset, sizeof(ulong)); + } + }); + } + + [Test] + public void WhenWritingGameObject_NetworkObjectIdIsWritten([Values] WriteType writeType) + { + RunGameObjectTest((obj, networkBehaviour, networkObject) => + { + var writer = new FastBufferWriter(FastBufferWriterExtensions.GetWriteSize(obj) + 1, Allocator.Temp); + using (writer) + { + Assert.IsTrue(writer.TryBeginWrite(FastBufferWriterExtensions.GetWriteSize(obj))); + + var offset = 0; + switch (writeType) + { + case WriteType.WriteDirect: + writer.WriteValue(obj); + break; + case WriteType.WriteSafe: + writer.WriteValueSafe(obj); + break; + case WriteType.WriteAsObject: + writer.WriteObject(obj); + // account for isNull byte + offset = 1; + break; + } + + Assert.AreEqual(FastBufferWriterExtensions.GetWriteSize(obj) + offset, writer.Position); + VerifyBytewiseEquality(networkObject.NetworkObjectId, writer.ToArray(), 0, offset, sizeof(ulong)); + } + }); + } + + [Test] + public void WhenCallingTryBeginWriteInternal_AllowedWritePositionDoesNotMoveBackward() + { + var writer = new FastBufferWriter(100, Allocator.Temp); + using (writer) + { + writer.TryBeginWrite(25); + writer.TryBeginWriteInternal(5); + Assert.AreEqual(writer.AllowedWriteMark, 25); + } + } + #endregion + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferWriterTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferWriterTests.cs.meta new file mode 100644 index 0000000000..b549311462 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferWriterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1cef42b60935e29469ed1404fb30ba2d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef b/com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef index 1c8e0294d9..a08074009c 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef +++ b/com.unity.netcode.gameobjects/Tests/Editor/com.unity.netcode.editortests.asmdef @@ -13,6 +13,18 @@ "includePlatforms": [ "Editor" ], + "excludePlatforms": [], + "allowUnsafeCode": true, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false, "versionDefines": [ { "name": "com.unity.multiplayer.tools", diff --git a/testproject/.gitignore b/testproject/.gitignore index acbbe841e6..fbe0ca9b9e 100644 --- a/testproject/.gitignore +++ b/testproject/.gitignore @@ -69,5 +69,7 @@ crashlytics-build.properties # Temporary auto-generated Android Assets /[Aa]ssets/[Ss]treamingAssets/aa.meta /[Aa]ssets/[Ss]treamingAssets/aa/* +/[Aa]ssets/[Ss]treamingAssets/BuildInfo.json +/[Aa]ssets/[Ss]treamingAssets/BuildInfo.json.meta InitTestScene*