Skip to content

Commit dc708a5

Browse files
authored
feat: Fast buffer reader and fast buffer writer (#1082)
* FastBufferWriter implemented and tested (still need to add and update some xmldoc comments though) * A few additional tests to cover the last missed cases I can think of (aside from INetworkSerializable, which isn't ready to be tested yet due to lack of BufferSerializer) * FastBufferReader + tests * - More tests - Streamlined the implementations of bit-packing uints and ulongs. * - Removed NativeArray from FastBufferReader and FastBufferWriter, replacing with byte* allocated directly through UnsafeUtility.Malloc() (same function used under the hood by NativeArray). This is required in order to be able to store pointers to FastBufferReader and FastBufferWriter in order to be able to wrap them with BufferSerializer - NativeArray contains a managed variable in it that disallows taking a pointer to it. And since FBW and FBR are structs, pointers are the only way to wrap them "by reference" in another struct - ref fields aren't allowed even inside ref structs. * Added utility ref struct "Ref<T>" to more generically support wrapping values in other ref structs in a capture-by-reference style, updated BitReader and BitWriter to use that. Also aggressively inlining properties in FBW and FBR. * BufferSerializer and tests. * Removed unnecessary comment. * XMLDocs + cleanup for PR * Replaced possibly unaligned memory access with UnsafeUtility.MemCpy... it's a little slower, but apparently some platforms won't support unaligned memory access (e.g., WEBGL, ARM processors) and there's no compile time way to detect ARM processors since the bytecode is not processor-dependent... the cost of the runtime detection would be more expensive than the cost of just doing the memcpy. * Resurrected BytewiseUtil.FastCopyBytes as a faster alternative to UnsafeUtility.MemCpy for small values, while still supporting unaligned access. * Reverting an accidental change. * Removed files that got accidentally duplicated from before the rename. * Standards fixes * Removed accidentally added files. * Added BuildInfo.json to the .gitignore so I stop accidentally checking it in. * Addressed most of the review feedback. Still need to do a little more restructuring of some of the other tests. * standards.py --fix * standards.py --fix * Fixed incorrect namespaces. * -Fixed a couple of issues where growing a FastBufferWriter wouldn't work correctly (requesting beyond MaxCapacity and requesting more than double current capacity) -Added support for FastBufferReader to be used in a mode that doesn't copy the input buffer * Fix a test failure and better implementation of large growths * - Removed RefArray - Fixed incorrect text in a warning in FastBufferReader * Removed RefArray meta file that stuck around. * Review feedback: Used nameof() instead of string literal. * -Removed BytewiseUtility.FastCopyBytes -Added documentation on PreChecked() functions. * removed .gitignore. * Fixed compile errors that somehow didn't happen on my machine until I looked at the files they were in? * standards.py --fix ... again ... despite no changes since the last success other than integrating develop ...
1 parent 5114ca8 commit dc708a5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+10892
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
using System.Runtime.CompilerServices;
2+
3+
namespace Unity.Netcode
4+
{
5+
public static class BitCounter
6+
{
7+
// Since we don't have access to BitOperations.LeadingZeroCount() (which would have been the fastest)
8+
// we use the De Bruijn sequence to do this calculation
9+
// See https://en.wikipedia.org/wiki/De_Bruijn_sequence and https://www.chessprogramming.org/De_Bruijn_Sequence
10+
private const ulong k_DeBruijnMagic64 = 0x37E84A99DAE458F;
11+
private const uint k_DeBruijnMagic32 = 0x06EB14F9;
12+
13+
// We're counting bytes, not bits, so these have all had the operation x/8 + 1 applied
14+
private static readonly int[] k_DeBruijnTableBytes64 =
15+
{
16+
0/8+1, 1/8+1, 17/8+1, 2/8+1, 18/8+1, 50/8+1, 3/8+1, 57/8+1,
17+
47/8+1, 19/8+1, 22/8+1, 51/8+1, 29/8+1, 4/8+1, 33/8+1, 58/8+1,
18+
15/8+1, 48/8+1, 20/8+1, 27/8+1, 25/8+1, 23/8+1, 52/8+1, 41/8+1,
19+
54/8+1, 30/8+1, 38/8+1, 5/8+1, 43/8+1, 34/8+1, 59/8+1, 8/8+1,
20+
63/8+1, 16/8+1, 49/8+1, 56/8+1, 46/8+1, 21/8+1, 28/8+1, 32/8+1,
21+
14/8+1, 26/8+1, 24/8+1, 40/8+1, 53/8+1, 37/8+1, 42/8+1, 7/8+1,
22+
62/8+1, 55/8+1, 45/8+1, 31/8+1, 13/8+1, 39/8+1, 36/8+1, 6/8+1,
23+
61/8+1, 44/8+1, 12/8+1, 35/8+1, 60/8+1, 11/8+1, 10/8+1, 9/8+1,
24+
};
25+
26+
private static readonly int[] k_DeBruijnTableBytes32 =
27+
{
28+
0/8+1, 1/8+1, 16/8+1, 2/8+1, 29/8+1, 17/8+1, 3/8+1, 22/8+1,
29+
30/8+1, 20/8+1, 18/8+1, 11/8+1, 13/8+1, 4/8+1, 7/8+1, 23/8+1,
30+
31/8+1, 15/8+1, 28/8+1, 21/8+1, 19/8+1, 10/8+1, 12/8+1, 6/8+1,
31+
14/8+1, 27/8+1, 9/8+1, 5/8+1, 26/8+1, 8/8+1, 25/8+1, 24/8+1,
32+
};
33+
34+
// And here we're counting the number of set bits, not the position of the highest set,
35+
// so these still have +1 applied - unfortunately 0 and 1 both return the same value.
36+
private static readonly int[] k_DeBruijnTableBits64 =
37+
{
38+
0+1, 1+1, 17+1, 2+1, 18+1, 50+1, 3+1, 57+1,
39+
47+1, 19+1, 22+1, 51+1, 29+1, 4+1, 33+1, 58+1,
40+
15+1, 48+1, 20+1, 27+1, 25+1, 23+1, 52+1, 41+1,
41+
54+1, 30+1, 38+1, 5+1, 43+1, 34+1, 59+1, 8+1,
42+
63+1, 16+1, 49+1, 56+1, 46+1, 21+1, 28+1, 32+1,
43+
14+1, 26+1, 24+1, 40+1, 53+1, 37+1, 42+1, 7+1,
44+
62+1, 55+1, 45+1, 31+1, 13+1, 39+1, 36+1, 6+1,
45+
61+1, 44+1, 12+1, 35+1, 60+1, 11+1, 10+1, 9+1,
46+
};
47+
48+
private static readonly int[] k_DeBruijnTableBits32 =
49+
{
50+
0+1, 1+1, 16+1, 2+1, 29+1, 17+1, 3+1, 22+1,
51+
30+1, 20+1, 18+1, 11+1, 13+1, 4+1, 7+1, 23+1,
52+
31+1, 15+1, 28+1, 21+1, 19+1, 10+1, 12+1, 6+1,
53+
14+1, 27+1, 9+1, 5+1, 26+1, 8+1, 25+1, 24+1,
54+
};
55+
56+
/// <summary>
57+
/// Get the minimum number of bytes required to represent the given value
58+
/// </summary>
59+
/// <param name="value">The value</param>
60+
/// <returns>The number of bytes required</returns>
61+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
62+
public static int GetUsedByteCount(uint value)
63+
{
64+
value |= value >> 1;
65+
value |= value >> 2;
66+
value |= value >> 4;
67+
value |= value >> 8;
68+
value |= value >> 16;
69+
value = value & ~(value >> 1);
70+
return k_DeBruijnTableBytes32[value * k_DeBruijnMagic32 >> 27];
71+
}
72+
73+
/// <summary>
74+
/// Get the minimum number of bytes required to represent the given value
75+
/// </summary>
76+
/// <param name="value">The value</param>
77+
/// <returns>The number of bytes required</returns>
78+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
79+
public static int GetUsedByteCount(ulong value)
80+
{
81+
value |= value >> 1;
82+
value |= value >> 2;
83+
value |= value >> 4;
84+
value |= value >> 8;
85+
value |= value >> 16;
86+
value |= value >> 32;
87+
value = value & ~(value >> 1);
88+
return k_DeBruijnTableBytes64[value * k_DeBruijnMagic64 >> 58];
89+
}
90+
91+
/// <summary>
92+
/// Get the minimum number of bits required to represent the given value
93+
/// </summary>
94+
/// <param name="value">The value</param>
95+
/// <returns>The number of bits required</returns>
96+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
97+
public static int GetUsedBitCount(uint value)
98+
{
99+
value |= value >> 1;
100+
value |= value >> 2;
101+
value |= value >> 4;
102+
value |= value >> 8;
103+
value |= value >> 16;
104+
value = value & ~(value >> 1);
105+
return k_DeBruijnTableBits32[value * k_DeBruijnMagic32 >> 27];
106+
}
107+
108+
/// <summary>
109+
/// Get the minimum number of bits required to represent the given value
110+
/// </summary>
111+
/// <param name="value">The value</param>
112+
/// <returns>The number of bits required</returns>
113+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
114+
public static int GetUsedBitCount(ulong value)
115+
{
116+
value |= value >> 1;
117+
value |= value >> 2;
118+
value |= value >> 4;
119+
value |= value >> 8;
120+
value |= value >> 16;
121+
value |= value >> 32;
122+
value = value & ~(value >> 1);
123+
return k_DeBruijnTableBits64[value * k_DeBruijnMagic64 >> 58];
124+
}
125+
}
126+
}

com.unity.netcode.gameobjects/Runtime/Serialization/BitCounter.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
using System;
2+
using System.Runtime.CompilerServices;
3+
using Unity.Collections.LowLevel.Unsafe;
4+
5+
namespace Unity.Netcode
6+
{
7+
/// <summary>
8+
/// Helper class for doing bitwise reads for a FastBufferReader.
9+
/// Ensures all bitwise reads end on proper byte alignment so FastBufferReader doesn't have to be concerned
10+
/// with misaligned reads.
11+
/// </summary>
12+
public ref struct BitReader
13+
{
14+
private Ref<FastBufferReader> m_Reader;
15+
private readonly unsafe byte* m_BufferPointer;
16+
private readonly int m_Position;
17+
private int m_BitPosition;
18+
#if DEVELOPMENT_BUILD || UNITY_EDITOR
19+
private int m_AllowedBitwiseReadMark;
20+
#endif
21+
22+
private const int k_BitsPerByte = 8;
23+
24+
/// <summary>
25+
/// Whether or not the current BitPosition is evenly divisible by 8. I.e. whether or not the BitPosition is at a byte boundary.
26+
/// </summary>
27+
public bool BitAligned
28+
{
29+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
30+
get => (m_BitPosition & 7) == 0;
31+
}
32+
33+
internal unsafe BitReader(ref FastBufferReader reader)
34+
{
35+
m_Reader = new Ref<FastBufferReader>(ref reader);
36+
37+
m_BufferPointer = m_Reader.Value.BufferPointer + m_Reader.Value.Position;
38+
m_Position = m_Reader.Value.Position;
39+
m_BitPosition = 0;
40+
#if DEVELOPMENT_BUILD || UNITY_EDITOR
41+
m_AllowedBitwiseReadMark = (m_Reader.Value.AllowedReadMark - m_Position) * k_BitsPerByte;
42+
#endif
43+
}
44+
45+
/// <summary>
46+
/// Pads the read bit count to byte alignment and commits the read back to the reader
47+
/// </summary>
48+
public void Dispose()
49+
{
50+
var bytesWritten = m_BitPosition >> 3;
51+
if (!BitAligned)
52+
{
53+
// Accounting for the partial read
54+
++bytesWritten;
55+
}
56+
57+
m_Reader.Value.CommitBitwiseReads(bytesWritten);
58+
}
59+
60+
/// <summary>
61+
/// Verifies the requested bit count can be read from the buffer.
62+
/// This exists as a separate method to allow multiple bit reads to be bounds checked with a single call.
63+
/// If it returns false, you may not read, and in editor and development builds, attempting to do so will
64+
/// throw an exception. In release builds, attempting to do so will read junk memory.
65+
/// </summary>
66+
/// <param name="bitCount">Number of bits you want to read, in total</param>
67+
/// <returns>True if you can read, false if that would exceed buffer bounds</returns>
68+
public bool TryBeginReadBits(uint bitCount)
69+
{
70+
var newBitPosition = m_BitPosition + bitCount;
71+
var totalBytesWrittenInBitwiseContext = newBitPosition >> 3;
72+
if ((newBitPosition & 7) != 0)
73+
{
74+
// Accounting for the partial read
75+
++totalBytesWrittenInBitwiseContext;
76+
}
77+
78+
if (m_Reader.Value.PositionInternal + totalBytesWrittenInBitwiseContext > m_Reader.Value.LengthInternal)
79+
{
80+
return false;
81+
}
82+
#if DEVELOPMENT_BUILD || UNITY_EDITOR
83+
m_AllowedBitwiseReadMark = (int)newBitPosition;
84+
#endif
85+
return true;
86+
}
87+
88+
/// <summary>
89+
/// Read a certain amount of bits from the stream.
90+
/// </summary>
91+
/// <param name="value">Value to store bits into.</param>
92+
/// <param name="bitCount">Amount of bits to read</param>
93+
public unsafe void ReadBits(out ulong value, uint bitCount)
94+
{
95+
#if DEVELOPMENT_BUILD || UNITY_EDITOR
96+
if (bitCount > 64)
97+
{
98+
throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read more than 64 bits from a 64-bit value!");
99+
}
100+
101+
if (bitCount < 0)
102+
{
103+
throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read fewer than 0 bits!");
104+
}
105+
106+
int checkPos = (int)(m_BitPosition + bitCount);
107+
if (checkPos > m_AllowedBitwiseReadMark)
108+
{
109+
throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()");
110+
}
111+
#endif
112+
ulong val = 0;
113+
114+
int wholeBytes = (int)bitCount / k_BitsPerByte;
115+
byte* asBytes = (byte*)&val;
116+
if (BitAligned)
117+
{
118+
if (wholeBytes != 0)
119+
{
120+
ReadPartialValue(out val, wholeBytes);
121+
}
122+
}
123+
else
124+
{
125+
for (var i = 0; i < wholeBytes; ++i)
126+
{
127+
ReadMisaligned(out asBytes[i]);
128+
}
129+
}
130+
131+
val |= (ulong)ReadByteBits((int)bitCount & 7) << ((int)bitCount & ~7);
132+
value = val;
133+
}
134+
135+
/// <summary>
136+
/// Read bits from stream.
137+
/// </summary>
138+
/// <param name="value">Value to store bits into.</param>
139+
/// <param name="bitCount">Amount of bits to read.</param>
140+
public void ReadBits(out byte value, uint bitCount)
141+
{
142+
#if DEVELOPMENT_BUILD || UNITY_EDITOR
143+
int checkPos = (int)(m_BitPosition + bitCount);
144+
if (checkPos > m_AllowedBitwiseReadMark)
145+
{
146+
throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()");
147+
}
148+
#endif
149+
value = ReadByteBits((int)bitCount);
150+
}
151+
152+
/// <summary>
153+
/// Read a single bit from the buffer
154+
/// </summary>
155+
/// <param name="bit">Out value of the bit. True represents 1, False represents 0</param>
156+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
157+
public unsafe void ReadBit(out bool bit)
158+
{
159+
#if DEVELOPMENT_BUILD || UNITY_EDITOR
160+
int checkPos = (m_BitPosition + 1);
161+
if (checkPos > m_AllowedBitwiseReadMark)
162+
{
163+
throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginReadBits)}()");
164+
}
165+
#endif
166+
167+
int offset = m_BitPosition & 7;
168+
int pos = m_BitPosition >> 3;
169+
bit = (m_BufferPointer[pos] & (1 << offset)) != 0;
170+
++m_BitPosition;
171+
}
172+
173+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
174+
private unsafe void ReadPartialValue<T>(out T value, int bytesToRead, int offsetBytes = 0) where T : unmanaged
175+
{
176+
var val = new T();
177+
byte* ptr = ((byte*)&val) + offsetBytes;
178+
byte* bufferPointer = m_BufferPointer + m_Position;
179+
UnsafeUtility.MemCpy(ptr, bufferPointer, bytesToRead);
180+
181+
m_BitPosition += bytesToRead * k_BitsPerByte;
182+
value = val;
183+
}
184+
185+
private byte ReadByteBits(int bitCount)
186+
{
187+
if (bitCount > 8)
188+
{
189+
throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read more than 8 bits into an 8-bit value!");
190+
}
191+
192+
if (bitCount < 0)
193+
{
194+
throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read fewer than 0 bits!");
195+
}
196+
197+
int result = 0;
198+
var convert = new ByteBool();
199+
for (int i = 0; i < bitCount; ++i)
200+
{
201+
ReadBit(out bool bit);
202+
result |= convert.Collapse(bit) << i;
203+
}
204+
205+
return (byte)result;
206+
}
207+
208+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
209+
private unsafe void ReadMisaligned(out byte value)
210+
{
211+
int off = m_BitPosition & 7;
212+
int pos = m_BitPosition >> 3;
213+
int shift1 = 8 - off;
214+
215+
value = (byte)((m_BufferPointer[pos] >> shift1) | (m_BufferPointer[(m_BitPosition += 8) >> 3] << shift1));
216+
}
217+
}
218+
}

com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)