using System; using System.Runtime.CompilerServices; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; namespace Unity.Netcode { public struct FastBufferWriter : IDisposable { internal struct WriterHandle { internal unsafe byte* BufferPointer; internal int Position; internal int Length; internal int Capacity; internal int MaxCapacity; internal Allocator Allocator; internal bool BufferGrew; #if DEVELOPMENT_BUILD || UNITY_EDITOR internal int AllowedWriteMark; internal bool InBitwiseContext; #endif } internal readonly unsafe WriterHandle* Handle; private static byte[] s_ByteArrayCache = new byte[65535]; /// /// The current write position /// public unsafe int Position { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Handle->Position; } /// /// The current total buffer size /// public unsafe int Capacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Handle->Capacity; } /// /// The maximum possible total buffer size /// public unsafe int MaxCapacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Handle->MaxCapacity; } /// /// The total amount of bytes that have been written to the stream /// public unsafe int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Handle->Position > Handle->Length ? Handle->Position : Handle->Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe void CommitBitwiseWrites(int amount) { Handle->Position += amount; #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->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) { // Allocating both the Handle struct and the buffer in a single allocation - sizeof(WriterHandle) + size // The buffer for the initial allocation is the next block of memory after the handle itself. // If the buffer grows, a new buffer will be allocated and the handle pointer pointed at the new location... // The original buffer won't be deallocated until the writer is destroyed since it's part of the handle allocation. Handle = (WriterHandle*)UnsafeUtility.Malloc(sizeof(WriterHandle) + size, UnsafeUtility.AlignOf(), allocator); #if DEVELOPMENT_BUILD || UNITY_EDITOR UnsafeUtility.MemSet(Handle, 0, sizeof(WriterHandle) + size); #endif Handle->BufferPointer = (byte*)(Handle + 1); Handle->Position = 0; Handle->Length = 0; Handle->Capacity = size; Handle->Allocator = allocator; Handle->MaxCapacity = maxSize < size ? size : maxSize; Handle->BufferGrew = false; #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->AllowedWriteMark = 0; Handle->InBitwiseContext = false; #endif } /// /// Frees the allocated buffer /// public unsafe void Dispose() { if (Handle->BufferGrew) { UnsafeUtility.Free(Handle->BufferPointer, Handle->Allocator); } UnsafeUtility.Free(Handle, Handle->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 unsafe 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, Handle->Capacity); if (Handle->Position > Handle->Length && where < Handle->Position) { Handle->Length = Handle->Position; } Handle->Position = 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 unsafe void Truncate(int where = -1) { if (where == -1) { where = Position; } if (Handle->Position > where) { Handle->Position = where; } if (Handle->Length > where) { Handle->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 unsafe BitWriter EnterBitwiseContext() { #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->InBitwiseContext = true; #endif return new BitWriter(this); } internal unsafe void Grow(int additionalSizeRequired) { var desiredSize = Handle->Capacity * 2; while (desiredSize < Position + additionalSizeRequired) { desiredSize *= 2; } var newSize = Math.Min(desiredSize, Handle->MaxCapacity); byte* newBuffer = (byte*)UnsafeUtility.Malloc(newSize, UnsafeUtility.AlignOf(), Handle->Allocator); #if DEVELOPMENT_BUILD || UNITY_EDITOR UnsafeUtility.MemSet(newBuffer, 0, newSize); #endif UnsafeUtility.MemCpy(newBuffer, Handle->BufferPointer, Length); if (Handle->BufferGrew) { UnsafeUtility.Free(Handle->BufferPointer, Handle->Allocator); } Handle->BufferGrew = true; Handle->BufferPointer = newBuffer; Handle->Capacity = 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 unsafe bool TryBeginWrite(int bytes) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferWriter in bytewise mode while in a bitwise context."); } #endif if (Handle->Position + bytes > Handle->Capacity) { if (Handle->Position + bytes > Handle->MaxCapacity) { return false; } if (Handle->Capacity < Handle->MaxCapacity) { Grow(bytes); } else { return false; } } #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->AllowedWriteMark = Handle->Position + 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferWriter in bytewise mode while in a bitwise context."); } #endif int len = sizeof(T); if (Handle->Position + len > Handle->Capacity) { if (Handle->Position + len > Handle->MaxCapacity) { return false; } if (Handle->Capacity < Handle->MaxCapacity) { Grow(len); } else { return false; } } #if DEVELOPMENT_BUILD || UNITY_EDITOR Handle->AllowedWriteMark = Handle->Position + 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 unsafe bool TryBeginWriteInternal(int bytes) { #if DEVELOPMENT_BUILD || UNITY_EDITOR if (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferWriter in bytewise mode while in a bitwise context."); } #endif if (Handle->Position + bytes > Handle->Capacity) { if (Handle->Position + bytes > Handle->MaxCapacity) { return false; } if (Handle->Capacity < Handle->MaxCapacity) { Grow(bytes); } else { return false; } } #if DEVELOPMENT_BUILD || UNITY_EDITOR if (Handle->Position + bytes > Handle->AllowedWriteMark) { Handle->AllowedWriteMark = 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; } /// /// Uses a static cached array to create an array segment with no allocations. /// This array can only be used until the next time ToTempByteArray() is called on ANY FastBufferWriter, /// as the cached buffer is shared by all of them and will be overwritten. /// As such, this should be used with care. /// /// internal unsafe ArraySegment ToTempByteArray() { var length = Length; if (length > s_ByteArrayCache.Length) { return new ArraySegment(ToArray(), 0, length); } fixed (byte* b = s_ByteArrayCache) { UnsafeUtility.MemCpy(b, Handle->BufferPointer, length); } return new ArraySegment(s_ByteArrayCache, 0, length); } /// /// 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; } /// /// 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)); } /// /// Write an INetworkSerializable /// /// The value to write /// public void WriteNetworkSerializable(in T value) where T : INetworkSerializable { var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(this)); value.NetworkSerialize(bufferSerializer); } /// /// Write an array of INetworkSerializables /// /// The value to write /// /// /// public void WriteNetworkSerializable(T[] array, int count = -1, int offset = 0) where T : INetworkSerializable { int sizeInTs = count != -1 ? count : array.Length - offset; WriteValueSafe(sizeInTs); foreach (var item in array) { WriteNetworkSerializable(item); } } /// /// 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 (Handle->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 (Handle->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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferWriter in bytewise mode while in a bitwise context."); } if (Handle->Position + bytesToWrite > Handle->AllowedWriteMark) { throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); } #endif byte* ptr = ((byte*)&value) + offsetBytes; byte* bufferPointer = Handle->BufferPointer + Handle->Position; UnsafeUtility.MemCpy(bufferPointer, ptr, bytesToWrite); Handle->Position += 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferWriter in bytewise mode while in a bitwise context."); } if (Handle->Position + 1 > Handle->AllowedWriteMark) { throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); } #endif Handle->BufferPointer[Handle->Position++] = 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 (Handle->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"); } Handle->BufferPointer[Handle->Position++] = 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferWriter in bytewise mode while in a bitwise context."); } if (Handle->Position + size > Handle->AllowedWriteMark) { throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); } #endif UnsafeUtility.MemCpy((Handle->BufferPointer + Handle->Position), value + offset, size); Handle->Position += 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 (Handle->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((Handle->BufferPointer + Handle->Position), value + offset, size); Handle->Position += 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 = -1, int offset = 0) { fixed (byte* ptr = value) { WriteBytes(ptr, size == -1 ? value.Length : 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 = -1, int offset = 0) { fixed (byte* ptr = value) { WriteBytesSafe(ptr, size == -1 ? value.Length : 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(Handle->BufferPointer, Handle->Position); } /// /// 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.Handle->BufferPointer, other.Handle->Position); } /// /// 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 (Handle->InBitwiseContext) { throw new InvalidOperationException( "Cannot use BufferWriter in bytewise mode while in a bitwise context."); } if (Handle->Position + len > Handle->AllowedWriteMark) { throw new OverflowException($"Attempted to write without first calling {nameof(TryBeginWrite)}()"); } #endif fixed (T* ptr = &value) { UnsafeUtility.MemCpy(Handle->BufferPointer + Handle->Position, (byte*)ptr, len); } Handle->Position += 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 (Handle->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(Handle->BufferPointer + Handle->Position, (byte*)ptr, len); } Handle->Position += len; } } }