using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections.Contracts; using Misaki.HighPerformance.LowLevel.Utilities; using System.Collections; using System.Diagnostics; using System.Runtime.CompilerServices; namespace Misaki.HighPerformance.LowLevel.Collections; internal class ConcurrentSparseSetDebugView where T : unmanaged { private readonly UnsafeSparseSet _set; public ConcurrentSparseSetDebugView(UnsafeSparseSet set) { _set = set; } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public T[] Items { get { var items = new T[_set.Count]; var index = 0; foreach (var item in _set) { items[index++] = item; } return items; } } } /// /// A sparse set data structure that provides O(1) insertion, deletion, and lookup operations. /// The sparse set uses three arrays: a dense array for storing values, a sparse array for mapping indices, /// and a reverse array for mapping dense indices back to sparse indices. /// Sparse indices work like entity IDs and are automatically generated. /// /// Represents a type that can be stored in the sparse set, constrained to unmanaged types for performance and safety. [DebuggerTypeProxy(typeof(ConcurrentSparseSetDebugView<>))] public unsafe struct UnsafeSparseSet : IUnsafeCollection where T : unmanaged { public struct Enumerator : IEnumerator { private readonly UnsafeSparseSet* _collection; private int _currentIndex; public readonly ref T Current => ref _collection->_dense[_currentIndex]; readonly T IEnumerator.Current => Current; readonly object IEnumerator.Current => Current; public Enumerator(UnsafeSparseSet* collection) { _collection = collection; _currentIndex = 0; } public bool MoveNext() { _currentIndex++; return _currentIndex < _collection->_count; } public void Reset() { _currentIndex = 0; } public readonly void Dispose() { } } private UnsafeArray _dense; private UnsafeArray _generations; private UnsafeArray _sparse; private UnsafeArray _reverse; // Maps dense index to sparse index. Since this is a general purpose sparse set, we have to include reverse array. In real world ecs, this should be replaced with entity id array. private UnsafeStack _freeSparse; private int _count; private int _nextId; // Next available sparse index private int _capacity; public readonly int Count => _count; public readonly int Capacity => _capacity; public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated; public Enumerator GetEnumerator() => new((UnsafeSparseSet*)UnsafeUtility.AddressOf(ref this)); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Constructs an UnsafeSparseSet with a default size of 1 and uses the Persistent allocator. /// public UnsafeSparseSet() : this(1, Allocator.Persistent) { } /// /// Initializes a new instance of UnsafeSparseSet with a specified capacity and an allocation handle. /// /// Specifies the initial capacity of the sparse set, which must be greater than zero. /// A reference to an AllocationHandle that manages the memory allocation for the sparse set. /// Specifies how the memory should be allocated. /// Thrown when the specified capacity is less than or equal to zero. public UnsafeSparseSet(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None) { if (capacity <= 0) { throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero."); } _dense = new UnsafeArray(capacity, handle, allocationOption); _generations = new UnsafeArray(capacity, handle, allocationOption); _sparse = new UnsafeArray(capacity, handle, allocationOption); _reverse = new UnsafeArray(capacity, handle, allocationOption); _freeSparse = new UnsafeStack(capacity, handle, allocationOption); if (!allocationOption.HasFlag(AllocationOption.Clear)) { _generations.AsSpan().Clear(); _sparse.AsSpan().Clear(); } _count = 0; _nextId = 0; _capacity = capacity; _sparse.AsSpan().Fill(-1); _generations.Clear(); } /// /// Initializes a new instance of UnsafeSparseSet with a specified capacity and an allocation type. /// /// Specifies the initial capacity of the sparse set, which must be greater than zero. /// Specifies the allocator to use for memory allocation, which determines the memory management strategy. /// Determines how the memory should be allocated. /// Thrown when the specified capacity is less than or equal to zero. public UnsafeSparseSet(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None) : this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption) { } [MethodImpl(MethodImplOptions.AggressiveInlining)] private readonly ref T GetDenseReferenceUnchecked(int sparseIndex) { return ref _dense[_sparse[sparseIndex]]; } /// /// Adds a value to the sparse set and returns a unique sparse index for the value. /// /// The value to add to the sparse set. /// Outputs the generation number associated with the added value. /// A unique sparse index that can be used to reference this value. public int Add(T value, out int generation) { if (!_freeSparse.TryPop(out var sparseIndex)) { // Use the next available ID sparseIndex = _nextId++; // Resize sparse array if necessary if (sparseIndex >= _sparse.Count) { ResizeSparse(sparseIndex + 1); } } // Resize dense arrays if necessary if (_count >= _capacity) { Resize(Math.Max(1, _capacity * 2)); } // Add the value to the dense array and update mappings _dense[_count] = value; _sparse[sparseIndex] = _count; _reverse[_count] = sparseIndex; _count++; generation = _generations[sparseIndex]; return sparseIndex; } /// /// Removes the value at the specified sparse index. /// /// The sparse index of the value to remove. /// The generation number associated with the sparse index to validate. /// True if the value was removed, false if the sparse index was not found. public bool Remove(int sparseIndex, int generation) { if (!Contains(sparseIndex, generation)) { return false; } var denseIndex = _sparse[sparseIndex]; var lastIndex = _count - 1; if (denseIndex != lastIndex) { // Move the last element to the position of the removed element var lastValue = _dense[lastIndex]; var lastSparseIndex = _reverse[lastIndex]; // Get sparse index of last element _dense[denseIndex] = lastValue; _reverse[denseIndex] = lastSparseIndex; // Update the sparse mapping for the moved element _sparse[lastSparseIndex] = denseIndex; } // Mark the sparse index as unused and add to free list _sparse[sparseIndex] = -1; _generations[sparseIndex]++; // Increment generation to invalidate old references _freeSparse.Push(sparseIndex); _count--; return true; } /// /// Checks if the sparse set contains a value at the specified sparse index. /// /// The sparse index to check. /// The generation number to validate against the stored generation. /// True if the sparse index is valid and contains a value, false otherwise. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool Contains(int sparseIndex, int generation) { if (sparseIndex < 0 || sparseIndex >= _sparse.Count) { return false; } var denseIndex = _sparse[sparseIndex]; return denseIndex >= 0 && denseIndex < _count && _generations[denseIndex] == generation; } /// /// Gets the value at the specified sparse index and generation. /// /// The sparse index to retrieve the value from. /// The generation number to validate against the stored generation. /// When this method returns, contains the value at the specified sparse index, if found. /// True if the sparse index contains a value, false otherwise. public readonly bool TryGetValue(int sparseIndex, int generation, out T value) { if (Contains(sparseIndex, generation)) { value = GetDenseReferenceUnchecked(sparseIndex); return true; } value = default; return false; } /// /// Gets the value at the specified sparse index and generation. /// /// The sparse index to retrieve the value from. /// The generation number to validate against the stored generation. /// The value at the specified sparse index. /// Thrown when the sparse index is not found. public readonly T GetValue(int sparseIndex, int generation) { if (!Contains(sparseIndex, generation)) { throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index and feneration not found in the set."); } return GetDenseReferenceUnchecked(sparseIndex); } /// /// Gets reference of the value at the specified sparse index and generation. /// /// The sparse index to retrieve the value from. /// The generation number to validate against the stored generation. /// Outputs whether the sparse index exists in the set. /// Reference of the value at the specified sparse index. /// Thrown when the sparse index is not found. public readonly ref T GetValueReference(int sparseIndex, int generation, out bool exist) { if (!Contains(sparseIndex, generation)) { exist = false; return ref Unsafe.NullRef(); } exist = true; return ref GetDenseReferenceUnchecked(sparseIndex); } /// /// Updates the value at the specified sparse index. /// /// The sparse index of the value to update. /// The generation number to validate against the stored generation. /// The new value. /// True if the value was updated, false if the sparse index was not found. public bool SetValue(int sparseIndex, int generation, T value) { if (!Contains(sparseIndex, generation)) { return false; } GetDenseReferenceUnchecked(sparseIndex) = value; return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ResizeSparse(int newSize) { var oldSize = _sparse.Count; _sparse.Resize(newSize); _sparse.AsSpan()[oldSize..newSize].Fill(-1); } /// public void Clear() { if (!IsCreated) { return; } _sparse.AsSpan().Fill(-1); _generations.AsSpan().Clear(); _count = 0; _nextId = 0; Add(default, out _); } /// public void Resize(int newSize, AllocationOption option = AllocationOption.None) { if (newSize <= 0) { throw new ArgumentOutOfRangeException(nameof(newSize), "New size must be greater than zero."); } _dense.Resize(newSize, option); _generations.Resize(newSize, option | AllocationOption.Clear); _reverse.Resize(newSize, option); if (newSize > _sparse.Count) { ResizeSparse(newSize); } _capacity = newSize; } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly void* GetUnsafePtr() { return (T*)_dense.GetUnsafePtr() + 1; } /// /// Converts the current sparse set to an UnsafeArray representation using its dense array. /// /// Returns a new UnsafeArray instance initialized with the dense array's pointer and count. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly UnsafeArray AsUnsafeArray() { return new UnsafeArray((T*)_dense.GetUnsafePtr(), _count); } /// public void Dispose() { _dense.Dispose(); _generations.Dispose(); _sparse.Dispose(); _reverse.Dispose(); _freeSparse.Dispose(); _count = 0; _nextId = 0; } }