using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Misaki.HighPerformance.LowLevel.Collections; internal class UnsafeSlotMapDebugView where T : unmanaged { private readonly UnsafeSlotMap _slotMap; public UnsafeSlotMapDebugView(UnsafeSlotMap slotMap) { _slotMap = slotMap; } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public T[] Items { get { var items = new List(_slotMap.Count); var enumerator = _slotMap.GetEnumerator(); while (enumerator.MoveNext()) { items.Add(enumerator.Current); } return items.ToArray(); } } } /// /// Provides an unsafe, high-performance slot map for storing and managing unmanaged values, supporting fast insertion, /// removal, and lookup by slot index and Generation. /// /// The type of value to store in the slot map. Must be unmanaged. [DebuggerTypeProxy(typeof(UnsafeSlotMapDebugView<>))] public unsafe struct UnsafeSlotMap : IUnsafeCollection where T : unmanaged { private struct SlotEntry { public T value; public int generation; } private const int _CHUNK_SHIFT = 8; private const int _CHUNK_SIZE = 1 << _CHUNK_SHIFT; private const int _CHUNK_MASK = _CHUNK_SIZE - 1; public ref struct Enumerator { private ref UnsafeSlotMap _collection; private int _currentIndex; public Enumerator(ref UnsafeSlotMap collection) { _collection = ref collection; _currentIndex = -1; } public readonly ref T Current { get { var chunks = _collection._chunks; var chunkIdx = _currentIndex >> _CHUNK_SHIFT; var localIdx = _currentIndex & _CHUNK_MASK; return ref chunks[chunkIdx][localIdx].value; } } public bool MoveNext() { _currentIndex = _collection._validBits.NextSetBit(_currentIndex + 1); return _currentIndex != -1; } public void Reset() { _currentIndex = -1; } } private UnsafeArray> _chunks; private UnsafeQueue _freeSlots; private UnsafeBitSet _validBits; private AllocationHandle _handle; private AllocationOption _allocationOption; private int _count; private int _capacity; private int _nextSlotIndex; public readonly int Count => _count; public readonly int Capacity => _capacity; public readonly bool IsCreated => _chunks.IsCreated && _freeSlots.IsCreated && _validBits.IsCreated; /// /// Initializes a new instance of UnsafeSlotMap with a default size of 1 and a persistent allocation handle. /// public UnsafeSlotMap() : this(1, AllocationHandle.Persistent) { } /// /// Initializes a new instance of the UnsafeSlotMap class with the specified capacity, allocation handle, and /// allocation options. /// /// The number of slots to allocate for the map. Must be greater than zero. /// A reference to the allocation handle used to manage memory for the slot map. /// The allocation options to use when creating internal data structures. The default is AllocationOption.None. /// Thrown when capacity is less than or equal to zero. public UnsafeSlotMap(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None) { if (capacity <= 0) { throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero."); } _handle = handle; _allocationOption = allocationOption; var initialChunks = (capacity + _CHUNK_MASK) / _CHUNK_SIZE; if (initialChunks == 0) initialChunks = 1; _capacity = initialChunks * _CHUNK_SIZE; _chunks = new UnsafeArray>(initialChunks, handle, allocationOption); for (var i = 0; i < initialChunks; i++) { _chunks[i] = new UnsafeArray(_CHUNK_SIZE, handle, allocationOption); if (!allocationOption.HasFlag(AllocationOption.Clear)) { _chunks[i].AsSpan().Clear(); } } _freeSlots = new UnsafeQueue(capacity, handle, allocationOption); _validBits = new UnsafeBitSet(_capacity, handle, allocationOption); if (!allocationOption.HasFlag(AllocationOption.Clear)) { _validBits.ClearAll(); } _count = 0; _nextSlotIndex = 0; } /// /// Initializes a new instance of the UnsafeSlotMap class with the specified capacity, allocator, and allocation /// options. /// /// The initial number of slots to allocate for the map. Must be greater than zero. /// The allocator to use for memory management of the slot map. /// The allocation option that determines how memory is allocated. The default is AllocationOption.None. [Obsolete("Use AllocationHandle instead.")] public UnsafeSlotMap(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None) : this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] [UnscopedRef] public Enumerator GetEnumerator() { return new Enumerator(ref this); } [MethodImpl(MethodImplOptions.NoInlining)] private void EnsureChunkExists(int requiredChunkIndex) { if (requiredChunkIndex < _chunks.Length) return; var newChunkCount = _chunks.Length; while (newChunkCount <= requiredChunkIndex) { newChunkCount *= 2; } _chunks.Resize(newChunkCount, _allocationOption); for (var i = _capacity / _CHUNK_SIZE; i < newChunkCount; i++) { _chunks[i] = new UnsafeArray(_CHUNK_SIZE, _handle, _allocationOption); if (!_allocationOption.HasFlag(AllocationOption.Clear)) { _chunks[i].AsSpan().Clear(); } } _capacity = newChunkCount * _CHUNK_SIZE; _validBits.Resize(_capacity, _allocationOption); } /// /// Adds the specified item to the collection and returns the index of the slot where it was stored. /// /// The item to add to the collection. /// When this method returns, contains the Generation number associated with the slot where the item was stored. /// The index of the slot in which the item was stored. public int Add(scoped in T item, out int generation) { if (_freeSlots.Count > 0) { var slotIndex = _freeSlots.Dequeue(); var chunkIdx = slotIndex >> _CHUNK_SHIFT; var localIdx = slotIndex & _CHUNK_MASK; ref var slot = ref _chunks[chunkIdx][localIdx]; generation = slot.generation; slot.value = item; _validBits.SetBit(slotIndex); _count++; return slotIndex; } var newSlotIndex = _nextSlotIndex++; var newChunkIdx = newSlotIndex >> _CHUNK_SHIFT; var newLocalIdx = newSlotIndex & _CHUNK_MASK; if (newChunkIdx >= _chunks.Length) { EnsureChunkExists(newChunkIdx); } ref var newSlot = ref _chunks[newChunkIdx][newLocalIdx]; newSlot.value = item; newSlot.generation = 0; _validBits.SetBit(newSlotIndex); generation = 0; _count++; return newSlotIndex; } /// /// Attempts to remove the item at the specified slot index and Generation from the collection. /// /// The zero-based index of the slot to remove. Must be within the valid range of slot indices. /// The Generation value associated with the slot. Removal succeeds only if this matches the current Generation of the slot. /// When this method returns, contains the item that was removed if the removal was successful; otherwise, the default value for type . /// true if the item was successfully removed; otherwise, false. public bool Remove(int slotIndex, int generation, out T item) { item = default; if (slotIndex < 0) { return false; } var chunkIdx = slotIndex >> _CHUNK_SHIFT; var localIdx = slotIndex & _CHUNK_MASK; if (chunkIdx >= _chunks.Length) { return false; } ref var slot = ref _chunks[chunkIdx][localIdx]; if (!_validBits.IsSet(slotIndex) || slot.generation != generation) { return false; } slot.generation++; _validBits.ClearBit(slotIndex); item = slot.value; slot.value = default; _freeSlots.Enqueue(slotIndex); _count--; return true; } /// /// Attempts to remove the item at the specified slot index and Generation from the collection. /// /// The zero-based index of the slot to remove. Must be within the valid range of slot indices. /// The Generation value associated with the slot. Removal succeeds only if this matches the current Generation of /// the slot. /// true if the item was successfully removed; otherwise, false. public bool Remove(int slotIndex, int generation) { return Remove(slotIndex, generation, out _); } /// /// Determines whether the specified slot index contains a valid entry with the given Generation. /// /// The zero-based index of the slot to check. Must be greater than or equal to 0 and less than the current capacity. /// The Generation value to compare against the slot's Generation. /// true if the slot at the specified index is valid and its Generation matches the specified value; otherwise, false. public readonly bool Contains(int slotIndex, int generation) { GetElementReferenceAt(slotIndex, generation, out var exist); return exist; } /// /// Attempts to retrieve the element at the specified slot index and Generation. /// /// The zero-based index of the slot to retrieve. Must be within the valid range of slots. /// The Generation identifier associated with the slot. Used to verify that the slot has not been replaced or /// invalidated. /// When this method returns, contains the element at the specified slot and Generation if found; otherwise, the /// default value for type . /// true if the element at the specified slot index and Generation is found; otherwise, false. public readonly bool TryGetElementAt(int slotIndex, int generation, out T value) { ref var val = ref GetElementReferenceAt(slotIndex, generation, out var exist); if (exist) { value = val; return true; } else { value = default; return false; } } /// /// Retrieves the element stored at the specified slot index and Generation. /// /// The zero-based index of the slot from which to retrieve the element. Must be within the valid range of allocated slots. /// The Generation identifier associated with the slot. Used to ensure the element has not been replaced or removed since allocation. /// The element of type stored at the specified slot and Generation. /// Thrown when is less than zero or greater than or equal to the capacity. /// Thrown when the specified slot is not occupied or the Generation does not match. public readonly T GetElementAt(int slotIndex, int generation) { if (!TryGetElementAt(slotIndex, generation, out var value)) { throw new InvalidOperationException("The specified slot is not occupied or the generation does not match."); } return value; } /// /// Returns a reference to the element at the specified slot index and Generation, if it exists; otherwise, returns /// a null reference. /// /// The zero-based index of the slot to retrieve. Must be within the valid range of allocated slots. /// The expected Generation value for the slot. Used to verify that the slot has not been recycled or replaced. /// When this method returns, contains if a valid element exists at the specified slot and Generation; otherwise, . /// A reference to the element of type at the specified slot and Generation if it exists; otherwise, a null reference. public readonly ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist) { if (slotIndex < 0) { exist = false; return ref Unsafe.NullRef(); } var chunkIdx = slotIndex >> _CHUNK_SHIFT; var localIdx = slotIndex & _CHUNK_MASK; if (chunkIdx >= _chunks.Length) { exist = false; return ref Unsafe.NullRef(); } ref var slot = ref _chunks[chunkIdx][localIdx]; if (_validBits.IsSet(slotIndex) && slot.generation == generation) { exist = true; return ref slot.value; } exist = false; return ref Unsafe.NullRef(); } public void Resize(int newSize, AllocationOption option = AllocationOption.None) { var requiredChunkIndex = (newSize + _CHUNK_MASK) / _CHUNK_SIZE - 1; EnsureChunkExists(requiredChunkIndex); _freeSlots.Resize(newSize, option); _validBits.Resize(newSize, option); } public void Clear() { for (var i = 0; i < _chunks.Length; i++) { if (_chunks[i].IsCreated) { var chunk = _chunks[i]; for (var slot = 0; slot < _CHUNK_SIZE; slot++) { chunk[slot].generation = 0; chunk[slot].value = default; } } } _freeSlots.Clear(); _validBits.ClearAll(); _count = 0; _nextSlotIndex = 0; } public readonly void* GetUnsafePtr() { return null; } public void Dispose() { for (var i = 0; i < _chunks.Length; i++) { if (_chunks[i].IsCreated) { _chunks[i].Dispose(); } } _chunks.Dispose(); _freeSlots.Dispose(); _validBits.Dispose(); _count = 0; _capacity = 0; _nextSlotIndex = 0; } }