using System; using System.Runtime.CompilerServices; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; namespace Unity.Netcode { public struct FastBufferReader : IDisposable { internal struct ReaderHandle { internal unsafe byte* BufferPointer; internal int Position; internal int Length; internal Allocator Allocator; #if DEVELOPMENT_BUILD || UNITY_EDITOR internal int AllowedReadMark; internal bool InBitwiseContext; #endif } internal unsafe ReaderHandle* Handle; /// /// Get the current read position /// public unsafe int Position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Handle->Position; } /// /// Get the total length of the buffer /// public unsafe int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Handle->Length; } /// /// Gets a value indicating whether the reader has been initialized and a handle allocated. /// public unsafe bool IsInitialized => Handle != null; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe void CommitBitwiseReads(int amount) { Handle->Position += amount; #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->InBitwiseContext = false; #endif } private static unsafe ReaderHandle* CreateHandle(byte* buffer, int length, int offset, Allocator allocator) { ReaderHandle* readerHandle = null; if (allocator == Allocator.None) { readerHandle = (ReaderHandle*)UnsafeUtility.Malloc(sizeof(ReaderHandle) + length, UnsafeUtility.AlignOf(), Allocator.Temp); readerHandle->BufferPointer = buffer; readerHandle->Position = offset; } else { readerHandle = (ReaderHandle*)UnsafeUtility.Malloc(sizeof(ReaderHandle) + length, UnsafeUtility.AlignOf(), allocator); UnsafeUtility.MemCpy(readerHandle + 1, buffer + offset, length); readerHandle->BufferPointer = (byte*)(readerHandle + 1); readerHandle->Position = 0; } readerHandle->Length = length; readerHandle->Allocator = allocator; #if DEVELOPMENT_BUILD || UNITY_EDITOR readerHandle->AllowedReadMark = 0; readerHandle->InBitwiseContext = false; #endif return readerHandle; } /// /// Create a FastBufferReader from a NativeArray. /// /// A new buffer will be created using the given allocator and the value will be copied in. /// FastBufferReader will then own the data. /// /// The exception to this is when the allocator passed in is Allocator.None. In this scenario, /// ownership of the data remains with the caller and the reader will point at it directly. /// When created with Allocator.None, FastBufferReader will allocate some internal data using /// Allocator.Temp, so it should be treated as if it's a ref struct and not allowed to outlive /// the context in which it was created (it should neither be returned from that function nor /// stored anywhere in heap memory). /// /// /// /// /// public unsafe FastBufferReader(NativeArray buffer, Allocator allocator, int length = -1, int offset = 0) { Handle = CreateHandle((byte*)buffer.GetUnsafePtr(), length == -1 ? buffer.Length : length, offset, allocator); } /// /// 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. /// /// Allocator.None is not supported for byte[]. If you need this functionality, use a fixed() block /// and ensure the FastBufferReader isn't used outside that block. /// /// 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) { if (allocator == Allocator.None) { throw new NotSupportedException("Allocator.None cannot be used with managed source buffers."); } fixed (byte* data = buffer.Array) { Handle = CreateHandle(data, length == -1 ? buffer.Count : length, offset, allocator); } } /// /// 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. /// /// Allocator.None is not supported for byte[]. If you need this functionality, use a fixed() block /// and ensure the FastBufferReader isn't used outside that block. /// /// 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) { if (allocator == Allocator.None) { throw new NotSupportedException("Allocator.None cannot be used with managed source buffers."); } fixed (byte* data = buffer) { Handle = CreateHandle(data, length == -1 ? buffer.Length : length, offset, allocator); } } /// /// 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 exception to this is when the allocator passed in is Allocator.None. In this scenario, /// ownership of the data remains with the caller and the reader will point at it directly. /// When created with Allocator.None, FastBufferReader will allocate some internal data using /// Allocator.Temp, so it should be treated as if it's a ref struct and not allowed to outlive /// the context in which it was created (it should neither be returned from that function nor /// stored anywhere in heap memory). /// /// 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) { Handle = CreateHandle(buffer, length, offset, allocator); } /// /// 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 exception to this is when the allocator passed in is Allocator.None. In this scenario, /// ownership of the data remains with the caller and the reader will point at it directly. /// When created with Allocator.None, FastBufferReader will allocate some internal data using /// Allocator.Temp, so it should be treated as if it's a ref struct and not allowed to outlive /// the context in which it was created (it should neither be returned from that function nor /// stored anywhere in heap memory). /// /// 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(FastBufferWriter writer, Allocator allocator, int length = -1, int offset = 0) { Handle = CreateHandle(writer.GetUnsafePtr(), length == -1 ? writer.Length : length, offset, allocator); } /// /// Frees the allocated buffer /// public unsafe void Dispose() { UnsafeUtility.Free(Handle, Handle->Allocator); Handle = null; } /// /// Move the read position in the stream /// /// Absolute value to move the position to, truncated to Length [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void Seek(int where) { Handle->Position = Math.Min(Length, where); } /// /// Mark that some bytes are going to be read via GetUnsafePtr(). /// /// Amount that will be read /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe void MarkBytesRead(int amount) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } if (Handle->Position + amount > Handle->AllowedReadMark) { throw new OverflowException("Attempted to read without first calling TryBeginRead()"); } #endif Handle->Position += 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 unsafe BitReader EnterBitwiseContext() { #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->InBitwiseContext = true; #endif return new BitReader(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 unsafe bool TryBeginRead(int bytes) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } #endif if (Handle->Position + bytes > Handle->Length) { return false; } #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->AllowedReadMark = Handle->Position + 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } #endif int len = sizeof(T); if (Handle->Position + len > Handle->Length) { return false; } #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->AllowedReadMark = Handle->Position + 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 unsafe bool TryBeginReadInternal(int bytes) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } #endif if (Handle->Position + bytes > Handle->Length) { return false; } #if DEVELOPMENT_BUILD || UNITY_EDITOR if (Handle->Position + bytes > Handle->AllowedReadMark) { Handle->AllowedReadMark = Handle->Position + 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, Handle->BufferPointer, Length); } return ret; } /// /// Gets a direct pointer to the underlying buffer /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe byte* GetUnsafePtr() { return Handle->BufferPointer; } /// /// Gets a direct pointer to the underlying buffer at the current read position /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe byte* GetUnsafePtrAtCurrentPosition() { return Handle->BufferPointer + Handle->Position; } /// /// Read an INetworkSerializable /// /// INetworkSerializable instance /// /// public void ReadNetworkSerializable(out T value) where T : INetworkSerializable, new() { value = new T(); var bufferSerializer = new BufferSerializer(new BufferSerializerReader(this)); value.NetworkSerialize(bufferSerializer); } /// /// Read an array of INetworkSerializables /// /// INetworkSerializable instance /// /// public void ReadNetworkSerializable(out T[] value) where T : INetworkSerializable, new() { ReadValueSafe(out int size); value = new T[size]; for (var i = 0; i < size; ++i) { ReadNetworkSerializable(out value[i]); } } /// /// 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 (Handle->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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } #endif if (!TryBeginReadInternal(sizeof(int))) { throw new OverflowException("Reading past the end of the buffer"); } ReadValue(out int sizeInTs); int sizeInBytes = sizeInTs * sizeof(T); if (!TryBeginReadInternal(sizeInBytes)) { throw new OverflowException("Reading 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } if (Handle->Position + bytesToRead > Handle->AllowedReadMark) { throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); } #endif var val = new T(); byte* ptr = ((byte*)&val) + offsetBytes; byte* bufferPointer = Handle->BufferPointer + Handle->Position; UnsafeUtility.MemCpy(ptr, bufferPointer, bytesToRead); Handle->Position += 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } if (Handle->Position + 1 > Handle->AllowedReadMark) { throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); } #endif value = Handle->BufferPointer[Handle->Position++]; } /// /// 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 (Handle->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 = Handle->BufferPointer[Handle->Position++]; } /// /// 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } if (Handle->Position + size > Handle->AllowedReadMark) { throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); } #endif UnsafeUtility.MemCpy(value + offset, (Handle->BufferPointer + Handle->Position), size); Handle->Position += 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } #endif if (!TryBeginReadInternal(size)) { throw new OverflowException("Reading past the end of the buffer"); } UnsafeUtility.MemCpy(value + offset, (Handle->BufferPointer + Handle->Position), size); Handle->Position += 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } if (Handle->Position + len > Handle->AllowedReadMark) { throw new OverflowException($"Attempted to read without first calling {nameof(TryBeginRead)}()"); } #endif fixed (T* ptr = &value) { UnsafeUtility.MemCpy((byte*)ptr, Handle->BufferPointer + Handle->Position, len); } Handle->Position += 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferReader in bytewise mode while in a bitwise context."); } #endif if (!TryBeginReadInternal(len)) { throw new OverflowException("Reading past the end of the buffer"); } fixed (T* ptr = &value) { UnsafeUtility.MemCpy((byte*)ptr, Handle->BufferPointer + Handle->Position, len); } Handle->Position += len; } } }