Enhance mathematical capabilities and job system

Added new numeric types for unsigned integers, including uint2, uint3, and uint4, along with their matrix types.
Added a new `quaternion` struct with constructors and methods for creating and manipulating quaternions.
Added methods for projecting and reflecting vectors, enhancing geometric operations.
Added utility functions for generating orthonormal bases and changing vector signs.
Added comprehensive unit tests for new mathematical functions and quaternion operations.
Added a high-performance job scheduling system with job management features and worker thread management.
Added new structs for job execution, allowing efficient job scheduling and execution.
Added utility functions for job execution, including methods for obtaining unique job IDs.

Changed access modifiers and property definitions in several files for improved clarity and maintainability.
Changed property definitions and method implementations in `ImageInfo.cs`, `ImageResult.cs`, and `ImageResultFloat.cs` for better readability.
Changed memory management functions in `CRuntime.cs` and improved memory allocation tracking in `MemoryStats.cs`.
Changed the project file to include references to necessary projects and enable unsafe code blocks.

Removed the `WorkerThreadPool.cs` file, integrating worker thread management directly into the `JobScheduler`.
Removed the `float4` struct and its associated methods and properties, transitioning to a new code generation strategy.
Removed the `float4.tt` template and other related files, indicating a shift in code generation approach.
Removed the `Vectorize.cs` file, indicating a change in how vector operations are handled.

Updated the `.gitignore` file to include IDE-specific settings.
Updated various XML files to define project components and structure.
Updated the `AllocationManager.cs` to improve memory allocation management and introduce new strategies.
Updated the `UnsafeArray.cs`, `UnsafeHashMap.cs`, and `UnsafeList.cs` to enhance performance and safety in unsafe contexts.
Updated error handling and function pointer management in `MemoryLeakException.cs` and `FunctionPointer.cs`.
Updated the `AssemblyInfo.cs` file to include global using directives for better code organization.
This commit is contained in:
2025-09-06 12:07:02 +09:00
parent eeff3313b5
commit a2a760594e
114 changed files with 20826 additions and 7217 deletions

View File

@@ -1,38 +0,0 @@
namespace Misaki.HighPerformance.LowLevel.Collections;
[Flags]
public enum AllocationOption : byte
{
None = 0,
/// <summary>
/// Allocator for initialized memory.
/// </summary>
Clear = 1 << 0,
/// <summary>
/// Allocator for untracked memory. It always allocates memory without using the allocation manager.
/// Always free it manually even if you use the <see cref="Allocator.Temp"/> allocator.
/// </summary>
/// <remarks>
/// Use this option carefully, as the allocation manager will not track the memory.
/// No warning will be given if the memory is not freed.
/// </remarks>
UnTracked = 1 << 1,
}
public enum Allocator : byte
{
// Make the first allocator as invalid because we don't want to user create a default collection without passing any parameters
Invalid,
/// <summary>
/// Allocator for temporary allocations. Allocations are cleared after use.
/// </summary>
Temp,
/// <summary>
/// Allocator for persistent allocations. Allocations are not cleared after use.
/// </summary>
Persistent,
/// <summary>
/// Allocator for external memory. Allocations are not cleared after use.
/// </summary>
External
}

View File

@@ -1,16 +1,7 @@
namespace Misaki.HighPerformance.LowLevel.Collections.Contracts;
public unsafe interface IUnsafeCollection<T> : IEnumerable<T>, IDisposable
where T : unmanaged
public unsafe interface IUnsafeCollection : IDisposable
{
/// <summary>
/// Gets the number of elements in a collection. The value is read-only.
/// </summary>
public int Count
{
get;
}
/// <summary>
/// Indicates whether the object has been created. Returns true if the object is created, otherwise false.
/// </summary>
@@ -24,15 +15,41 @@ public unsafe interface IUnsafeCollection<T> : IEnumerable<T>, IDisposable
/// </summary>
public void Clear();
/// <summary>
/// Changes the size of a collection or array to the specified value.
/// </summary>
/// <param name="newSize">Specifies the new size to which the collection or array should be adjusted.</param>
public void Resize(int newSize);
/// <summary>
/// Returns a pointer to an unmanaged memory location. This pointer can be used for low-level memory operations.
/// </summary>
/// <returns>The method returns a void pointer to the unsafe memory location.</returns>
public void* GetUnsafePtr();
}
public unsafe interface IUnsafeCollection<T> : IUnsafeCollection, IEnumerable<T>
where T : unmanaged
{
/// <summary>
/// Gets the number of elements in a collection. The value is read-only.
/// </summary>
public int Count
{
get;
}
/// <summary>
/// Changes the size of a collection to the specified value.
/// </summary>
/// <param name="newSize">Specifies the new size to which the collection should be adjusted.</param>
public void Resize(int newSize);
}
public unsafe interface IUnTypedCollection : IUnsafeCollection
{
/// <summary>
/// The total size of the buffer in bytes.
/// </summary>
public uint Size
{
get;
}
public ref T GetElementAt<T>(uint index)
where T : unmanaged;
}

View File

@@ -0,0 +1,152 @@
using System.Collections;
namespace Misaki.HighPerformance.LowLevel.Collections;
public class SlotMap<T> : IEnumerable<T>
{
public struct Enumerator : IEnumerator<T>
{
private readonly SlotMap<T> _slotMap;
private int _currentIndex;
public Enumerator(SlotMap<T> slotMap)
{
_slotMap = slotMap;
_currentIndex = -1;
}
public readonly T Current => _slotMap._data[_currentIndex].value;
readonly object? IEnumerator.Current => Current;
public bool MoveNext()
{
while (++_currentIndex < _slotMap._capacity)
{
if (_slotMap._data[_currentIndex].isValid)
{
return true;
}
}
return false;
}
public void Reset() => _currentIndex = -1;
public void Dispose()
{
}
}
private struct SlotData
{
public T value;
public bool isValid;
}
private SlotData[] _data;
private readonly Queue<int> _freeSlots;
private int _count;
private int _capacity;
public int Count => _count;
public int Capacity => _capacity;
public ref T this[int slotIndex]
{
get
{
if (slotIndex < 0 || slotIndex >= _capacity)
{
throw new ArgumentOutOfRangeException(nameof(slotIndex), "Slot index is out of range.");
}
ref var slot = ref _data[slotIndex];
if (!slot.isValid)
{
throw new InvalidOperationException($"Slot {slotIndex} is not occupied.");
}
return ref slot.value;
}
}
public IEnumerator<T> GetEnumerator() => new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public SlotMap(int initialCapacity = 16)
{
_capacity = initialCapacity;
_data = new SlotData[initialCapacity];
_freeSlots = new Queue<int>(initialCapacity);
}
private void Resize()
{
var newCapacity = _capacity * 2;
Array.Resize(ref _data, newCapacity);
_freeSlots.EnsureCapacity(newCapacity);
_capacity = newCapacity;
}
public int Add(T item)
{
if (_count >= _capacity)
{
Resize();
}
int slotIndex;
if (_freeSlots.Count == 0)
{
slotIndex = _count;
}
else
{
slotIndex = _freeSlots.Dequeue();
}
ref var slot = ref _data[slotIndex];
slot.value = item;
slot.isValid = true;
_count++;
return slotIndex;
}
public bool Remove(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= _capacity)
{
return false;
}
ref var slot = ref _data[slotIndex];
if (!slot.isValid)
{
return false;
}
slot.isValid = false;
_freeSlots.Enqueue(slotIndex);
_count--;
return true;
}
public void Clear()
{
_count = 0;
_data.AsSpan().Clear();
_freeSlots.Clear();
}
}

View File

@@ -0,0 +1,125 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
public unsafe struct UnTypedArray : IUnTypedCollection
{
private void* _buffer;
private uint _size;
private uint _alignment;
private AllocationHandle* _handle;
public readonly uint Size => _size;
public readonly uint Alignment => _alignment;
public readonly bool IsCreated
{
get => _buffer != null;
}
/// <summary>
/// Constructs an UnsafeArray with a default size of 1 and uses the Persistent allocator.
/// </summary>
public UnTypedArray()
: this(1, 1, Allocator.Persistent)
{
}
public UnTypedArray(uint size, uint alignment, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{
if (size <= 0)
{
throw new ArgumentOutOfRangeException(nameof(size), "Count must be greater than zero.");
}
_handle = (AllocationHandle*)Unsafe.AsPointer(ref handle);
_buffer = handle.Alloc(_handle->Allocator, size, alignment, allocationOption);
_size = size;
_alignment = alignment;
}
/// <summary>
/// Initializes a new instance of UnsafeArray with a specified number of elements and an allocation type.
/// </summary>
/// <param name="count">Specifies the number of elements to allocate in the array, which must be greater than zero.</param>
/// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param>
/// <param name="allocationOption">Determines how the memory should be allocated.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception>
public UnTypedArray(uint size, uint alignment, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
: this(size, alignment, ref AllocationManager.GetAllocationHandle(allocator), allocationOption)
{
}
/// <summary>
/// Initializes an UnsafeArray with a pointer to a buffer and a count of elements. This does not copy the data.
/// </summary>
/// <param name="buffer">A pointer to the memory location that holds the elements of the array.</param>
/// <param name="count">The total size of the data.</param>
/// <remarks>
/// When using this constructor, the user is responsible for managing the memory pointed to by the buffer.
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
/// </remarks>
public UnTypedArray(void* buffer, uint size)
{
_buffer = buffer;
_size = size;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ref T GetElementAt<T>(uint index)
where T : unmanaged
{
return ref UnsafeUtilities.ReadArrayElementRef<T>(_buffer, index);
}
/// <inheritdoc/>
public void Resize(uint newSize)
{
if (newSize == _size)
{
return;
}
_buffer = _handle->Realloc(_handle->Allocator, _buffer, newSize, _alignment);
_size = newSize;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Clear()
{
MemClear(_buffer, _size);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _buffer;
}
/// <inheritdoc/>
public void Dispose()
{
if (!IsCreated)
{
return;
}
if (_handle != null)
{
_handle->Free(_handle->Allocator, _buffer);
}
_handle = null;
_buffer = null;
_size = 0;
_alignment = 0;
}
}

View File

@@ -81,7 +81,21 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
}
return ref _buffer[index];
return ref UnsafeUtilities.ReadArrayElementRef<T>(_buffer, index);
}
}
public readonly ref T this[uint index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
if (index >= _count)
{
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
}
return ref UnsafeUtilities.ReadArrayElementRef<T>(_buffer, index);
}
}
@@ -98,10 +112,17 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
/// Constructs an UnsafeArray with a default size of 1 and uses the Persistent allocator.
/// </summary>
public UnsafeArray()
: this(1, Allocator.Persistent)
: this(0, Allocator.Invalid)
{
}
/// <summary>
/// Initializes a new instance of UnsafeArray with a specified number of elements and an allocation handle.
/// </summary>
/// <param name="count">Specifies the number of elements to allocate in the array, which must be greater than zero.</param>
/// <param name="handle">A reference to an AllocationHandle that manages the memory allocation for the array.</param>
/// <param name="allocationOption">Specifies how the memory should be allocated.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception>
public UnsafeArray(int count, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{
if (count <= 0)
@@ -136,11 +157,10 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
/// </remarks>
public UnsafeArray(void* buffer, int count)
public UnsafeArray(T* buffer, int count)
{
_buffer = (T*)buffer;
_buffer = buffer;
_count = count;
_handle = (AllocationHandle*)Unsafe.AsPointer(ref AllocationManager.EmptyAllocator.Handle);
}
/// <inheritdoc/>
@@ -177,7 +197,10 @@ public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
return;
}
_handle->Free(_handle->Allocator, _buffer);
if (_handle != null)
{
_handle->Free(_handle->Allocator, _buffer);
}
_handle = null;
_buffer = null;

View File

@@ -215,9 +215,4 @@ public unsafe struct UnsafeHashMap<TKey, TValue> : IUnsafeCollection<KeyValuePai
{
_hashMap.Dispose();
}
public void Test(ref HashMapHelper<TKey> t)
{
Console.WriteLine(t.Equals(_hashMap));
}
}

View File

@@ -1,4 +1,6 @@
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;
@@ -81,7 +83,7 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
/// </summary>
public UnsafeList<T>* listData;
internal unsafe ParallelWriter(UnsafeList<T>* list)
internal ParallelWriter(UnsafeList<T>* list)
{
listData = list;
}
@@ -102,11 +104,15 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
/// </summary>
/// <param name="ptr">Points to the source data to be copied into the buffer.</param>
/// <param name="count">Indicates the number of elements to be added from the source data.</param>
public void AddRangeNoResize(T* ptr, int count)
public void AddRangeNoResize(ReadOnlySpan<T> collection, int count)
{
var idx = Interlocked.Add(ref listData->_count, count) - count;
listData->CheckNoResizeCapacity(idx, count);
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(listData->_array.GetUnsafePtr(), idx), ptr, (uint)(count * sizeof(T)));
var index = Interlocked.Add(ref listData->_count, count) - count;
listData->CheckNoResizeCapacity(index, count);
fixed (T* pCollection = collection)
{
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(listData->_array.GetUnsafePtr(), index), pCollection, (uint)(count * sizeof(T)));
}
}
}
@@ -124,18 +130,47 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
get => ref _array[index];
}
public readonly ref T this[uint index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _array[index];
}
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeList<T>*)UnsafeUtilities.AddressOf(ref this));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Provides a parallel writer for the current list, enabling thread-safe additions to the list.
/// </summary>
/// <returns>A <see cref="ParallelWriter"/> instance that can be used to add items to the list in a thread-safe manner.</returns>
public ParallelWriter AsParallelWriter() => new((UnsafeList<T>*)UnsafeUtilities.AddressOf(ref this));
public UnsafeList() : this(1, Allocator.Persistent)
/// <summary>
/// Converts the current list to an UnsafeArray representation.
/// </summary>
/// <returns>A new <see cref="UnsafeArray{T}"/> instance.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly UnsafeArray<T> AsUnsafeArray() => new((T*)_array.GetUnsafePtr(), _count);
public UnsafeList()
: this(0, Allocator.Invalid)
{
}
public UnsafeList(int capacity, ref AllocationHandle handle, AllocationOption allocationType = AllocationOption.None)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
}
_array = new UnsafeArray<T>(capacity, ref handle, allocationType);
_count = 0;
}
public UnsafeList(int capacity, Allocator allocator, AllocationOption allocationType = AllocationOption.None)
{
_array = new UnsafeArray<T>(capacity, allocator, allocationType);
_count = 0;
}
private readonly void CheckNoResizeCapacity(int count)
@@ -176,6 +211,10 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
}
}
/// <summary>
/// Adds a new element to the end of the list, resizing the internal array if necessary.
/// </summary>
/// <param name="value">The element to be added to the list.</param>
public void Add(T value)
{
if (_count >= Capacity)
@@ -187,6 +226,10 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
_count++;
}
/// <summary>
/// Adds the specified value to the collection without resizing the underlying storage.
/// </summary>
/// <param name="value">The value to add to the collection.</param>
public void AddNoResize(T value)
{
CheckNoResizeCapacity(1);
@@ -195,6 +238,12 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
_count++;
}
/// <summary>
/// Adds a range of elements to the collection.
/// </summary>
/// <param name="values">A span containing the elements to add. The span must not exceed the specified <paramref name="count"/>.</param>
/// <param name="count">The number of elements to add from the <paramref name="values"/> span. Must be non-negative and less than or
/// equal to the length of <paramref name="values"/>.</param>
public void AddRange(Span<T> values, int count)
{
var newSize = _count + count;
@@ -211,18 +260,27 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
_count += count;
}
public void AddRangeNoResize(ReadOnlySpan<T> values)
/// <summary>
/// Adds the elements of the specified collection to the current list without resizing the underlying storage.
/// </summary>
/// <param name="collection">A read-only span containing the elements to add. The span must not exceed the available capacity.</param>
public void AddRangeNoResize(ReadOnlySpan<T> collection)
{
CheckNoResizeCapacity(values.Length);
CheckNoResizeCapacity(collection.Length);
fixed (T* ptr = values)
fixed (T* pCollection = collection)
{
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), _count), ptr, (uint)(values.Length * sizeof(T)));
MemCpy(UnsafeUtilities.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), _count), pCollection, (uint)(collection.Length * sizeof(T)));
}
_count += values.Length;
_count += collection.Length;
}
/// <summary>
/// Adds a range of elements from a pointer to the collection without resizing the underlying storage.
/// </summary>
/// <param name="ptr">Points to the source data to be copied into the collection.</param>
/// <param name="count">Indicates the number of elements to be added from the source data.</param>
public void AddRangeNoResize(T* ptr, int count)
{
CheckNoResizeCapacity(count);
@@ -231,6 +289,11 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
_count += count;
}
/// <summary>
/// Removes a range of elements from the list starting at the specified index.
/// </summary>
/// <param name="start">The zero-based index at which to start removing elements.</param>
/// <param name="length">The number of elements to remove.</param>
public void RemoveRange(int start, int length)
{
CheckIndexCount(start, length);
@@ -248,11 +311,20 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
_count -= length;
}
/// <summary>
/// Removes the element at the specified index from the collection.
/// </summary>
/// <param name="index">The zero-based index of the element to remove.</param>
public void RemoveAt(int index)
{
RemoveRange(index, 1);
}
/// <summary>
/// Removes a range of elements from the list starting at the specified index by swapping them with the last elements.
/// </summary>
/// <param name="start">The zero-based index at which to start removing elements.</param>
/// <param name="length">The number of elements to remove.</param>
public void RemoveRangeSwapBack(int start, int length)
{
CheckIndexCount(start, length);
@@ -270,6 +342,11 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
_count -= length;
}
/// <summary>
/// Removes the element at the specified index by swapping it with the last element and reducing the collection
/// size.
/// </summary>
/// <param name="index">The zero-based index of the element to remove. Must be within the bounds of the collection.</param>
public void RemoveAtSwapBack(int index)
{
RemoveRangeSwapBack(index, 1);
@@ -291,27 +368,12 @@ public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
_count = 0;
}
/// <summary>
/// Returns a pointer to the underlying data of the array in an unsafe manner. This method is optimized for
/// performance.
/// </summary>
/// <returns>A pointer to the array's data.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _array.GetUnsafePtr();
}
/// <summary>
/// Converts the current array to an UnsafeArray representation using its pointer and count.
/// </summary>
/// <returns>Returns a new UnsafeArray instance initialized with the array's unsafe pointer and its count.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly UnsafeArray<T> AsUnsafeArray()
{
return new UnsafeArray<T>(_array.GetUnsafePtr(), _count);
}
public void Dispose()
{
_array.Dispose();

View File

@@ -1,3 +1,4 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
@@ -90,6 +91,21 @@ public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
_array = new UnsafeArray<T>(capacity, allocator, allocationType);
}
/// <summary>
/// Returns a reference to the item at the front of the queue without removing it.
/// </summary>
/// <returns>A reference to the item at the front of the queue.</returns>
/// <exception cref="InvalidOperationException">Thrown if the queue is empty.</exception>
public readonly ref T Peek()
{
if (_count == 0)
{
throw new InvalidOperationException("Queue is empty.");
}
return ref UnsafeUtilities.ReadArrayElementRef<T>(_array.GetUnsafePtr(), _offset);
}
/// <summary>
/// Adds an element to the end of a collection, resizing if the current capacity is reached. The new element is
/// stored in a circular buffer.

View File

@@ -0,0 +1,528 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Collections;
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">Represents a type that can be stored in the sparse set, constrained to unmanaged types for performance and safety.</typeparam>
public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
where T : unmanaged
{
public struct Enumerator : IEnumerator<T>
{
private UnsafeSparseSet<T>* _collection;
private int _index;
private T _value;
public Enumerator(UnsafeSparseSet<T>* collection)
{
_collection = collection;
_index = -1;
_value = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext()
{
_index++;
if (_index < _collection->_count)
{
_value = UnsafeUtilities.ReadArrayElement<T>(_collection->_dense.GetUnsafePtr(), _index);
return true;
}
_value = default;
return false;
}
public void Reset()
{
_index = -1;
}
public readonly T Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _value;
}
readonly object IEnumerator.Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Current;
}
public unsafe readonly void Dispose()
{
}
}
public struct ParallelWriter
{
private UnsafeSparseSet<T>* _sparseSet;
internal ParallelWriter(UnsafeSparseSet<T>* sparseSet)
{
_sparseSet = sparseSet;
}
/// <summary>
/// Adds a value to the sparse set without resizing the internal arrays.
/// </summary>
/// <param name="value">The value to add to the sparse set.</param>
/// <returns> Returns the sparse index assigned to the value. -1 if the sparse index is out of bounds.</returns>
public int AddNoResize(T value)
{
int sparseIndex;
if (_sparseSet->_freeCount > 0)
{
var index = Interlocked.Decrement(ref _sparseSet->_freeCount);
sparseIndex = _sparseSet->_freeList[index];
}
else
{
sparseIndex = Interlocked.Increment(ref _sparseSet->_nextId) - 1;
}
if (sparseIndex >= _sparseSet->_sparse.Count)
{
return -1;
}
var count = Interlocked.Increment(ref _sparseSet->_count) - 1;
_sparseSet->_dense[count] = value;
_sparseSet->_sparse[sparseIndex] = count;
_sparseSet->_reverse[count] = sparseIndex;
return sparseIndex;
}
/// <summary>
/// Attempts to add a value at the specified sparse index without resizing the underlying collection.
/// </summary>
/// <param name="sparseIndex">The index in the sparse array where the value should be added. Must be within the valid range of the sparse
/// array.</param>
/// <param name="value">The value to add to the collection.</param>
/// <returns><see langword="true"/> if the value was successfully added at the specified index; otherwise, <see
/// langword="false"/>.</returns>
public bool AddAtNoResize(int sparseIndex, T value)
{
if (sparseIndex < 0 || sparseIndex >= _sparseSet->_sparse.Count)
{
return false;
}
if (_sparseSet->Contains(sparseIndex))
{
return false;
}
if (_sparseSet->_count >= _sparseSet->_dense.Count)
{
return false;
}
var count = Interlocked.Increment(ref _sparseSet->_count) - 1;
_sparseSet->_dense[count] = value;
_sparseSet->_sparse[sparseIndex] = count;
_sparseSet->_reverse[count] = sparseIndex;
return true;
}
/// <summary>
/// Removes a value at the specified sparse index without resizing the internal arrays.
/// </summary>
/// <param name="sparseIndex">The sparse index of the value to remove.</param>
/// <returns>Returns <see langword="true"/> if the value was successfully removed; otherwise, <see langword="false"/>.</returns>
public bool RemoveNoResize(int sparseIndex)
{
if (!_sparseSet->Contains(sparseIndex))
{
return false;
}
var denseIndex = _sparseSet->_sparse[sparseIndex];
var lastIndex = _sparseSet->_count - 1;
if (denseIndex != lastIndex)
{
var lastValue = _sparseSet->_dense[lastIndex];
var lastSparseIndex = _sparseSet->_reverse[lastIndex];
_sparseSet->_dense[denseIndex] = lastValue;
_sparseSet->_reverse[denseIndex] = lastSparseIndex;
_sparseSet->_sparse[lastSparseIndex] = denseIndex;
}
_sparseSet->_sparse[sparseIndex] = -1;
if (_sparseSet->_freeCount >= _sparseSet->_freeList.Count)
{
return false;
}
_sparseSet->_freeList[_sparseSet->_freeCount] = sparseIndex;
Interlocked.Increment(ref _sparseSet->_freeCount);
Interlocked.Decrement(ref _sparseSet->_count);
return true;
}
}
private UnsafeArray<T> _dense;
private UnsafeArray<int> _sparse;
private UnsafeArray<int> _reverse; // Maps dense index to sparse index
private UnsafeArray<int> _freeList; // Stack of available sparse indices
private int _count;
private int _nextId; // Next available sparse index
private int _freeCount; // Number of items in the free list
public readonly int Count => _count;
public readonly int Capacity => _dense.Count;
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated && _freeList.IsCreated;
public readonly ref T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref _dense[index];
}
public IEnumerator<T> GetEnumerator() => new Enumerator((UnsafeSparseSet<T>*)UnsafeUtilities.AddressOf(ref this));
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// Constructs an UnsafeSparseSet with a default size of 1 and uses the Persistent allocator.
/// </summary>
public UnsafeSparseSet()
: this(1, Allocator.Persistent)
{
}
/// <summary>
/// Initializes a new instance of UnsafeSparseSet with a specified capacity and an allocation handle.
/// </summary>
/// <param name="capacity">Specifies the initial capacity of the sparse set, which must be greater than zero.</param>
/// <param name="handle">A reference to an AllocationHandle that manages the memory allocation for the sparse set.</param>
/// <param name="allocationOption">Specifies how the memory should be allocated.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified capacity is less than or equal to zero.</exception>
public UnsafeSparseSet(int capacity, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
}
_dense = new UnsafeArray<T>(capacity, ref handle, allocationOption);
_sparse = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_reverse = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_freeList = new UnsafeArray<int>(capacity, ref handle, allocationOption);
_count = 0;
_nextId = 0;
_freeCount = 0;
Clear();
}
/// <summary>
/// Initializes a new instance of UnsafeSparseSet with a specified capacity and an allocation type.
/// </summary>
/// <param name="capacity">Specifies the initial capacity of the sparse set, which must be greater than zero.</param>
/// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param>
/// <param name="allocationOption">Determines how the memory should be allocated.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified capacity is less than or equal to zero.</exception>
public UnsafeSparseSet(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
: this(capacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption)
{
}
/// <summary>
/// Adds a value to the sparse set and returns a unique sparse index for the value.
/// </summary>
/// <param name="value">The value to add to the sparse set.</param>
/// <returns>A unique sparse index that can be used to reference this value.</returns>
public int Add(T value)
{
int sparseIndex;
// Try to reuse a freed sparse index first
if (_freeCount > 0)
{
_freeCount--;
sparseIndex = _freeList[_freeCount];
}
else
{
// 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 >= _dense.Count)
{
var newCapacity = _dense.Count + Math.Max(1, _dense.Count / 2);
_dense.Resize(newCapacity);
_reverse.Resize(newCapacity);
}
// Add the value to the dense array and update mappings
_dense[_count] = value;
_sparse[sparseIndex] = _count;
_reverse[_count] = sparseIndex;
_count++;
return sparseIndex;
}
/// <summary>
/// Adds a value to the sparse set at the specified sparse index.
/// This method is provided for compatibility when you need to specify the exact sparse index.
/// </summary>
/// <param name="sparseIndex">The index in the sparse array where the value should be mapped.</param>
/// <param name="value">The value to add to the sparse set.</param>
/// <returns>True if the value was added, false if the sparse index is already occupied.</returns>
public bool AddAt(int sparseIndex, T value)
{
if (sparseIndex < 0)
{
throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index must be non-negative.");
}
if (sparseIndex >= _sparse.Count)
{
ResizeSparse(sparseIndex + 1);
}
if (Contains(sparseIndex))
{
return false;
}
if (_count >= _dense.Count)
{
var newCapacity = _dense.Count + Math.Max(1, _dense.Count / 2);
_dense.Resize(newCapacity);
_reverse.Resize(newCapacity);
}
// Add the value to the dense array and update mappings
_dense[_count] = value;
_sparse[sparseIndex] = _count;
_reverse[_count] = sparseIndex; // Store reverse mapping
_count++;
// Update _nextId if we're using a higher ID
if (sparseIndex >= _nextId)
{
_nextId = sparseIndex + 1;
}
return true;
}
/// <summary>
/// Removes the value at the specified sparse index.
/// </summary>
/// <param name="sparseIndex">The sparse index of the value to remove.</param>
/// <returns>True if the value was removed, false if the sparse index was not found.</returns>
public bool Remove(int sparseIndex)
{
if (!Contains(sparseIndex))
{
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;
// Add the freed sparse index to the free list for reuse
if (_freeCount >= _freeList.Count)
{
_freeList.Resize(_freeList.Count + Math.Max(1, _freeList.Count / 2));
}
_freeList[_freeCount] = sparseIndex;
_freeCount++;
_count--;
return true;
}
/// <summary>
/// Checks if the sparse set contains a value at the specified sparse index.
/// </summary>
/// <param name="sparseIndex">The sparse index to check.</param>
/// <returns>True if the sparse index is valid and contains a value, false otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly bool Contains(int sparseIndex)
{
if (sparseIndex < 0 || sparseIndex >= _sparse.Count)
{
return false;
}
var denseIndex = _sparse[sparseIndex];
return denseIndex >= 0 && denseIndex < _count;
}
/// <summary>
/// Gets the value at the specified sparse index.
/// </summary>
/// <param name="sparseIndex">The sparse index to retrieve the value from.</param>
/// <param name="value">When this method returns, contains the value at the specified sparse index, if found.</param>
/// <returns>True if the sparse index contains a value, false otherwise.</returns>
public readonly bool TryGetValue(int sparseIndex, out T value)
{
if (Contains(sparseIndex))
{
var denseIndex = _sparse[sparseIndex];
value = _dense[denseIndex];
return true;
}
value = default;
return false;
}
/// <summary>
/// Gets the value at the specified sparse index.
/// </summary>
/// <param name="sparseIndex">The sparse index to retrieve the value from.</param>
/// <returns>The value at the specified sparse index.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the sparse index is not found.</exception>
public readonly T GetValue(int sparseIndex)
{
if (!Contains(sparseIndex))
{
throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index not found in the set.");
}
var denseIndex = _sparse[sparseIndex];
return _dense[denseIndex];
}
/// <summary>
/// Updates the value at the specified sparse index.
/// </summary>
/// <param name="sparseIndex">The sparse index of the value to update.</param>
/// <param name="value">The new value.</param>
/// <returns>True if the value was updated, false if the sparse index was not found.</returns>
public bool SetValue(int sparseIndex, T value)
{
if (!Contains(sparseIndex))
{
return false;
}
var denseIndex = _sparse[sparseIndex];
_dense[denseIndex] = value;
return true;
}
private void ResizeSparse(int newSize)
{
var oldSize = _sparse.Count;
_sparse.Resize(newSize);
_sparse.AsSpan()[oldSize..newSize].Fill(-1);
}
/// <inheritdoc/>
public void Clear()
{
if (!IsCreated)
{
return;
}
_sparse.AsSpan().Fill(-1);
_count = 0;
_nextId = 0;
_freeCount = 0;
}
/// <inheritdoc/>
public void Resize(int newSize)
{
if (newSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(newSize), "New size must be greater than zero.");
}
_dense.Resize(newSize);
_reverse.Resize(newSize);
_freeList.Resize(newSize);
if (newSize > _sparse.Count)
{
ResizeSparse(newSize);
}
if (_count > newSize)
{
_count = newSize;
}
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetUnsafePtr()
{
return _dense.GetUnsafePtr();
}
/// <summary>
/// Converts the current sparse set to an UnsafeArray representation using its dense array.
/// </summary>
/// <returns>Returns a new UnsafeArray instance initialized with the dense array's pointer and count.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly UnsafeArray<T> AsUnsafeArray()
{
return new UnsafeArray<T>((T*)_dense.GetUnsafePtr(), _count);
}
/// <inheritdoc/>
public void Dispose()
{
_dense.Dispose();
_sparse.Dispose();
_reverse.Dispose();
_freeList.Dispose();
_count = 0;
_nextId = 0;
_freeCount = 0;
}
}

View File

@@ -1,4 +1,5 @@
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Helpers;
using System.Collections;
using System.Runtime.CompilerServices;