using Misaki.HighPerformance.LowLevel.Contracts; using Misaki.HighPerformance.LowLevel.Exceptions; using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Misaki.HighPerformance.LowLevel.Buffer; /// /// Holds information about a memory allocation. /// public readonly unsafe struct AllocationInfo { /// /// Get the size of the allocation in bytes. /// public nuint Size { get; init; } /// /// Get the allocator used for the allocation. /// public void* Allocator { get; init; } /// /// Get the function pointer used to free the allocated memory. /// public FreeFunc FreeHandler { get; init; } /// /// Get the stack trace at the time of allocation for debugging purposes. /// public StackTrace StackTrace { get; init; } } /// /// Provides memory allocation management for native memory allocations, with support for tracking, /// debugging, and custom allocation strategies. /// public static unsafe class AllocationManager { private unsafe struct ArenaAllocator : IAllocator, IDisposable { private DynamicArena _arena; private AllocationHandle _handle; public readonly ref AllocationHandle Handle => ref Unsafe.AsRef(in _handle); public void Init(uint initialSize) { _arena = new(initialSize); _handle = new(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock); } private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption) { var selfPtr = (ArenaAllocator*)instance; var ptr = selfPtr->_arena.Allocate(size, alignment, allocationOption); return ptr; } private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment) { var selfPtr = (ArenaAllocator*)instance; var newPtr = selfPtr->_arena.Allocate(size, alignment, AllocationOption.None); MemCpy(newPtr, ptr, size); // We do not free the old pointer here, as it is managed by the arena. return newPtr; } private static void FreeBlock(void* instance, void* ptr) { // The arena allocator does not free individual blocks, as it manages memory in chunks. } public void Reset() { _arena.Reset(); } public void Dispose() { _arena.Dispose(); } } private unsafe struct HeapAllocator : IAllocator { private AllocationHandle _handle; public readonly ref AllocationHandle Handle => ref Unsafe.AsRef(in _handle); public void Init() { _handle = new(Unsafe.AsPointer(ref this), &Allocate, &Reallocate, &FreeBlock); } private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption) { var ptr = AlignedAlloc(size, alignment); if (!allocationOption.HasFlag(AllocationOption.UnTracked)) { TrackAllocation(ptr, size, instance, &FreeBlock); } if (allocationOption.HasFlag(AllocationOption.Clear)) { MemClear(ptr, size); } return ptr; } private static void* Reallocate(void* instance, void* ptr, nuint size, nuint alignment) { var newPtr = AlignedRealloc(ptr, size, alignment); UpdateAllocation(ptr, newPtr, size, instance, &FreeBlock); return newPtr; } private static void FreeBlock(void* instance, void* ptr) { AlignedFree(ptr); UntrackAllocation(ptr); } } private const uint _DEFAULT_ARENA_SIZE = 512 * 1024; private static readonly ArenaAllocator* s_arenaAllocator; private static readonly HeapAllocator* s_persistentAllocator; private static bool s_debugLayer; private static ConcurrentDictionary? s_allocated; static AllocationManager() { s_arenaAllocator = (ArenaAllocator*)NativeMemory.Alloc((nuint)sizeof(ArenaAllocator)); s_persistentAllocator = (HeapAllocator*)NativeMemory.Alloc((nuint)sizeof(HeapAllocator)); s_arenaAllocator->Init(_DEFAULT_ARENA_SIZE); s_persistentAllocator->Init(); } /// /// Enables the debug layer, allowing additional diagnostic information to be collected. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void EnableDebugLayer() { s_debugLayer = true; s_allocated ??= new(-1, 64); } /// /// Gets a reference to the allocation handle for the specified allocator type. /// /// The allocator type for which to retrieve the allocation handle. /// A reference to the allocation handle associated with the specified allocator type. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ref AllocationHandle GetAllocationHandle(Allocator allocator) { switch (allocator) { case Allocator.Temp: return ref s_arenaAllocator->Handle; case Allocator.Persistent: return ref s_persistentAllocator->Handle; default: throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)); } } /// /// Tracks a memory allocation in the allocation manager. /// /// 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, FreeFunc freeFunc) { if (!s_debugLayer || s_allocated == null || ptr == null) { return; } s_allocated[(nint)ptr] = new AllocationInfo { Size = allocationSize, Allocator = allocator, FreeHandler = freeFunc, StackTrace = new StackTrace(true) }; } /// /// 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, FreeFunc freeFunc) { if (!s_debugLayer || s_allocated == null || oldPtr == null || newPtr == null) { return; } // If we don't find the allocation info, that means the oldPtr was not tracked if (s_allocated.Remove((nint)oldPtr, out var info)) { s_allocated[(nint)newPtr] = new AllocationInfo { Size = allocationSize, Allocator = allocator, FreeHandler = freeFunc, StackTrace = info.StackTrace }; } } /// /// Removes the specified memory allocation from the tracking system. /// /// A pointer to the memory allocation to untrack. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void UntrackAllocation(void* ptr) { if (s_allocated == null) { return; } s_allocated.Remove((nint)ptr, out _); } /// /// Resets the temporary memory allocator, clearing all allocated memory. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ResetTempAllocator() { s_arenaAllocator->Reset(); } /// /// Disposes of the AllocationManager, freeing all allocated memory and resources. /// public static void Dispose() { if (s_allocated != null) { nuint unfreeBytes = 0u; foreach (var pair in s_allocated) { unfreeBytes += pair.Value.Size; pair.Value.FreeHandler(pair.Value.Allocator, (void*)pair.Key); } if (unfreeBytes > 0u) { throw new MemoryLeakException([.. s_allocated.Values]); } s_allocated.Clear(); } if (s_arenaAllocator != null) { s_arenaAllocator->Dispose(); NativeMemory.Free(s_arenaAllocator); } if (s_persistentAllocator != null) { NativeMemory.Free(s_persistentAllocator); } } }