using System.Collections; using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; using UnityEngine.Assertions; namespace Pathfinding.Util { /// /// Implements an efficient circular buffer that can be appended to in both directions. /// /// See: /// public struct NativeCircularBuffer : IReadOnlyList, IReadOnlyCollection where T : unmanaged { [NativeDisableUnsafePtrRestriction] internal unsafe T* data; internal int head; int length; /// Capacity of the allocation minus 1. Invariant: (a power of two) minus 1 int capacityMask; /// The allocator used to create the internal buffer. public AllocatorManager.AllocatorHandle Allocator; /// Number of items in the buffer public readonly int Length => length; /// Absolute index of the first item in the buffer, may be negative or greater than public readonly int AbsoluteStartIndex => head; /// Absolute index of the last item in the buffer, may be negative or greater than public readonly int AbsoluteEndIndex => head + length - 1; /// First item in the buffer throws if the buffer is empty public readonly ref T First { [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] get { unsafe { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (length == 0) throw new System.InvalidOperationException(); #endif return ref data[head & capacityMask]; } } } /// Last item in the buffer, throws if the buffer is empty public readonly ref T Last { [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] get { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (length == 0) throw new System.InvalidOperationException(); #endif unsafe { return ref data[(head+length-1) & capacityMask]; } } } readonly int IReadOnlyCollection.Count => Length; public readonly bool IsCreated { get { unsafe { return data != null; } } } /// Create a new empty buffer public NativeCircularBuffer(AllocatorManager.AllocatorHandle allocator) { unsafe { data = null; } Allocator = allocator; capacityMask = -1; head = 0; length = 0; } /// Create a new buffer with the given capacity public NativeCircularBuffer(int initialCapacity, AllocatorManager.AllocatorHandle allocator) { initialCapacity = math.ceilpow2(initialCapacity); unsafe { data = AllocatorManager.Allocate(allocator, initialCapacity); capacityMask = initialCapacity - 1; } Allocator = allocator; head = 0; length = 0; } unsafe public NativeCircularBuffer(CircularBuffer buffer, out ulong gcHandle) : this(buffer.data, buffer.head, buffer.Length, out gcHandle) {} unsafe public NativeCircularBuffer(T[] data, int head, int length, out ulong gcHandle) { Assert.IsTrue((data.Length & (data.Length - 1)) == 0); Assert.IsTrue(length <= data.Length); unsafe { this.data = (T*)UnsafeUtility.PinGCArrayAndGetDataAddress(data, out gcHandle); } this.capacityMask = data.Length - 1; this.head = head; this.length = length; Allocator = Unity.Collections.Allocator.None; } /// Resets the buffer's length to zero. Does not clear the current allocation public void Clear () { length = 0; head = 0; } /// Appends a list of items to the end of the buffer public void AddRange (List items) { // TODO: Can be optimized for (int i = 0; i < items.Count; i++) PushEnd(items[i]); } /// Pushes a new item to the start of the buffer [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void PushStart (T item) { if (length > capacityMask) Grow(); length += 1; head -= 1; this[0] = item; } /// Pushes a new item to the end of the buffer [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void PushEnd (T item) { if (length > capacityMask) Grow(); length += 1; this[length-1] = item; } /// Pushes a new item to the start or the end of the buffer [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Push (bool toStart, T item) { if (toStart) PushStart(item); else PushEnd(item); } /// Removes and returns the first element public T PopStart () { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (length == 0) throw new System.InvalidOperationException(); #endif var r = this[0]; head++; length--; return r; } /// Removes and returns the last element public T PopEnd () { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (length == 0) throw new System.InvalidOperationException(); #endif var r = this[length-1]; length--; return r; } /// Pops either from the start or from the end of the buffer public T Pop (bool fromStart) { if (fromStart) return PopStart(); else return PopEnd(); } /// Return either the first element or the last element public readonly T GetBoundaryValue (bool start) { return start ? GetAbsolute(AbsoluteStartIndex) : GetAbsolute(AbsoluteEndIndex); } /// Lowers the length of the buffer to the given value, and does nothing if the given value is greater or equal to the current length public void TrimTo (int length) { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (length < 0) throw new System.ArgumentOutOfRangeException(); #endif this.length = math.min(this.length, length); } /// Removes toRemove items from the buffer, starting at startIndex, and then inserts the toInsert items at startIndex public void Splice (int startIndex, int toRemove, List toInsert) { SpliceAbsolute(startIndex + head, toRemove, toInsert); } /// Like , but startIndex is an absolute index public void SpliceAbsolute (int startIndex, int toRemove, List toInsert) { SpliceUninitializedAbsolute(startIndex, toRemove, toInsert.Count); unsafe { for (int i = 0; i < toInsert.Count; i++) data[(startIndex + i) & capacityMask] = toInsert[i]; } } /// Like , but the newly inserted items are left in an uninitialized state public void SpliceUninitialized (int startIndex, int toRemove, int toInsert) { SpliceUninitializedAbsolute(startIndex + head, toRemove, toInsert); } /// Like , but startIndex is an absolute index public void SpliceUninitializedAbsolute (int startIndex, int toRemove, int toInsert) { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (startIndex - head < 0 || startIndex + toRemove - head > length) throw new System.ArgumentOutOfRangeException(); #endif var itemsToAdd = toInsert - toRemove; while (this.length + itemsToAdd > capacityMask + 1) Grow(); // move items [startIndex+length .. end] itemsToAdd steps forward in the array MoveAbsolute(startIndex + toRemove, AbsoluteEndIndex, itemsToAdd); this.length += itemsToAdd; } void MoveAbsolute (int startIndex, int endIndex, int deltaIndex) { unsafe { if (deltaIndex > 0) { for (int i = endIndex; i >= startIndex; i--) data[(i+deltaIndex) & capacityMask] = data[i & capacityMask]; } else if (deltaIndex < 0) { for (int i = startIndex; i <= endIndex; i++) data[(i+deltaIndex) & capacityMask] = data[i & capacityMask]; } } } /// Indexes the buffer, with index 0 being the first element public T this[int index] { [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] readonly get { #if ENABLE_UNITY_COLLECTIONS_CHECKS if ((uint)index >= length) throw new System.ArgumentOutOfRangeException(); #endif unsafe { return data[(index+head) & capacityMask]; } } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] set { #if ENABLE_UNITY_COLLECTIONS_CHECKS if ((uint)index >= length) throw new System.ArgumentOutOfRangeException(); #endif unsafe { data[(index+head) & capacityMask] = value; } } } /// /// Indexes the buffer using absolute indices. /// When pushing to and popping from the buffer, the absolute indices do not change. /// So e.g. after doing PushStart(x) on an empty buffer, GetAbsolute(-1) will get the newly pushed element. /// [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public readonly T GetAbsolute (int index) { #if ENABLE_UNITY_COLLECTIONS_CHECKS if ((uint)(index - head) >= length) throw new System.ArgumentOutOfRangeException(); #endif unsafe { return data[index & capacityMask]; } } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] void Grow () { unsafe { // Note: Will always be a power of 2 since capacity is a power of 2 var capacity = capacityMask + 1; var newCapacity = math.max(4, capacity*2); var newData = AllocatorManager.Allocate(this.Allocator, newCapacity); if (data != null) { var inOrderItems = capacity - (head & capacityMask); UnsafeUtility.MemCpy(newData + (head & (newCapacity - 1)), data + (head & capacityMask), inOrderItems * sizeof(T)); var wraparoundItems = length - inOrderItems; if (wraparoundItems > 0) { UnsafeUtility.MemCpy(newData + ((head + inOrderItems) & (newCapacity - 1)), data, wraparoundItems * sizeof(T)); } AllocatorManager.Free(Allocator, data); } capacityMask = newCapacity - 1; data = newData; } } /// Releases the unmanaged memory held by this container public void Dispose () { capacityMask = -1; length = 0; head = 0; unsafe { AllocatorManager.Free(Allocator, data); data = null; } } public IEnumerator GetEnumerator () { for (int i = 0; i < length; i++) { yield return this[i]; } } IEnumerator IEnumerable.GetEnumerator () { for (int i = 0; i < length; i++) { yield return this[i]; } } public NativeCircularBuffer Clone () { unsafe { var newData = AllocatorManager.Allocate(this.Allocator, capacityMask + 1); UnsafeUtility.MemCpy(newData, data, length * sizeof(T)); return new NativeCircularBuffer { data = newData, head = head, length = length, capacityMask = capacityMask, Allocator = this.Allocator }; } } } }