diff --git a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs
index f67b01b..f0ff442 100644
--- a/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs
+++ b/Misaki.HighPerformance.LowLevel/Buffer/AllocationManager.cs
@@ -1,6 +1,4 @@
-using Misaki.HighPerformance.LowLevel.Contracts;
-using Misaki.HighPerformance.LowLevel.Exceptions;
-using System.Collections.Concurrent;
+using Misaki.HighPerformance.LowLevel.Contracts;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -20,14 +18,6 @@ public readonly unsafe struct AllocationInfo
get; init;
}
- ///
- /// Get the allocator used for the allocation.
- ///
- public void* Allocator
- {
- get; init;
- }
-
///
/// Get the stack trace at the time of allocation for debugging purposes.
///
@@ -43,6 +33,17 @@ public readonly unsafe struct AllocationInfo
///
public static unsafe class AllocationManager
{
+ // === Intrusive allocation tracking (enabled when debug layer is on) ===
+ [StructLayout(LayoutKind.Sequential)]
+ private struct AllocationHeader
+ {
+ public AllocationHeader* prev;
+ public AllocationHeader* next;
+ public void* basePtr; // pointer returned by underlying allocator
+ public nuint userSize; // requested size from the user
+ public nint stackHandle; // GCHandle to managed StackTrace (stored as IntPtr)
+ }
+
private unsafe struct ArenaAllocator : IAllocator, IDisposable
{
private DynamicArena _arena;
@@ -111,49 +112,17 @@ public static unsafe class AllocationManager
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption)
{
- var ptr = AlignedAlloc(size, alignment);
-
- if (allocationOption.HasFlag(AllocationOption.Clear))
- {
- MemClear(ptr, size);
- }
-
- if (!allocationOption.HasFlag(AllocationOption.UnTracked))
- {
- TrackAllocation(ptr, size, instance);
- }
-
- return ptr;
+ return HeapAlloc(size, alignment, allocationOption);
}
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
{
- var newPtr = AlignedRealloc(ptr, newSize, alignment);
-
- if (allocationOption.HasFlag(AllocationOption.Clear))
- {
- if (newSize > oldSize)
- {
- MemClear((byte*)newPtr + oldSize, newSize - oldSize);
- }
- }
-
- if (!allocationOption.HasFlag(AllocationOption.UnTracked))
- {
- UpdateAllocation(ptr, newPtr, newSize, instance);
- }
- else
- {
- UntrackAllocation(ptr);
- }
-
- return newPtr;
+ return HeapRealloc(ptr, oldSize, newSize, alignment, allocationOption);
}
private static void FreeBlock(void* instance, void* ptr)
{
- AlignedFree(ptr);
- UntrackAllocation(ptr);
+ HeapFree(ptr);
}
}
@@ -207,32 +176,193 @@ public static unsafe class AllocationManager
private const uint _DEFAULT_MEMORY_POOL_SIZE = 512 * 1024;
- private static readonly ArenaAllocator* s_arenaAllocator;
- private static readonly HeapAllocator* s_persistentAllocator;
- private static readonly StackAllocator* s_stackAllocator;
+ private static readonly ArenaAllocator* s_pArenaAllocator;
+ private static readonly HeapAllocator* s_pHeapAllocator;
+ private static readonly StackAllocator* s_pStackAllocator;
private static bool s_debugLayer;
- private static ConcurrentDictionary? s_allocated;
+ private static bool s_disposed;
+
+ private static AllocationHeader* s_liveHead;
+ private static SpinLock s_liveLock;
+
+ // Lightweight allocation counter for non-debug layer (no sizes, just count of live heap blocks)
+ private static long s_activeHeapAllocations;
static AllocationManager()
{
- s_arenaAllocator = (ArenaAllocator*)NativeMemory.Alloc((nuint)sizeof(ArenaAllocator));
- s_persistentAllocator = (HeapAllocator*)NativeMemory.Alloc((nuint)sizeof(HeapAllocator));
- s_stackAllocator = (StackAllocator*)NativeMemory.Alloc((nuint)sizeof(StackAllocator));
+ s_pArenaAllocator = (ArenaAllocator*)NativeMemory.Alloc((nuint)sizeof(ArenaAllocator));
+ s_pHeapAllocator = (HeapAllocator*)NativeMemory.Alloc((nuint)sizeof(HeapAllocator));
+ s_pStackAllocator = (StackAllocator*)NativeMemory.Alloc((nuint)sizeof(StackAllocator));
- s_arenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE);
- s_persistentAllocator->Init();
- s_stackAllocator->Init();
+ s_liveLock = new SpinLock(false);
+
+ s_pArenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE);
+ s_pHeapAllocator->Init();
+ s_pStackAllocator->Init();
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static byte* AlignUp(byte* p, nuint alignment)
+ {
+ var a = alignment == 0 ? (nuint)IntPtr.Size : alignment;
+ return (byte*)(((nuint)p + (a - 1)) & ~(a - 1));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static GCHandle HeaderGetHandle(AllocationHeader* header) => GCHandle.FromIntPtr(header->stackHandle);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void HeaderSetHandle(AllocationHeader* header, GCHandle handle) => header->stackHandle = GCHandle.ToIntPtr(handle);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void HeaderFreeHandle(AllocationHeader* header)
+ {
+ if (header->stackHandle != 0)
+ {
+ GCHandle.FromIntPtr(header->stackHandle).Free();
+ header->stackHandle = 0;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void LinkHeader(AllocationHeader* header)
+ {
+ var taken = false;
+ try
+ {
+ s_liveLock.Enter(ref taken);
+ header->prev = null;
+ header->next = s_liveHead;
+ if (s_liveHead != null)
+ {
+ s_liveHead->prev = header;
+ }
+ s_liveHead = header;
+ }
+ finally
+ {
+ if (taken)
+ {
+ s_liveLock.Exit();
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void UnlinkHeader(AllocationHeader* header)
+ {
+ var taken = false;
+ try
+ {
+ s_liveLock.Enter(ref taken);
+ var prev = header->prev;
+ var next = header->next;
+
+ if (prev != null)
+ prev->next = next;
+ else
+ s_liveHead = next;
+
+ if (next != null)
+ next->prev = prev;
+
+ header->prev = header->next = null;
+ }
+ finally
+ {
+ if (taken)
+ {
+ s_liveLock.Exit();
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void* DebugAllocate(nuint size, nuint alignment)
+ {
+ // Over-allocate to fit header + alignment padding; we align the user pointer, header is placed just before it.
+ var pad = alignment == 0 ? (nuint)IntPtr.Size : alignment;
+ var total = size + (nuint)sizeof(AllocationHeader) + (pad - 1);
+
+ var basePtr = AlignedAlloc(total, pad);
+ var user = AlignUp((byte*)basePtr + (nuint)sizeof(AllocationHeader), pad);
+ var header = (AllocationHeader*)(user - (nuint)sizeof(AllocationHeader));
+
+ header->basePtr = basePtr;
+ header->userSize = size;
+ HeaderSetHandle(header, GCHandle.Alloc(new StackTrace(2, true)));
+
+ LinkHeader(header);
+ return user;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void DebugFree(void* userPtr)
+ {
+ var header = (AllocationHeader*)((byte*)userPtr - (nuint)sizeof(AllocationHeader));
+ UnlinkHeader(header);
+ HeaderFreeHandle(header);
+ AlignedFree(header->basePtr);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void* DebugReallocate(void* userPtr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption)
+ {
+ if (userPtr == null)
+ {
+ return DebugAllocate(newSize, alignment);
+ }
+
+ var oldHeader = (AllocationHeader*)((byte*)userPtr - (nuint)sizeof(AllocationHeader));
+ var handle = HeaderGetHandle(oldHeader); // preserve original allocation StackTrace
+
+ var pad = alignment == 0 ? (nuint)IntPtr.Size : alignment;
+ var total = newSize + (nuint)sizeof(AllocationHeader) + (pad - 1);
+ var newBase = AlignedAlloc(total, pad);
+ var newUser = AlignUp((byte*)newBase + (nuint)sizeof(AllocationHeader), pad);
+ var newHeader = (AllocationHeader*)(newUser - (nuint)sizeof(AllocationHeader));
+
+ newHeader->basePtr = newBase;
+ newHeader->userSize = newSize;
+ HeaderSetHandle(newHeader, handle); // transfer ownership to the new header
+
+ LinkHeader(newHeader);
+
+ // Mirror original behavior: copy newSize bytes
+ MemCpy(newUser, userPtr, newSize);
+ if (allocationOption.HasFlag(AllocationOption.Clear) && newSize > oldSize)
+ {
+ MemClear((byte*)newUser + oldSize, newSize - oldSize);
+ }
+
+ // Unlink and free the old block (without freeing the StackTrace handle again)
+ oldHeader->stackHandle = 0;
+ UnlinkHeader(oldHeader);
+ AlignedFree(oldHeader->basePtr);
+
+ return newUser;
+ }
+
+ ///
+ /// Gets the number of live persistent heap allocations when the debug layer is disabled.
+ ///
+ public static long LiveHeapAllocationCount => Interlocked.Read(ref s_activeHeapAllocations);
+
///
/// Enables the debug layer, allowing additional diagnostic information to be collected.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EnableDebugLayer()
{
+ // To avoid ambiguity between pointers allocated before/after enabling, this must be called
+ // before any heap allocations are live.
+ if (Interlocked.Read(ref s_activeHeapAllocations) != 0)
+ {
+ throw new InvalidOperationException("EnableDebugLayer must be called before any heap allocations are active.");
+ }
+
s_debugLayer = true;
- s_allocated ??= new(-1, 64);
}
///
@@ -247,80 +377,95 @@ public static unsafe class AllocationManager
switch (allocator)
{
case Allocator.Temp:
- return ref s_arenaAllocator->Handle;
+ return ref s_pArenaAllocator->Handle;
case Allocator.Persistent:
- return ref s_persistentAllocator->Handle;
+ return ref s_pHeapAllocator->Handle;
case Allocator.Stack:
- return ref s_stackAllocator->Handle;
+ return ref s_pStackAllocator->Handle;
default:
throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator));
}
}
///
- /// Tracks a memory allocation in the allocation manager.
+ /// Allocates a block of memory from the heap with the specified size and alignment, using the given allocation
+ /// options.
///
- /// The pointer to the allocated memory.
- /// The size of the allocation in bytes.
- /// The allocator used for the allocation.
- /// The function pointer used to free the allocated memory.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void TrackAllocation(void* ptr, nuint allocationSize, void* allocator)
+ /// The number of bytes to allocate. Must be greater than zero.
+ /// The alignment, in bytes, for the allocated memory block. Must be a power of two.
+ /// An optional set of flags that control allocation behavior, such as whether the memory should be cleared or
+ /// tracked. The default is .
+ /// A pointer to the beginning of the allocated memory block.
+ /// Thrown if the allocation fails.
+ public static void* HeapAlloc(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{
- if (!s_debugLayer || s_allocated == null || ptr == null)
+ void* ptr;
+ if (s_debugLayer)
{
- return;
+ ptr = DebugAllocate(size, alignment);
+ }
+ else
+ {
+ ptr = AlignedAlloc(size, alignment);
}
- s_allocated[(nint)ptr] = new AllocationInfo
+ if (allocationOption.HasFlag(AllocationOption.Clear))
{
- Size = allocationSize,
- Allocator = allocator,
- StackTrace = new StackTrace(true)
- };
+ MemClear(ptr, size);
+ }
+
+ Interlocked.Increment(ref s_activeHeapAllocations);
+ return ptr;
}
///
- /// Updates the allocation tracking information by replacing the old pointer with a new pointer.
- ///
- /// A pointer to the previously allocated memory. If is not tracked, the method does nothing.
- /// A pointer to the newly allocated memory. This pointer will replace in the allocation tracking.
- /// The size, in bytes, of the new allocation.
- /// A pointer to the allocator responsible for the new allocation.
- /// A delegate or function pointer used to free the memory associated with the allocation.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void UpdateAllocation(void* oldPtr, void* newPtr, nuint allocationSize, void* allocator)
+ /// Reallocates a block of memory to a new size and alignment, optionally clearing newly allocated memory and
+ /// applying allocation options.
+ /// \
+ /// A pointer to the previously allocated memory block to be reallocated. Can be to allocate new memory.
+ /// The size, in bytes, of the memory block currently pointed to by .
+ /// The desired size, in bytes, for the reallocated memory block.
+ /// The required alignment, in bytes, for the reallocated memory block. Must be a power of two.
+ /// An optional set of flags that control allocation behavior, such as whether to clear newly allocated memory or
+ /// track the allocation. The default is .
+ /// A pointer to the reallocated memory block with the specified size and alignment. Returns
+ /// if the allocation fails.
+ public static void* HeapRealloc(void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{
- if (!s_debugLayer || s_allocated == null || oldPtr == null || newPtr == null)
+ if (s_debugLayer)
{
- return;
+ return DebugReallocate(ptr, oldSize, newSize, alignment, allocationOption);
}
- // If we don't find the allocation info, that means the oldPtr was not tracked
- if (s_allocated.Remove((nint)oldPtr, out var info))
+ var newPtr = AlignedRealloc(ptr, newSize, alignment);
+ if (allocationOption.HasFlag(AllocationOption.Clear))
{
- s_allocated[(nint)newPtr] = new AllocationInfo
+ if (newSize > oldSize)
{
- Size = allocationSize,
- Allocator = allocator,
- StackTrace = info.StackTrace
- };
+ MemClear((byte*)newPtr + oldSize, newSize - oldSize);
+ }
}
+
+ return newPtr;
}
///
- /// Removes the specified memory allocation from the tracking system.
+ /// Releases a block of unmanaged memory previously allocated by the heap allocator.
///
- /// A pointer to the memory allocation to untrack.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void UntrackAllocation(void* ptr)
+ /// A pointer to the memory block to be freed. The pointer must have been returned by a compatible heap allocation
+ /// method and must not be null.
+ public static void HeapFree(void* ptr)
{
- if (s_allocated == null)
+ if (s_debugLayer)
{
- return;
+ DebugFree(ptr);
+ }
+ else
+ {
+ AlignedFree(ptr);
}
- s_allocated.Remove((nint)ptr, out _);
+ Interlocked.Decrement(ref s_activeHeapAllocations);
}
///
@@ -329,7 +474,7 @@ public static unsafe class AllocationManager
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ResetTempAllocator()
{
- s_arenaAllocator->Reset();
+ s_pArenaAllocator->Reset();
}
///
@@ -347,36 +492,74 @@ public static unsafe class AllocationManager
///
public static void Dispose()
{
- if (s_allocated != null)
+ if (s_disposed)
{
- nuint unfreeBytes = 0u;
- foreach (var pair in s_allocated)
+ return;
+ }
+
+ // In debug mode, walk the intrusive list to surface any leaks.
+ if (s_debugLayer)
+ {
+ var snapshot = new List();
+ var taken = false;
+ try
{
- unfreeBytes += pair.Value.Size;
+ s_liveLock.Enter(ref taken);
+ if (s_liveHead != null)
+ {
+ snapshot.Capacity = 128;
+ for (var p = s_liveHead; p != null; p = p->next)
+ {
+ var trace = (StackTrace)HeaderGetHandle(p).Target!;
+ snapshot.Add(new AllocationInfo
+ {
+ Size = p->userSize,
+ StackTrace = trace
+ });
+ }
+ }
+ }
+ finally
+ {
+ if (taken)
+ {
+ s_liveLock.Exit();
+ }
+ }
+
+ nuint unfreeBytes = 0u;
+ foreach (var info in snapshot)
+ {
+ unfreeBytes += info.Size;
}
if (unfreeBytes > 0u)
{
- throw new MemoryLeakException([.. s_allocated.Values]);
+ throw new MemoryLeakException(snapshot);
}
-
- s_allocated.Clear();
}
-
- if (s_arenaAllocator != null)
+ else if (s_activeHeapAllocations != 0)
{
- s_arenaAllocator->Dispose();
- NativeMemory.Free(s_arenaAllocator);
+ throw new MemoryLeakException($"Found {s_activeHeapAllocations} memory lakes! Please enable debug layer for more informations.");
}
- if (s_stackAllocator != null)
+ if (s_pArenaAllocator != null)
{
- NativeMemory.Free(s_stackAllocator);
+ s_pArenaAllocator->Dispose();
+ NativeMemory.Free(s_pArenaAllocator);
}
- if (s_persistentAllocator != null)
+ if (s_pHeapAllocator != null)
{
- NativeMemory.Free(s_persistentAllocator);
+ NativeMemory.Free(s_pHeapAllocator);
}
+
+ if (s_pStackAllocator != null)
+ {
+ NativeMemory.Free(s_pStackAllocator);
+ }
+
+ s_activeHeapAllocations = 0;
+ s_disposed = true;
}
}
\ No newline at end of file
diff --git a/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs b/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs
index 36e613e..73bf2f1 100644
--- a/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs
+++ b/Misaki.HighPerformance.LowLevel/Buffer/AllocationOption.cs
@@ -1,21 +1,16 @@
-namespace Misaki.HighPerformance.LowLevel.Buffer;
+namespace Misaki.HighPerformance.LowLevel.Buffer;
[Flags]
public enum AllocationOption : byte
{
+ ///
+ /// Default allocation option. Values are uninitialized.
+ ///
None = 0,
///
- /// Allocator for initialized memory.
+ /// Clear the memory to zero upon allocation.
///
Clear = 1 << 0,
- ///
- /// Allocator for untracked memory.
- ///
- ///
- /// Use this option carefully, as the allocation manager will not track the memory.
- /// No warning will be given if the memory is not freed.
- ///
- UnTracked = 1 << 1,
}
public enum Allocator : byte
@@ -23,15 +18,15 @@ 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,
///
- /// Allocator for temporary allocations. Allocations are released after use automatically.
+ /// Allocator for temporary allocations. Allocations are automatically released after use automatically.
///
Temp,
///
- /// Allocator for persistent allocations. Allocations are not released after use.
+ /// Allocator for persistent allocations. Allocations are not automatically released after use.
///
Persistent,
///
- /// Allocator for stack allocations. Must have at least one active stack scope. Allocations are released when the stack scope is exited.
+ /// Allocator for stack allocations. Must have at least one active stack scope. Allocations are automatically released when the stack scope is exited.
///
Stack
}
\ No newline at end of file
diff --git a/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs b/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs
index 2dc87d2..3f01c5d 100644
--- a/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs
+++ b/Misaki.HighPerformance.LowLevel/Buffer/Stack.cs
@@ -1,4 +1,4 @@
-using System.Runtime.CompilerServices;
+using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
@@ -11,7 +11,7 @@ public unsafe struct Stack : IDisposable
{
private const nuint _DEFAULT_SIZE = 1024 * 1024; // 1MB
- public readonly struct Scope : IDisposable
+ public readonly ref struct Scope : IDisposable
{
private readonly Stack* _allocator;
private readonly nuint _originalOffset;
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeArray.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeArray.cs
index 9751430..9e73d3a 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeArray.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeArray.cs
@@ -1,4 +1,4 @@
-using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
@@ -107,7 +107,8 @@ public unsafe struct UnsafeArray : IUnsafeCollection
get => _buffer != null;
}
- public IEnumerator GetEnumerator() => new Enumerator((UnsafeArray*)UnsafeUtility.AddressOf(ref this));
+ public Enumerator GetEnumerator() => new ((UnsafeArray*)UnsafeUtility.AddressOf(ref this));
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashMap.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashMap.cs
index 56ae1aa..01545ba 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashMap.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashMap.cs
@@ -1,4 +1,4 @@
-using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
@@ -92,7 +92,8 @@ public unsafe struct UnsafeHashMap : IUnsafeCollection> GetEnumerator() => new Enumerator((HashMapHelper*)UnsafeUtility.AddressOf(ref _hashMap));
+ public Enumerator GetEnumerator() => new((HashMapHelper*)UnsafeUtility.AddressOf(ref this));
+ IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashSet.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashSet.cs
index 11deb0d..0bf5b7e 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashSet.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeHashSet.cs
@@ -1,4 +1,4 @@
-using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
@@ -48,7 +48,8 @@ public unsafe struct UnsafeHashSet : IUnsafeCollection, IEnumerable
public readonly int Capacity => _hashMap.Capacity;
public readonly bool IsCreated => _hashMap.IsCreated;
- public IEnumerator GetEnumerator() => new Enumerator((HashMapHelper*)UnsafeUtility.AddressOf(ref _hashMap));
+ public Enumerator GetEnumerator() => new((HashMapHelper*)UnsafeUtility.AddressOf(ref this));
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs
index 6f7c541..0534552 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeList.cs
@@ -1,4 +1,4 @@
-using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
@@ -136,7 +136,8 @@ public unsafe struct UnsafeList : IUnsafeCollection
get => ref _array[index];
}
- public IEnumerator GetEnumerator() => new Enumerator((UnsafeList*)UnsafeUtility.AddressOf(ref this));
+ public Enumerator GetEnumerator() => new ((UnsafeList*)UnsafeUtility.AddressOf(ref this));
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeQueue.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeQueue.cs
index d1a2c36..d0fbd91 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeQueue.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeQueue.cs
@@ -80,7 +80,8 @@ public unsafe struct UnsafeQueue : IUnsafeCollection
set => _array[index] = value;
}
- public IEnumerator GetEnumerator() => new Enumerator((UnsafeQueue*)UnsafeUtility.AddressOf(ref this));
+ public Enumerator GetEnumerator() => new((UnsafeQueue*)UnsafeUtility.AddressOf(ref this));
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSlotMap.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSlotMap.cs
index 9a1a6e0..4ab075a 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSlotMap.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSlotMap.cs
@@ -67,8 +67,8 @@ public unsafe struct UnsafeSlotMap : IUnsafeCollection
public readonly bool IsCreated => _data.IsCreated && _freeSlots.IsCreated;
- public IEnumerator GetEnumerator() => new Enumerator((UnsafeSlotMap*)UnsafeUtility.AddressOf(ref this));
-
+ public Enumerator GetEnumerator() => new((UnsafeSlotMap*)UnsafeUtility.AddressOf(ref this));
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs
index c109adb..387b2d2 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeSparseSet.cs
@@ -80,7 +80,8 @@ public unsafe struct UnsafeSparseSet : IUnsafeCollection
public readonly int Capacity => _capacity;
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated;
- public IEnumerator GetEnumerator() => new Enumerator((UnsafeSparseSet*)UnsafeUtility.AddressOf(ref this));
+ public Enumerator GetEnumerator() => new((UnsafeSparseSet*)UnsafeUtility.AddressOf(ref this));
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
diff --git a/Misaki.HighPerformance.LowLevel/Collections/UnsafeStack.cs b/Misaki.HighPerformance.LowLevel/Collections/UnsafeStack.cs
index 154a5bd..250f3cb 100644
--- a/Misaki.HighPerformance.LowLevel/Collections/UnsafeStack.cs
+++ b/Misaki.HighPerformance.LowLevel/Collections/UnsafeStack.cs
@@ -1,4 +1,4 @@
-using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
using Misaki.HighPerformance.LowLevel.Contracts;
using Misaki.HighPerformance.LowLevel.Utilities;
@@ -15,20 +15,64 @@ namespace Misaki.HighPerformance.LowLevel.Collections;
public unsafe struct UnsafeStack : IUnsafeCollection
where T : unmanaged
{
+ public struct Enumerator : IEnumerator
+ {
+ private readonly UnsafeStack* _collection;
+ private int _index;
+ private T _value;
+
+ public Enumerator(UnsafeStack* collection)
+ {
+ _collection = collection;
+ _index = collection->Count;
+ _value = default;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool MoveNext()
+ {
+ _index--;
+ if (_index >= 0)
+ {
+ _value = UnsafeUtility.ReadArrayElement(_collection->_array.GetUnsafePtr(), _index);
+ return true;
+ }
+
+ _value = default;
+ return false;
+ }
+
+ public void Reset()
+ {
+ _index = _collection->Count;
+ }
+
+ public readonly T Current
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _value;
+ }
+
+ readonly object IEnumerator.Current
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Current;
+ }
+
+ public readonly void Dispose()
+ {
+ }
+ }
+
private UnsafeArray _array;
private int _count;
public readonly int Count => _count;
public readonly bool IsCreated => _array.IsCreated;
- public IEnumerator GetEnumerator()
- {
- throw new NotImplementedException();
- }
- IEnumerator IEnumerable.GetEnumerator()
- {
- return GetEnumerator();
- }
+ public Enumerator GetEnumerator() => new((UnsafeStack*)UnsafeUtility.AddressOf(ref this));
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
///
/// Invalid constructor, use or instead.
@@ -41,23 +85,23 @@ public unsafe struct UnsafeStack : IUnsafeCollection
///
/// Initializes a new instance of the UnsafeStack class with the specified initial capacity and allocation options.
///
- /// The number of elements the stack can initially hold. Must be greater than zero.
+ /// The number of elements the stack can initially hold. Must be greater than zero.
/// A reference to an AllocationHandle used to manage the underlying memory allocation for the stack.
/// Specifies additional options for memory allocation. The default is AllocationOption.None.
- public UnsafeStack(int initialCapacity, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
+ public UnsafeStack(int capacity, ref AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
{
- _array = new UnsafeArray(initialCapacity, ref handle, allocationOption);
+ _array = new UnsafeArray(capacity, ref handle, allocationOption);
}
///
/// Initializes a new instance of the UnsafeStack class with the specified initial capacity, allocator, and
/// allocation options.
///
- /// The initial number of elements that the stack can hold. Must be greater than zero.
+ /// The initial number of elements that the stack can hold. Must be greater than zero.
/// The allocator to use for memory management of the stack's storage.
/// The allocation option that determines how memory is allocated for the stack. The default is AllocationOption.None.
- public UnsafeStack(int initialCapacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
- : this(initialCapacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption)
+ public UnsafeStack(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
+ : this(capacity, ref AllocationManager.GetAllocationHandle(allocator), allocationOption)
{
}
diff --git a/Misaki.HighPerformance.LowLevel/Exceptions/MemoryLeakException.cs b/Misaki.HighPerformance.LowLevel/MemoryLeakException.cs
similarity index 64%
rename from Misaki.HighPerformance.LowLevel/Exceptions/MemoryLeakException.cs
rename to Misaki.HighPerformance.LowLevel/MemoryLeakException.cs
index de8a0bb..69a002d 100644
--- a/Misaki.HighPerformance.LowLevel/Exceptions/MemoryLeakException.cs
+++ b/Misaki.HighPerformance.LowLevel/MemoryLeakException.cs
@@ -1,15 +1,28 @@
-using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Buffer;
using System.Diagnostics;
using System.Text;
-namespace Misaki.HighPerformance.LowLevel.Exceptions;
+namespace Misaki.HighPerformance.LowLevel;
///
/// An exception that is thrown when a memory leak is detected.
///
/// An array of AllocationInfo containing details about the memory leaks.
-public class MemoryLeakException(params AllocationInfo[] Infos) : Exception
+public class MemoryLeakException : Exception
{
+ private readonly IEnumerable? _infos;
+ private readonly string _message = string.Empty;
+
+ public MemoryLeakException(IEnumerable infos)
+ {
+ _infos = infos;
+ }
+
+ public MemoryLeakException(string message)
+ {
+ _message = message;
+ }
+
private static string GetMessage(StackTrace? stackTrace)
{
if (stackTrace == null)
@@ -36,9 +49,15 @@ public class MemoryLeakException(params AllocationInfo[] Infos) : Exception
{
get
{
+ if (_infos == null)
+ {
+ return _message;
+ }
+
var stringBuilder = new StringBuilder();
- stringBuilder.AppendLine($"Found {Infos.Length} memory lakes!");
- foreach (var info in Infos)
+ stringBuilder.AppendLine($"Found {_infos.Count()} memory lakes!");
+
+ foreach (var info in _infos)
{
stringBuilder.AppendLine(GetMessage(info.StackTrace));
}
diff --git a/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs b/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs
index 281c636..2d75aa0 100644
--- a/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs
+++ b/Misaki.HighPerformance.LowLevel/Utilities/MemoryUtility.cs
@@ -1,4 +1,4 @@
-using System.Runtime.CompilerServices;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Utilities;
@@ -69,11 +69,6 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Free(void* ptr)
{
- if (ptr == null)
- {
- return;
- }
-
NativeMemory.Free(ptr);
}
@@ -85,11 +80,6 @@ public static unsafe partial class MemoryUtility
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AlignedFree(void* ptr)
{
- if (ptr == null)
- {
- return;
- }
-
NativeMemory.AlignedFree(ptr);
}
diff --git a/Misaki.HighPerformance.Test/Program.cs b/Misaki.HighPerformance.Test/Program.cs
index 13aaf49..42157eb 100644
--- a/Misaki.HighPerformance.Test/Program.cs
+++ b/Misaki.HighPerformance.Test/Program.cs
@@ -43,4 +43,14 @@
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
-var array = new UnsafeArray(10, Allocator.Persistent);
+//AllocationManager.EnableDebugLayer();
+//var array = new UnsafeArray(10, Allocator.Persistent);
+//var array2 = new UnsafeArray(10, Allocator.Persistent);
+//array.Dispose();
+//array2.Dispose();
+//AllocationManager.Dispose();
+
+using (AllocationManager.CreateStackScope())
+{
+ var arr = new UnsafeArray(10, Allocator.Stack);
+}
\ No newline at end of file
diff --git a/Misaki.HighPerformance.Test/UnitTest/Buffer/TestAllocationManager.cs b/Misaki.HighPerformance.Test/UnitTest/Buffer/TestAllocationManager.cs
new file mode 100644
index 0000000..e8ed6f0
--- /dev/null
+++ b/Misaki.HighPerformance.Test/UnitTest/Buffer/TestAllocationManager.cs
@@ -0,0 +1,61 @@
+using Misaki.HighPerformance.LowLevel;
+using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Collections;
+
+namespace Misaki.HighPerformance.Test.UnitTest.Buffer;
+
+[TestClass]
+public class TestAllocationManager
+{
+ [TestInitialize]
+ public void Initialize()
+ {
+ AllocationManager.EnableDebugLayer();
+ }
+
+ [TestMethod]
+ public void ShouldNotLeakTest()
+ {
+ try
+ {
+ var array = new UnsafeArray(10, Allocator.Persistent);
+ var array2 = new UnsafeArray(10, Allocator.Persistent);
+
+ array.Dispose();
+ array2.Dispose();
+
+ AllocationManager.Dispose();
+ }
+ finally
+ {
+ var leaks = AllocationManager.LiveHeapAllocationCount;
+ Assert.AreEqual(0, leaks);
+ }
+ }
+
+ [TestMethod]
+ public void ShouldLeakTest()
+ {
+ var array = new UnsafeArray(10, Allocator.Persistent);
+ var array2 = new UnsafeArray(10, Allocator.Persistent);
+
+ try
+ {
+ AllocationManager.Dispose();
+ }
+ catch (MemoryLeakException)
+ {
+ var leaks = AllocationManager.LiveHeapAllocationCount;
+ Assert.AreEqual(2, leaks);
+
+ return;
+ }
+ finally
+ {
+ array.Dispose();
+ array2.Dispose();
+ }
+
+ Assert.Fail("Expected MemoryLeakException was not thrown.");
+ }
+}
\ No newline at end of file
diff --git a/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeArray.cs b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeArray.cs
new file mode 100644
index 0000000..dee7deb
--- /dev/null
+++ b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeArray.cs
@@ -0,0 +1,56 @@
+using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Collections;
+
+namespace Misaki.HighPerformance.Test.UnitTest.Collections;
+
+[TestClass]
+public class TestUnsafeArray
+{
+ private UnsafeArray _arr;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _arr = new UnsafeArray(16, Allocator.Persistent);
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ _arr.Dispose();
+ }
+
+ [TestMethod]
+ public void TestIndexAccess()
+ {
+ for (int i = 0; i < _arr.Count; i++)
+ {
+ _arr[i] = i * 10;
+ }
+
+ for (int i = 0; i < _arr.Count; i++)
+ {
+ Assert.AreEqual(i * 10, _arr[i]);
+ }
+ }
+
+ [TestMethod]
+ public void TestEnumeration()
+ {
+ _arr.Clear();
+
+ int expectedValue = 0;
+ foreach (var item in _arr)
+ {
+ Assert.AreEqual(expectedValue, item);
+ }
+ }
+
+ [TestMethod]
+ public void TestIsCreated()
+ {
+ Assert.IsTrue(_arr.IsCreated);
+ _arr.Dispose();
+ Assert.IsFalse(_arr.IsCreated);
+ }
+}
\ No newline at end of file
diff --git a/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeStack.cs b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeStack.cs
new file mode 100644
index 0000000..7c12e9b
--- /dev/null
+++ b/Misaki.HighPerformance.Test/UnitTest/Collections/TestUnsafeStack.cs
@@ -0,0 +1,68 @@
+using Misaki.HighPerformance.LowLevel.Buffer;
+using Misaki.HighPerformance.LowLevel.Collections;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Misaki.HighPerformance.Test.UnitTest.Collections;
+
+[TestClass]
+public class TestUnsafeStack
+{
+ private UnsafeStack _stack;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _stack = new UnsafeStack(16, Allocator.Persistent);
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ _stack.Dispose();
+ }
+
+ [TestMethod]
+ public void TestPushPop()
+ {
+ for (int i = 0; i < 10; i++)
+ {
+ _stack.Push(i);
+ }
+ Assert.AreEqual(10, _stack.Count);
+ for (int i = 9; i >= 0; i--)
+ {
+ int value = _stack.Pop();
+ Assert.AreEqual(i, value);
+ }
+ Assert.AreEqual(0, _stack.Count);
+ }
+
+ [TestMethod]
+ public void TestPeek()
+ {
+ _stack.Push(42);
+ int value = _stack.Peek();
+ Assert.AreEqual(42, value);
+ Assert.AreEqual(1, _stack.Count);
+ }
+
+ [TestMethod]
+ public void TestEnumeration()
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ _stack.Push(i);
+ }
+
+ int expected = 4;
+ foreach (var item in _stack)
+ {
+ Assert.AreEqual(expected, item);
+ expected--;
+ }
+ }
+}