namespace Pathfinding.Util { using Unity.Mathematics; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; /// /// A tiny slab allocator. /// Allocates spans of type T in power-of-two sized blocks. /// /// Note: This allocator has no support for merging adjacent freed blocks. /// Therefore it is best suited for similarly sized allocations which are relatively small. /// /// Can be used in burst jobs. /// /// This is faster than allocating NativeArrays using the Temp allocator, and significantly faster /// than allocating them using the Persistent allocator. /// public struct SlabAllocator where T : unmanaged { public const int MaxAllocationSizeIndex = 10; const uint UsedBit = 1u << 31; const uint AllocatedBit = 1u << 30; const uint LengthMask = AllocatedBit - 1; /// Allocation which is always invalid public const int InvalidAllocation = -2; /// Allocation representing a zero-length array public const int ZeroLengthArray = -1; [NativeDisableUnsafePtrRestriction] unsafe AllocatorData* data; struct AllocatorData { public UnsafeList mem; public unsafe fixed int freeHeads[MaxAllocationSizeIndex+1]; } struct Header { public uint length; } struct NextBlock { public int next; } public bool IsCreated { get { unsafe { return data != null; } } } public int ByteSize { get { unsafe { return data->mem.Length; } } } public SlabAllocator(int initialCapacityBytes, AllocatorManager.AllocatorHandle allocator) { unsafe { data = AllocatorManager.Allocate(allocator); data->mem = new UnsafeList(initialCapacityBytes, allocator); Clear(); } } /// /// Frees all existing allocations. /// Does not free the underlaying unmanaged memory. Use for that. /// public void Clear () { CheckDisposed(); unsafe { data->mem.Clear(); for (int i = 0; i < MaxAllocationSizeIndex + 1; i++) { data->freeHeads[i] = -1; } } } /// /// Get the span representing the given allocation. /// The returned array does not need to be disposed. /// It is only valid until the next call to , or . /// public UnsafeSpan GetSpan (int allocatedIndex) { CheckDisposed(); unsafe { if (allocatedIndex == ZeroLengthArray) return new UnsafeSpan(null, 0); #if ENABLE_UNITY_COLLECTIONS_CHECKS if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException($"Invalid allocation {allocatedIndex}"); #endif var ptr = data->mem.Ptr + allocatedIndex; var header = (Header*)(ptr - sizeof(Header)); var length = header->length & LengthMask; #if ENABLE_UNITY_COLLECTIONS_CHECKS if (length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception($"Invalid allocation {allocatedIndex}"); if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to get a span for an unallocated index"); #endif return new UnsafeSpan(ptr, (int)length); } } public List GetList (int allocatedIndex) { return new List(this, allocatedIndex); } public void Realloc (ref int allocatedIndex, int nElements) { CheckDisposed(); if (allocatedIndex == ZeroLengthArray) { allocatedIndex = Allocate(nElements); return; } unsafe { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException(); #endif var ptr = data->mem.Ptr + allocatedIndex; var header = (Header*)(ptr - sizeof(Header)); var length = header->length & LengthMask; #if ENABLE_UNITY_COLLECTIONS_CHECKS if (length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception("Invalid index"); if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to get a span for an unallocated index"); #endif var capacityIndex = ElementsToSizeIndex((int)length); var newCapacityIndex = ElementsToSizeIndex((int)nElements); if (capacityIndex == newCapacityIndex) { header->length = (uint)nElements | AllocatedBit | UsedBit; } else { int newAllocation = Allocate(nElements); var oldSpan = GetSpan(allocatedIndex); var newSpan = GetSpan(newAllocation); oldSpan.Slice(0, math.min((int)length, nElements)).CopyTo(newSpan); Free(allocatedIndex); allocatedIndex = newAllocation; } } } internal static int SizeIndexToElements (int sizeIndex) { return 1 << sizeIndex; } internal static int ElementsToSizeIndex (int nElements) { if (nElements < 0) throw new System.Exception("SlabAllocator cannot allocate less than 1 element"); if (nElements == 0) return 0; int sizeIndex = CollectionHelper.Log2Ceil(nElements); if (sizeIndex > MaxAllocationSizeIndex) throw new System.Exception("SlabAllocator cannot allocate more than 2^(MaxAllocationSizeIndex-1) elements"); return sizeIndex; } /// /// Allocates an array big enough to fit the given values and copies them to the new allocation. /// Returns: An ID for the new allocation. /// public int Allocate (System.Collections.Generic.List values) { var index = Allocate(values.Count); var span = GetSpan(index); for (int i = 0; i < span.Length; i++) span[i] = values[i]; return index; } /// /// Allocates an array big enough to fit the given values and copies them to the new allocation. /// Returns: An ID for the new allocation. /// public int Allocate (NativeList values) { var index = Allocate(values.Length); GetSpan(index).CopyFrom(values.AsArray()); return index; } /// /// Allocates an array of type T with length nElements. /// Must later be freed using (or . /// /// Returns: An ID for the new allocation. /// public int Allocate (int nElements) { CheckDisposed(); if (nElements == 0) return ZeroLengthArray; var sizeIndex = ElementsToSizeIndex(nElements); unsafe { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (sizeIndex < 0 || sizeIndex > MaxAllocationSizeIndex) throw new System.Exception("Invalid size index " + sizeIndex); #endif int head = data->freeHeads[sizeIndex]; if (head != -1) { var ptr = data->mem.Ptr; data->freeHeads[sizeIndex] = ((NextBlock*)(ptr + head))->next; *(Header*)(ptr + head - sizeof(Header)) = new Header { length = (uint)nElements | UsedBit | AllocatedBit }; return head; } int headerStart = data->mem.Length; int requiredSize = headerStart + sizeof(Header) + SizeIndexToElements(sizeIndex)*sizeof(T); if (Unity.Burst.CompilerServices.Hint.Unlikely(requiredSize > data->mem.Capacity)) { data->mem.SetCapacity(math.max(data->mem.Capacity*2, requiredSize)); } // Set the length field directly because we know we don't have to resize the list, // and we do not care about zeroing the memory. data->mem.m_length = requiredSize; *(Header*)(data->mem.Ptr + headerStart) = new Header { length = (uint)nElements | UsedBit | AllocatedBit }; return headerStart + sizeof(Header); } } /// Frees a single allocation public void Free (int allocatedIndex) { CheckDisposed(); if (allocatedIndex == ZeroLengthArray) return; unsafe { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException(); #endif var ptr = data->mem.Ptr; var header = (Header*)(ptr + allocatedIndex - sizeof(Header)); var length = (int)(header->length & LengthMask); #if ENABLE_UNITY_COLLECTIONS_CHECKS if (length < 0 || length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception("Invalid index"); if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to free an already freed index"); #endif var sizeIndex = ElementsToSizeIndex(length); *(NextBlock*)(ptr + allocatedIndex) = new NextBlock { next = data->freeHeads[sizeIndex] }; data->freeHeads[sizeIndex] = allocatedIndex; // Mark as not allocated header->length &= ~(AllocatedBit | UsedBit); } } public void CopyTo (SlabAllocator other) { CheckDisposed(); other.CheckDisposed(); unsafe { other.data->mem.CopyFrom(data->mem); for (int i = 0; i < MaxAllocationSizeIndex + 1; i++) { other.data->freeHeads[i] = data->freeHeads[i]; } } } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] void CheckDisposed () { #if ENABLE_UNITY_COLLECTIONS_CHECKS unsafe { if (data == null) throw new System.InvalidOperationException("SlabAllocator is already disposed or not initialized"); } #endif } /// Frees all unmanaged memory associated with this container public void Dispose () { unsafe { if (data == null) return; var allocator = data->mem.Allocator; data->mem.Dispose(); AllocatorManager.Free(allocator, data); data = null; } } public ref struct List { public UnsafeSpan span; SlabAllocator allocator; // TODO: Can be derived from span public int allocationIndex; public List(SlabAllocator allocator, int allocationIndex) { this.span = allocator.GetSpan(allocationIndex); this.allocator = allocator; this.allocationIndex = allocationIndex; } public void Add (T value) { allocator.Realloc(ref allocationIndex, span.Length + 1); span = allocator.GetSpan(allocationIndex); span[span.Length - 1] = value; } public void RemoveAt (int index) { span.Slice(index + 1).CopyTo(span.Slice(index, span.Length - index - 1)); allocator.Realloc(ref allocationIndex, span.Length - 1); span = allocator.GetSpan(allocationIndex); } public void Clear () { allocator.Realloc(ref allocationIndex, 0); span = allocator.GetSpan(allocationIndex); } public int Length => span.Length; public ref T this[int index] { [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] get { return ref span[index]; } } } } public static class SlabListExtensions { public static void Remove(ref this SlabAllocator.List list, T value) where T : unmanaged, System.IEquatable { int idx = list.span.IndexOf(value); if (idx != -1) list.RemoveAt(idx); } } }