using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Utilities; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Misaki.HighPerformance.LowLevel.Collections; internal class UnsafeChunkedListDebugView where T : unmanaged { private readonly UnsafeChunkedList _list; public UnsafeChunkedListDebugView(UnsafeChunkedList list) { _list = list; } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public T[] Items { get { var array = new T[_list.Count]; _list.CopyTo(array); return array; } } } /// /// A collection that stores elements in fixed-size chunks, enabling stable element addresses /// and eliminating large reallocation during growth. Adding elements never moves existing ones. /// /// Represents a type that can be stored in the collection, constrained to unmanaged types for performance and safety. [DebuggerTypeProxy(typeof(UnsafeChunkedListDebugView<>))] public unsafe struct UnsafeChunkedList : IUnsafeCollection where T : unmanaged { public const int DEFAULT_CHUNK_SIZE_IN_BYTES = 16384; public ref struct Enumerator { private ref UnsafeChunkedList _collection; private int _index; public readonly ref T Current { get { var (chunkIdx, offset) = SplitIndex(_index, _collection._chunkCapacity); return ref ((T*)_collection._chunks[chunkIdx])[offset]; } } public Enumerator(ref UnsafeChunkedList collection) { _collection = ref collection; _index = -1; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { _index++; return _index < _collection._count; } public void Reset() { _index = -1; } } /// /// A parallel reader for an UnsafeChunkedList. /// public readonly unsafe struct ParallelReader { public readonly UnsafeChunkedList* listData; public readonly int Count => listData->_count; public readonly int ChunkCapacity => listData->_chunkCapacity; public ref readonly T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { var (chunkIdx, offset) = SplitIndex(index, listData->_chunkCapacity); return ref ((T*)listData->_chunks[chunkIdx])[offset]; } } public ref readonly T this[uint index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref this[(int)index]; } internal ParallelReader(UnsafeChunkedList* list) { listData = list; } public readonly Enumerator GetEnumerator() { ref var list = ref Unsafe.AsRef>(listData); return new Enumerator(ref list); } } /// /// A parallel writer for an UnsafeChunkedList. /// /// /// Adding elements is thread-safe and auto-allocates chunks as needed, since new chunks never move existing data. /// The chunk pointer array must be pre-sized via before dispatching parallel writes. /// public readonly struct ParallelWriter { public readonly UnsafeChunkedList* listData; internal ParallelWriter(UnsafeChunkedList* list) { listData = list; } /// /// Thread-safely adds a value, auto-allocating new chunks as needed. /// public void Add(scoped in T value) { var idx = Interlocked.Increment(ref listData->_count) - 1; var (chunkIdx, offset) = SplitIndex(idx, listData->_chunkCapacity); listData->EnsureChunkParallel(chunkIdx); ((T*)listData->_chunks[chunkIdx])[offset] = value; } /// /// Thread-safely adds a range of elements, auto-allocating new chunks as needed. /// public void AddRange(ReadOnlySpan collection) { var count = collection.Length; var index = Interlocked.Add(ref listData->_count, count) - count; fixed (T* pCollection = collection) { var remaining = count; T* srcPtr = pCollection; var currentIndex = index; while (remaining > 0) { var (chunkIdx, offset) = SplitIndex(currentIndex, listData->_chunkCapacity); var copyCount = Math.Min(remaining, listData->_chunkCapacity - offset); listData->EnsureChunkParallel(chunkIdx); var dstPtr = (T*)listData->_chunks[chunkIdx] + offset; MemoryUtility.MemCpy(dstPtr, srcPtr, (nuint)(copyCount * sizeof(T))); srcPtr += copyCount; currentIndex += copyCount; remaining -= copyCount; } } } } private UnsafeArray _chunks; private int _chunkCount; private int _count; private readonly int _chunkCapacity; private readonly AllocationHandle _allocationHandle; public readonly int Count => _count; public readonly int ChunkCapacity => _chunkCapacity; public readonly int ChunkCount => _chunkCount; public readonly int Capacity => _chunkCount * _chunkCapacity; public readonly bool IsCreated => _chunks.IsCreated; public readonly ref T this[int index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { var (chunkIdx, offset) = SplitIndex(index, _chunkCapacity); return ref ((T*)_chunks[chunkIdx])[offset]; } } public readonly ref T this[uint index] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { var (chunkIdx, offset) = SplitIndex((int)index, _chunkCapacity); return ref ((T*)_chunks[chunkIdx])[offset]; } } /// /// Invalid constructor, use instead. /// public UnsafeChunkedList() : this(DEFAULT_CHUNK_SIZE_IN_BYTES / sizeof(T), AllocationHandle.Persistent) { } /// /// Initializes a new instance with a specified chunk capacity and allocator. /// /// The maximum number of elements per chunk. /// A reference to an AllocationHandle that manages memory allocation. /// Specifies how the memory should be allocated. public UnsafeChunkedList(int chunkCapacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None) { chunkCapacity = Math.Max(1, chunkCapacity); _chunks = new UnsafeArray(4, handle, allocationOption); _chunkCount = 0; _count = 0; _chunkCapacity = chunkCapacity; _allocationHandle = handle; } /// /// Initializes a new instance with a specified chunk capacity and an allocation type. /// [Obsolete("Use AllocationHandle instead.")] public UnsafeChunkedList(int chunkCapacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None) : this(chunkCapacity, AllocationManager.GetAllocationHandle(allocator), allocationOption) { } [Conditional("MHP_ENABLE_SAFETY_CHECKS")] private readonly void CheckIndexBounds(int index) { if (index < 0 || index >= _count) { throw new ArgumentOutOfRangeException(nameof(index)); } } [Conditional("MHP_ENABLE_SAFETY_CHECKS")] private readonly void CheckIndexCount(int index, int count) { if (count < 0) { throw new ArgumentOutOfRangeException($"Value for count {count} must be positive."); } if (index < 0) { throw new ArgumentOutOfRangeException($"Value for index {index} must be positive."); } if (index > Count) { throw new ArgumentOutOfRangeException($"Value for index {index} is out of bounds."); } if (index + count > Count) { throw new ArgumentOutOfRangeException($"Value for count {count} is out of bounds."); } } [Conditional("MHP_ENABLE_SAFETY_CHECKS")] private readonly void ThrowIfNotCreated() { if (!IsCreated) { throw new InvalidOperationException("The UnsafeChunkedList is not created."); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (int chunkIndex, int offset) SplitIndex(int index, int chunkCapacity) { return (index / chunkCapacity, index % chunkCapacity); } private void GrowChunkArray(int minCapacity) { var newCapacity = Math.Max(minCapacity, Math.Max(_chunks.Count * 2, 4)); _chunks.Resize(newCapacity); } private void AllocateChunk(int chunkIndex) { if (chunkIndex >= _chunks.Count) { GrowChunkArray(chunkIndex + 1); } var sizeInBytes = (nuint)(_chunkCapacity * sizeof(T)); _chunks[chunkIndex] = (nint)_allocationHandle.Alloc(sizeInBytes, MemoryUtility.AlignOf()); _chunkCount = Math.Max(_chunkCount, chunkIndex + 1); } private void EnsureChunkIndex(int elementIndex) { if (elementIndex < 0) { return; } var (chunkIdx, _) = SplitIndex(elementIndex, _chunkCapacity); while (_chunkCount <= chunkIdx) { AllocateChunk(_chunkCount); } } private void EnsureChunkParallel(int chunkIndex) { if (chunkIndex < Volatile.Read(ref _chunkCount)) { return; } var chunksPtr = (nint*)_chunks.GetUnsafePtr(); while (true) { var currentCount = Volatile.Read(ref _chunkCount); if (chunkIndex < currentCount) { return; } var toAlloc = currentCount; if (toAlloc >= _chunks.Count) { Thread.SpinWait(1); continue; } var sizeInBytes = (nuint)(_chunkCapacity * sizeof(T)); var data = (nint)_allocationHandle.Alloc(sizeInBytes, MemoryUtility.AlignOf()); var old = Interlocked.CompareExchange(ref chunksPtr[toAlloc], data, 0); if (old == 0) { Interlocked.Increment(ref _chunkCount); if (chunkIndex >= currentCount + 1) { continue; } return; } _allocationHandle.Free((void*)data); } } private void FreeChunk(int chunkIndex) { var ptr = (void*)_chunks[chunkIndex]; if (ptr != null) { _allocationHandle.Free(ptr); _chunks[chunkIndex] = 0; } } private void FreeTrailingEmptyChunks() { var neededChunks = _count > 0 ? (_count + _chunkCapacity - 1) / _chunkCapacity : 0; while (_chunkCount > neededChunks) { _chunkCount--; FreeChunk(_chunkCount); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] [UnscopedRef] public Enumerator GetEnumerator() { return new Enumerator(ref this); } /// /// Provides a parallel reader for the current list, enabling thread-safe read operations. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public ParallelReader AsParallelReader() { return new((UnsafeChunkedList*)Unsafe.AsPointer(ref this)); } /// /// Provides a parallel writer for the current list, enabling thread-safe additions. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public ParallelWriter AsParallelWriter() { return new((UnsafeChunkedList*)Unsafe.AsPointer(ref this)); } /// /// Adds a new element to the end of the list, allocating new chunks as needed. /// public void Add(scoped in T value) { EnsureChunkIndex(_count); var (chunkIdx, offset) = SplitIndex(_count, _chunkCapacity); ((T*)_chunks[chunkIdx])[offset] = value; _count++; } /// /// Adds the specified value to the collection. For chunked lists, this is equivalent to , /// since allocating new chunks never moves existing elements. /// public void AddNoResize(scoped in T value) { EnsureChunkIndex(_count); var (chunkIdx, offset) = SplitIndex(_count, _chunkCapacity); ((T*)_chunks[chunkIdx])[offset] = value; _count++; } /// /// Adds a range of elements to the collection, allocating new chunks as needed. /// public void AddRange(ReadOnlySpan values) { if (values.Length == 0) { return; } EnsureChunkIndex(_count + values.Length - 1); CopyFromSpan(values, _count); _count += values.Length; } /// /// Adds a range of elements from a pointer to the collection, allocating new chunks as needed. /// public void AddRange(T* ptr, int count) { if (count <= 0) { return; } EnsureChunkIndex(_count + count - 1); CopyFromPtr(ptr, _count, count); _count += count; } /// /// Adds a range of elements. For chunked lists, this is equivalent to , /// since allocating new chunks never moves existing elements. /// public void AddRangeNoResize(ReadOnlySpan collection) { if (collection.Length == 0) { return; } EnsureChunkIndex(_count + collection.Length - 1); CopyFromSpan(collection, _count); _count += collection.Length; } /// /// Adds a range of elements from a pointer. For chunked lists, this is equivalent to , /// since allocating new chunks never moves existing elements. /// public void AddRangeNoResize(T* ptr, int count) { if (count <= 0) { return; } EnsureChunkIndex(_count + count - 1); CopyFromPtr(ptr, _count, count); _count += count; } private void CopyFromSpan(ReadOnlySpan source, int startIndex) { fixed (T* pSrc = source) { CopyFromPtr(pSrc, startIndex, source.Length); } } private void CopyFromPtr(T* srcPtr, int startIndex, int count) { var remaining = count; var src = srcPtr; var currentIndex = startIndex; while (remaining > 0) { var (chunkIdx, offset) = SplitIndex(currentIndex, _chunkCapacity); var dstPtr = (T*)_chunks[chunkIdx] + offset; var copyCount = Math.Min(remaining, _chunkCapacity - offset); MemoryUtility.MemCpy(dstPtr, src, (nuint)(copyCount * sizeof(T))); src += copyCount; currentIndex += copyCount; remaining -= copyCount; } } /// /// Removes a range of elements from the list starting at the specified index. /// public void RemoveRange(int start, int length) { CheckIndexCount(start, length); if (length <= 0) { return; } var copyFrom = Math.Min(start + length, _count); var numToMove = _count - copyFrom; for (var i = 0; i < numToMove; i++) { var (srcChunk, srcOffset) = SplitIndex(copyFrom + i, _chunkCapacity); var (dstChunk, dstOffset) = SplitIndex(start + i, _chunkCapacity); ((T*)_chunks[dstChunk])[dstOffset] = ((T*)_chunks[srcChunk])[srcOffset]; } _count -= length; FreeTrailingEmptyChunks(); } /// /// Removes the element at the specified index. /// public void RemoveAt(int index) { RemoveRange(index, 1); } /// /// Removes a range of elements by swapping them with elements from the end of the list. /// public void RemoveRangeSwapBack(int start, int length) { CheckIndexCount(start, length); if (length <= 0) { return; } var numToCopy = Math.Min(length, _count - (start + length)); var copyFrom = _count - numToCopy; for (var i = 0; i < numToCopy; i++) { var (dstChunk, dstOffset) = SplitIndex(start + i, _chunkCapacity); var (srcChunk, srcOffset) = SplitIndex(copyFrom + i, _chunkCapacity); ((T*)_chunks[dstChunk])[dstOffset] = ((T*)_chunks[srcChunk])[srcOffset]; } _count -= length; FreeTrailingEmptyChunks(); } /// /// Removes the element at the specified index by swapping it with the last element. /// public void RemoveAtSwapBack(int index) { RemoveRangeSwapBack(index, 1); } public void Resize(int newSize, AllocationOption option = AllocationOption.None) { if (newSize < 0) { throw new ArgumentOutOfRangeException(nameof(newSize)); } if (newSize > _count) { EnsureChunkIndex(newSize - 1); } _count = newSize; FreeTrailingEmptyChunks(); } /// /// Pre-allocates chunks to accommodate at least the specified number of elements. /// public void EnsureCapacity(int capacity) { if (capacity > 0) { EnsureChunkIndex(capacity - 1); } } public void Clear() { _count = 0; FreeTrailingEmptyChunks(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void* GetUnsafePtr() { ThrowIfNotCreated(); if (_chunkCount == 1) { return (void*)_chunks[0]; } throw new InvalidOperationException("Cannot get a single contiguous pointer for a multi-chunk UnsafeChunkedList. Use CopyTo instead."); } /// /// Copies all elements into a destination span. /// public readonly void CopyTo(Span destination) { var size = Math.Min(destination.Length, Count); var remaining = size; var elementIndex = 0; fixed (T* pDest = destination) { var dst = pDest; while (remaining > 0) { var (chunkIdx, offset) = SplitIndex(elementIndex, _chunkCapacity); var srcPtr = (T*)_chunks[chunkIdx] + offset; var copyCount = Math.Min(remaining, _chunkCapacity - offset); MemoryUtility.MemCpy(dst, srcPtr, (nuint)(copyCount * sizeof(T))); elementIndex += copyCount; dst += copyCount; remaining -= copyCount; } } } /// /// Copies a range of elements from the list to a destination span. /// public readonly void CopyTo(Span destination, int sourceIndex, int destinationIndex, int length) { if (sourceIndex + length > _count || destinationIndex + length > destination.Length) { throw new ArgumentOutOfRangeException(nameof(length), "Source collection or destination span is too small for the specified range."); } fixed (T* pDest = destination) { var dst = pDest + destinationIndex; var remaining = length; var elementIndex = sourceIndex; while (remaining > 0) { var (chunkIdx, offset) = SplitIndex(elementIndex, _chunkCapacity); var srcPtr = (T*)_chunks[chunkIdx] + offset; var copyCount = Math.Min(remaining, _chunkCapacity - offset); MemoryUtility.MemCpy(dst, srcPtr, (nuint)(copyCount * sizeof(T))); elementIndex += copyCount; dst += copyCount; remaining -= copyCount; } } } /// /// Copies elements from a source span into the list, growing as needed. /// public void CopyFrom(ReadOnlySpan source) { if (_count < source.Length) { Resize(source.Length); } CopyFromSpan(source, 0); } /// /// Copies a range of elements from a source span to the list. /// public void CopyFrom(ReadOnlySpan source, int sourceIndex, int destinationIndex, int length) { if (sourceIndex + length > source.Length) { throw new ArgumentOutOfRangeException(nameof(length), "Source span or destination collection is too small for the specified range."); } if (destinationIndex + length > _count) { Resize(destinationIndex + length); } fixed (T* pSrc = source) { CopyFromPtr(pSrc + sourceIndex, destinationIndex, length); } } /// /// Creates a new containing the elements. /// public readonly List ToList() { var list = new List(_count); var remaining = _count; var elementIndex = 0; while (remaining > 0) { var (chunkIdx, offset) = SplitIndex(elementIndex, _chunkCapacity); var chunkSize = Math.Min(remaining, _chunkCapacity - offset); var srcPtr = (T*)_chunks[chunkIdx] + offset; var span = new ReadOnlySpan(srcPtr, chunkSize); list.AddRange(span); elementIndex += chunkSize; remaining -= chunkSize; } return list; } public void Dispose() { for (var i = 0; i < _chunkCount; i++) { FreeChunk(i); } _chunks.Dispose(); _chunkCount = 0; _count = 0; } }