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 FastBufferReader 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; private int BytePosition => m_BitPosition >> 3; /// /// 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(FastBufferReader reader) { m_Reader = reader; m_BufferPointer = m_Reader.Handle->BufferPointer + m_Reader.Handle->Position; m_Position = m_Reader.Handle->Position; m_BitPosition = 0; #if DEVELOPMENT_BUILD || UNITY_EDITOR m_AllowedBitwiseReadMark = (m_Reader.Handle->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.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 unsafe 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.Handle->Position + totalBytesWrittenInBitwiseContext > m_Reader.Handle->Length) { 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!"); } 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 = BytePosition; 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 + BytePosition; 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] >> off) | (m_BufferPointer[(m_BitPosition += 8) >> 3] << shift1)); } } }