#if MHP_ENABLE_SAFETY_CHECKS using Misaki.HighPerformance.Collections; #endif using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.Versioning; namespace Misaki.HighPerformance.LowLevel.Buffer; /// /// Holds information about a memory allocation. /// public readonly struct AllocationInfo { /// /// Gets the address of the allocated memory block. /// public IntPtr Address { get; init; } /// /// Gets the newSize of the allocation in bytes. /// public nuint Size { get; init; } #if MHP_ENABLE_STACKTRACE /// /// Gets the stack trace at the time of allocation for debugging purposes. /// public StackTrace? StackTrace { get; init; } #endif } public readonly struct AllocationManagerInitOpts { public nuint ArenaCapacity { get; init; } public nuint StackCapacity { get; init; } public nuint FreeListChunkSize { get; init; } public nuint FreeListDefaultAlignment { get; init; } public int FreeListConcurrencyLevel { get; init; } public static AllocationManagerInitOpts Default => new AllocationManagerInitOpts { ArenaCapacity = 1024 * 1024 * 1024, // 1 GB StackCapacity = 16 * 1024 * 1024, // 16 MB per thread FreeListChunkSize = 64 * 1024 * 1024, FreeListDefaultAlignment = 8, FreeListConcurrencyLevel = Environment.ProcessorCount }; } /// /// Provides memory allocation management for native memory allocations, with support for tracking, /// debugging, and custom allocation strategies. /// public static unsafe class AllocationManager { internal struct HeapAllocator : IAllocator { private AllocationHandle _handle; public readonly AllocationHandle Handle => _handle; public void Init() { _handle = new AllocationHandle { State = null, Alloc = &Allocate, Realloc = &Reallocate, Free = &Free }; } private static void* Allocate(void* _, nuint size, nuint alignment, AllocationOption allocationOption) { var ptr = AlignedAlloc(size, alignment); if (ptr == null) { return null; } if (allocationOption.HasFlag(AllocationOption.Clear)) { MemClear(ptr, size); } return ptr; } private static void* Reallocate(void* _, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption) { var newPtr = AlignedRealloc(ptr, newSize, alignment); if (newPtr == null) { return null; } if (allocationOption.HasFlag(AllocationOption.Clear) && newSize > oldSize) { var offset = (byte*)newPtr + oldSize; var clearSize = newSize - oldSize; MemClear(offset, clearSize); } return newPtr; } private static void Free(void* _, void* ptr) { AlignedFree(ptr); } #if MHP_ENABLE_SAFETY_CHECKS private static bool IsValid(void* _, MemoryHandle handle) { return ContainsAllocation(handle); } #endif } internal static MemoryPool s_arenaAllocator; internal static MemoryPool s_freeListAllocator; internal static HeapAllocator* s_pHeapAllocator; [ThreadStatic] private static MemoryPool t_stackAllocator; #if MHP_ENABLE_SAFETY_CHECKS private static ConcurrentSlotMap s_allocations = null!; #endif /// /// Gets the number of live tracked heap allocations. Always returns 0 if MHP_ENABLE_SAFETY_CHECKS is disabled. /// public static int LiveAllocationCount => #if MHP_ENABLE_SAFETY_CHECKS s_allocations.Count; #else 0; #endif private static volatile bool s_initialized; private static nuint s_threadLocalStackSize; private static SpinLock s_stackLocker = new SpinLock(false); private static VirtualStack** s_ppStack; private static int s_ppStackCount; private static int s_ppStackCapacity; private static void EnsureThreadLocalStackInitialize() { if (Unsafe.IsNullRef(ref t_stackAllocator.Allocator)) { t_stackAllocator = new MemoryPool(new VirtualStack.CreationOptions { reserveCapacity = s_threadLocalStackSize }); var token = false; try { s_stackLocker.Enter(ref token); if (s_ppStack == null) { s_ppStack = (VirtualStack**)Malloc((nuint)(sizeof(VirtualStack*) * Environment.ProcessorCount)); s_ppStackCapacity = Environment.ProcessorCount; } if (s_ppStackCount >= s_ppStackCapacity) { var pOld = s_ppStack; var newCapacity = s_ppStackCapacity * 2; var pNew = (VirtualStack**)Realloc(pOld, (nuint)(sizeof(VirtualStack*) * newCapacity)); s_ppStack = pNew; s_ppStackCapacity = newCapacity; } s_ppStack[s_ppStackCount] = (VirtualStack*)Unsafe.AsPointer(ref t_stackAllocator.Allocator); var test = s_ppStack[s_ppStackCount]; s_ppStackCount++; } finally { if (token) { s_stackLocker.Exit(); } } } } public static void Initialize(AllocationManagerInitOpts opts) { if (s_initialized) { return; } #if MHP_ENABLE_SAFETY_CHECKS s_allocations = new ConcurrentSlotMap(256); #endif s_arenaAllocator = new MemoryPool(new VirtualArena.CreationOptions { reserveCapacity = opts.ArenaCapacity }); s_freeListAllocator = new MemoryPool(new FreeList.CreationOptions { alignment = opts.FreeListDefaultAlignment, chunkSize = opts.FreeListChunkSize, maxConcurrencyLevel = opts.FreeListConcurrencyLevel }); s_pHeapAllocator = (HeapAllocator*)Malloc((nuint)(sizeof(HeapAllocator))); s_pHeapAllocator->Init(); s_threadLocalStackSize = opts.StackCapacity; s_initialized = true; } /// /// Gets a reference to the allocation pHandle for the specified allocator type. /// /// The allocator type for which to retrieve the allocation pHandle. /// A reference to the allocation pHandle associated with the specified allocator type. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] [Obsolete("Use AllocationHandle instead.")] public static AllocationHandle GetAllocationHandle(Allocator allocator) { Debug.Assert(s_initialized, "AllocationManager is not initialized."); return allocator switch { Allocator.Temp => s_arenaAllocator.AllocationHandle, Allocator.Persistent => s_pHeapAllocator->Handle, Allocator.FreeList => s_freeListAllocator.AllocationHandle, _ => throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)), }; } /// /// Resets the temporary memory allocator, clearing all allocated memory. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ResetTempAllocator() { Debug.Assert(s_initialized, "AllocationManager is not initialized."); s_arenaAllocator.Allocator.Reset(); } /// /// Creates a new thread local stack scope for managing temporary allocations within the current context. /// /// A instance representing the newly created stack scope. The scope must be disposed when no longer needed to release allocated resources. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static VirtualStack.Scope CreateStackScope() { Debug.Assert(s_initialized, "AllocationManager is not initialized."); EnsureThreadLocalStackInitialize(); return t_stackAllocator.Allocator.CreateScope(t_stackAllocator.AllocationHandle); } /// /// Registers a memory allocation and returns a handle that can be used to manage or reference the allocated memory. /// /// /// Always returns an invalid handle if MHP_ENABLE_SAFETY_CHECKS is disabled. /// /// A pointer to the memory block to be registered. The pointer must reference a valid, allocated memory region. /// The newSize of the memory block to be registered. /// A MemoryHandle representing the registered allocation. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static MemoryHandle AddAllocation(void* ptr, nuint size) { #if MHP_ENABLE_SAFETY_CHECKS Debug.Assert(s_initialized, "AllocationManager is not initialized."); var info = new AllocationInfo { Address = (IntPtr)ptr, Size = size, #if MHP_ENABLE_STACKTRACE StackTrace = new StackTrace(1, true) #endif }; var id = s_allocations.Add(info, out var generation); return new MemoryHandle(id, generation); #else return MemoryHandle.Invalid; #endif } public static void UpdateAllocation(MemoryHandle handle, void* newPtr, nuint newSize) { #if MHP_ENABLE_SAFETY_CHECKS Debug.Assert(s_initialized, "AllocationManager is not initialized."); if (newPtr == null) { s_allocations.Remove(handle.ID, handle.Generation); } else { if (s_allocations.TryGetElement(handle.ID, handle.Generation, out var oldInfo)) { var newInfo = oldInfo with { Address = (IntPtr)newPtr, Size = newSize }; s_allocations.UpdateElement(handle.ID, handle.Generation, newInfo); } } #endif } /// /// Removes the memory allocation associated with the specified handle. /// /// /// Always returns false if debug layer is disabled. /// /// The handle representing the memory allocation to remove. The handle must be valid and previously allocated. /// true if the allocation was successfully removed; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool RemoveAllocation(MemoryHandle handle) { #if MHP_ENABLE_SAFETY_CHECKS Debug.Assert(s_initialized, "AllocationManager is not initialized."); return s_allocations.Remove(handle.ID, handle.Generation, out var info); #else return false; #endif } /// /// Attempts to retrieve the memory allocation pointer associated with the specified handle. /// /// /// Always returns false if debug layer is disabled, and the output pointer will be set to . /// /// The memory handle identifying the allocation to retrieve allocation. /// When this method returns, contains the allocation information associated with the specified handle, if the allocation was found; otherwise, an uninitialized value. /// true if the allocation was found and contains valid allocation information; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryGetAllocation(MemoryHandle handle, out AllocationInfo info) { #if MHP_ENABLE_SAFETY_CHECKS Debug.Assert(s_initialized, "AllocationManager is not initialized."); return s_allocations.TryGetElement(handle.ID, handle.Generation, out info); #else info = default; return false; #endif } /// /// Determines whether the specified memory handle refers to a currently tracked allocation. /// /// /// This only validates the memory when you added the allocation via . /// For validating memory from , use instead. /// Always returns false if debug layer is disabled. /// /// The memory handle to check for an associated allocation. /// true if the allocation corresponding to the handle exists; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ContainsAllocation(MemoryHandle handle) { #if MHP_ENABLE_SAFETY_CHECKS Debug.Assert(s_initialized, "AllocationManager is not initialized."); return s_allocations.Contains(handle.ID, handle.Generation); #else return false; #endif } /// /// Gets the total newSize of all currently tracked allocations. /// /// /// Always returns 0 if MHP_ENABLE_SAFETY_CHECKS is disabled. /// /// The total newSize of all currently tracked allocations. public static nuint GetTotalAllocatedMemory() { #if MHP_ENABLE_SAFETY_CHECKS Debug.Assert(s_initialized, "AllocationManager is not initialized."); nuint total = 0; foreach (var allocation in s_allocations) { total += allocation.Size; } return total; #else return 0; #endif } /// /// Disposes of the AllocationManager, freeing all allocated memory and resources. /// public static void Dispose() { if (!s_initialized) { return; } s_initialized = false; #if MHP_ENABLE_SAFETY_CHECKS if (s_allocations.Count > 0) { throw new MemoryLeakException(s_allocations); } #endif s_arenaAllocator.Dispose(); s_freeListAllocator.Dispose(); if (s_ppStack != null) { for (var i = 0; i < s_ppStackCount; i++) { var pStack = s_ppStack[i]; if (pStack != null) { pStack->Dispose(); } } Free(s_ppStack); s_ppStack = null; } if (s_pHeapAllocator != null) { Free(s_pHeapAllocator); s_pHeapAllocator = null; } } }