Fix source only package issue in nuget

This commit is contained in:
2026-02-22 12:36:51 +09:00
parent 49dc44605b
commit ffac1c643e
35 changed files with 69 additions and 8 deletions

View File

@@ -1,713 +0,0 @@
using Misaki.HighPerformance.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// Holds information about a memory allocation.
/// </summary>
public readonly struct AllocationInfo
{
/// <summary>
/// Get the size of the allocation in bytes.
/// </summary>
public nuint Size
{
get; init;
}
/// <summary>
/// Get the stack trace at the time of allocation for debugging purposes.
/// </summary>
public StackTrace StackTrace
{
get; init;
}
}
/// <summary>
/// Provides memory allocation management for native memory allocations, with support for tracking,
/// debugging, and custom allocation strategies.
/// </summary>
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 GCHandle stackHandle; // GCHandle to managed StackTrace
}
private struct ArenaAllocator : IAllocator, IDisposable
{
private const int _ARENA_MAGIC_ID = -3941029;
private DynamicArena _arena;
private AllocationHandle _handle;
private int _currentTick;
public readonly AllocationHandle Handle => _handle;
public readonly int CurrentTick => _currentTick;
public void Init(uint initialSize)
{
_arena = new DynamicArena(initialSize);
_handle = new AllocationHandle
{
State = Unsafe.AsPointer(ref this),
Alloc = &Allocate,
Realloc = &Reallocate,
Free = null,
IsValid = &IsValid
};
_currentTick = 0;
}
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{
var selfPtr = (ArenaAllocator*)instance;
var ptr = selfPtr->_arena.Allocate(size, alignment, allocationOption);
if (ptr == null)
{
*pHandle = MemoryHandle.Invalid;
return null;
}
*pHandle = new MemoryHandle(_ARENA_MAGIC_ID, selfPtr->_currentTick);
return ptr;
}
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{
if (ptr == null)
{
return Allocate(instance, newSize, alignment, allocationOption, pHandle);
}
var selfPtr = (ArenaAllocator*)instance;
var newPtr = selfPtr->_arena.Allocate(newSize, alignment, allocationOption);
if (newPtr == null)
{
return null;
}
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
*pHandle = new MemoryHandle(_ARENA_MAGIC_ID, selfPtr->_currentTick);
return newPtr;
}
private static bool IsValid(void* instance, MemoryHandle handle)
{
var selfPtr = (ArenaAllocator*)instance;
return handle.id == _ARENA_MAGIC_ID && handle.generation == selfPtr->_currentTick;
}
public void Reset()
{
_arena.Reset();
_currentTick++;
}
public void Dispose()
{
_arena.Dispose();
}
}
private 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,
IsValid = &IsValid
};
}
private static void* Allocate(void* _, nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{
return HeapAlloc(size, alignment, allocationOption, pHandle);
}
private static void* Reallocate(void* _, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{
if (ptr == null)
{
return Allocate(null, newSize, alignment, allocationOption, pHandle);
}
MemoryHandle newHandle;
var newPtr = HeapAlloc(newSize, alignment, allocationOption, &newHandle);
if (newPtr == null)
{
return null;
}
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
HeapFree(ptr, *pHandle);
*pHandle = newHandle;
return newPtr;
}
private static void Free(void* _, void* ptr, MemoryHandle handle)
{
HeapFree(ptr, handle);
}
private static bool IsValid(void* _, MemoryHandle handle)
{
return ContainsAllocation(handle);
}
}
private struct StackAllocator : IAllocator
{
private const int _STACK_MAGIC_ID = -6843541;
[ThreadStatic]
private static Stack s_stack;
private AllocationHandle _handle;
public readonly AllocationHandle Handle => _handle;
public void Init()
{
_handle = new AllocationHandle
{
State = Unsafe.AsPointer(ref this),
Alloc = &Allocate,
Realloc = &Reallocate,
Free = null,
IsValid = &IsValid
};
}
private static void* Allocate(void* instance, nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{
var ptr = s_stack.Allocate(size, alignment, allocationOption);
if (ptr == null)
{
*pHandle = MemoryHandle.Invalid;
return null;
}
*pHandle = new MemoryHandle(_STACK_MAGIC_ID, (int)s_stack.Offset);
return ptr;
}
private static void* Reallocate(void* instance, void* ptr, nuint oldSize, nuint newSize, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{
if (ptr == null)
{
return Allocate(instance, newSize, alignment, allocationOption, pHandle);
}
// Optimize for last allocation. Set offset directly.
var oldBase = s_stack.Buffer + s_stack.Offset - oldSize;
if (ptr == oldBase)
{
if (newSize > oldSize)
{
var diff = newSize - oldSize;
s_stack.Offset += diff;
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(s_stack.Buffer + s_stack.Offset - diff, diff);
}
}
*pHandle = new MemoryHandle(_STACK_MAGIC_ID, (int)s_stack.Offset);
return ptr;
}
var newPtr = s_stack.Allocate(newSize, alignment, allocationOption);
if (newPtr == null)
{
return null;
}
MemCpy(newPtr, ptr, Math.Min(oldSize, newSize));
*pHandle = new MemoryHandle(_STACK_MAGIC_ID, (int)s_stack.Offset);
return newPtr;
}
private static bool IsValid(void* instance, MemoryHandle handle)
{
return handle.id == _STACK_MAGIC_ID && handle.generation <= (int)s_stack.Offset;
}
public static Stack.Scope CreateScope(StackAllocator* pSelf)
{
return s_stack.CreateScope(pSelf->_handle);
}
}
private const uint _DEFAULT_MEMORY_POOL_SIZE = 1024 * 1024; // 1 MB
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 bool s_disposed;
private static AllocationHeader* s_pLiveHead;
private static SpinLock s_liveLock;
private static readonly ConcurrentSlotMap<IntPtr> s_allocations;
public static readonly MemoryHandle MagicHandle = new MemoryHandle(int.MinValue, int.MinValue);
/// <summary>
/// Gets the number of live persistent heap allocations when the debug layer is disabled.
/// </summary>
public static int LiveAllocationCount => s_allocations.Count;
/// <summary>
/// Gets a value indicating whether the debug layer is currently enabled.
/// </summary>
public static bool IsDebugLayerEnabled => s_debugLayer;
static AllocationManager()
{
var allocatorTotalSize = (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator) + sizeof(StackAllocator));
var basePtr = NativeMemory.Alloc(allocatorTotalSize);
s_pArenaAllocator = (ArenaAllocator*)basePtr;
s_pHeapAllocator = (HeapAllocator*)((byte*)basePtr + (nuint)sizeof(ArenaAllocator));
s_pStackAllocator = (StackAllocator*)((byte*)basePtr + (nuint)(sizeof(ArenaAllocator) + sizeof(HeapAllocator)));
s_liveLock = new SpinLock(false);
s_allocations = new ConcurrentSlotMap<IntPtr>(256);
s_pArenaAllocator->Init(_DEFAULT_MEMORY_POOL_SIZE);
s_pHeapAllocator->Init();
s_pStackAllocator->Init();
s_pLiveHead = null;
}
[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) => header->stackHandle;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void HeaderSetHandle(AllocationHeader* header, GCHandle handle) => header->stackHandle = handle;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void HeaderFreeHandle(AllocationHeader* header)
{
if (header->stackHandle.IsAllocated)
{
header->stackHandle.Free();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void LinkHeader(AllocationHeader* header)
{
var taken = false;
try
{
s_liveLock.Enter(ref taken);
header->prev = null;
header->next = s_pLiveHead;
if (s_pLiveHead != null)
{
s_pLiveHead->prev = header;
}
s_pLiveHead = 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_pLiveHead = 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(newUser + oldSize, newSize - oldSize);
}
// Unlink and free the old block (without freeing the StackTrace pHandle again)
//oldHeader->stackHandle = GCHandle.FromIntPtr(0);
UnlinkHeader(oldHeader);
AlignedFree(oldHeader->basePtr);
return newUser;
}
/// <summary>
/// Enables the debug layer, allowing additional diagnostic information to be collected.
/// </summary>
[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 (s_allocations.Count != 0)
{
throw new InvalidOperationException("EnableDebugLayer must be called before any allocations are active.");
}
s_debugLayer = true;
}
/// <summary>
/// Gets a reference to the allocation pHandle for the specified allocator type.
/// </summary>
/// <param name="allocator">The allocator type for which to retrieve the allocation pHandle.</param>
/// <returns>A reference to the allocation pHandle associated with the specified allocator type.</returns>
/// <exception cref="ArgumentException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static AllocationHandle GetAllocationHandle(Allocator allocator)
{
return allocator switch
{
Allocator.Temp => s_pArenaAllocator->Handle,
Allocator.Persistent => s_pHeapAllocator->Handle,
_ => throw new ArgumentException("Target allocator type does not support custom allocation.", nameof(allocator)),
};
}
/// <summary>
/// Allocates a block of memory from the heap with the specified size and alignment, using the given allocation options.
/// </summary>
/// <remarks>
/// This will allocate memory from the heap. If the debug layer is enabled, additional tracking information will be recorded.
/// The memory handle is always tracked unless the <see cref="AllocationOption.Untrack"/> flag is specified.
/// </remarks>
/// <param name="size">The number of bytes to allocate. Must be greater than zero.</param>
/// <param name="alignment">The alignment, in bytes, for the allocated memory block. Must be a power of two.</param>
/// <param name="allocationOption">An optional set of flags that control allocation behavior, such as whether the memory should be cleared or
/// tracked. The default is <see cref="AllocationOption.None"/>.</param>
/// <returns>A pointer to the beginning of the allocated memory block.</returns>
/// <exception cref="OutOfMemoryException">Thrown if the allocation fails.</exception>
public static void* HeapAlloc(nuint size, nuint alignment, AllocationOption allocationOption, MemoryHandle* pHandle)
{
var isUntrack = allocationOption.HasFlag(AllocationOption.Untrack);
void* ptr;
if (s_debugLayer && !isUntrack)
{
ptr = DebugAllocate(size, alignment);
}
else
{
ptr = AlignedAlloc(size, alignment);
}
if (ptr == null)
{
*pHandle = MemoryHandle.Invalid;
return null;
}
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(ptr, size);
}
if (isUntrack)
{
*pHandle = MagicHandle;
}
else
{
*pHandle = AddAllocation((IntPtr)ptr);
}
return ptr;
}
/// <summary>
/// Releases a block of unmanaged memory previously allocated by the heap allocator.
/// </summary>
/// <param name="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.</param>
/// <param name="handle">The handle representing the memory allocation to free. The handle must be valid and previously allocated.</param>
public static void HeapFree(void* ptr, MemoryHandle handle)
{
if (s_debugLayer && handle != MagicHandle)
{
DebugFree(ptr);
}
else
{
AlignedFree(ptr);
}
RemoveAllocation(handle);
}
/// <summary>
/// Releases a block of unmanaged memory previously allocated by the heap allocator.
/// </summary>
/// <param name="handle">The handle representing the memory allocation to free. The handle must be valid and previously allocated.</param>
public static void HeapFree(MemoryHandle handle)
{
if (TryGetAllocation(handle, out var ptr))
{
HeapFree((void*)ptr, handle);
}
}
/// <summary>
/// Resets the temporary memory allocator, clearing all allocated memory.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ResetTempAllocator()
{
s_pArenaAllocator->Reset();
}
/// <summary>
/// Creates a new thread local stack scope for managing temporary allocations within the current context.
/// </summary>
/// <returns>A <see cref="Stack.Scope"/> instance representing the newly created stack scope. The scope must be disposed when no longer needed to release allocated resources.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Stack.Scope CreateStackScope()
{
return StackAllocator.CreateScope(s_pStackAllocator);
}
/// <summary>
/// Registers a memory allocation and returns a handle that can be used to manage or reference the allocated memory.
/// </summary>
/// <param name="ptr">A pointer to the memory block to be registered. The pointer must reference a valid, allocated memory region.</param>
/// <returns>A MemoryHandle representing the registered allocation.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryHandle AddAllocation(IntPtr ptr)
{
var id = s_allocations.Add(ptr, out var generation);
return new MemoryHandle(id, generation);
}
/// <summary>
/// Removes the memory allocation associated with the specified handle.
/// </summary>
/// <param name="handle">The handle representing the memory allocation to remove. The handle must be valid and previously allocated.</param>
/// <returns>true if the allocation was successfully removed; otherwise, false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool RemoveAllocation(MemoryHandle handle)
{
return s_allocations.Remove(handle.id, handle.generation);
}
/// <summary>
/// Attempts to retrieve the memory allocation pointer associated with the specified handle.
/// </summary>
/// <param name="handle">The memory handle identifying the allocation to retrieve allocation.</param>
/// <param name="ptr">When this method returns, contains the pointer to the memory allocation if found; otherwise, <see cref="IntPtr.Zero"/>.</param>
/// <returns>true if the allocation was found and <paramref name="ptr"/> contains a valid pointer; otherwise, false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryGetAllocation(MemoryHandle handle, out IntPtr ptr)
{
return s_allocations.TryGetElement(handle.id, handle.generation, out ptr);
}
/// <summary>
/// Determines whether the specified memory handle refers to a currently tracked allocation.
/// </summary>
/// <remarks>
/// This only validates the memory when you added the allocation via <see cref="AddAllocation(IntPtr)"/>.
/// For validating memory from <see cref="AllocationHandle"/>, use <see cref="AllocationHandle.IsValid"/> instead.
/// </remarks>
/// <param name="handle">The memory handle to check for an associated allocation.</param>
/// <returns>true if the allocation corresponding to the handle exists; otherwise, false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ContainsAllocation(MemoryHandle handle)
{
if (handle == MagicHandle)
{
return true;
}
return s_allocations.Contains(handle.id, handle.generation);
}
/// <summary>
/// Disposes of the AllocationManager, freeing all allocated memory and resources.
/// </summary>
public static void Dispose()
{
if (s_disposed)
{
return;
}
// In debug mode, walk the intrusive list to surface any leaks.
if (s_debugLayer)
{
var snapshot = new List<AllocationInfo>();
var taken = false;
try
{
s_liveLock.Enter(ref taken);
if (s_pLiveHead != null)
{
snapshot.Capacity = 128;
for (var p = s_pLiveHead; 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(CollectionsMarshal.AsSpan(snapshot));
}
}
if (LiveAllocationCount != 0)
{
throw new MemoryLeakException($"Found {LiveAllocationCount} memory lakes! Please enable debug layer for more informations.");
}
// NOTE: Arena allocator holds the base ptr for all allocators, heap and stack allocators do not own any memory themselves.
if (s_pArenaAllocator != null)
{
s_pArenaAllocator->Dispose();
Stack.DisposeAll();
NativeMemory.Free(s_pArenaAllocator);
}
s_disposed = true;
}
}

View File

@@ -1,32 +0,0 @@
namespace Misaki.HighPerformance.LowLevel.Buffer;
[Flags]
public enum AllocationOption : byte
{
/// <summary>
/// Default allocation option. Values are uninitialized.
/// </summary>
None = 0,
/// <summary>
/// Clear the memory to zero upon allocation.
/// </summary>
Clear = 1 << 0,
/// <summary>
/// Specify that this memory allocation should not been tracked competly, which <see cref="AllocationManager"/> will not perform any safty check like use after free and leack detection.
/// </summary>
Untrack = 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 automatically released after use automatically.
/// </summary>
Temp,
/// <summary>
/// Allocator for persistent allocations. Allocations are not automatically released after use.
/// </summary>
Persistent,
}

View File

@@ -1,109 +0,0 @@
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// A memory management structure that allocates and resets memory blocks with specified alignment.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 64)] // Cache line aligned to prevent false sharing
public unsafe struct Arena : IDisposable
{
[FieldOffset(0)]
private byte* _buffer;
[FieldOffset(8)]
private nuint _size;
[FieldOffset(16)]
private nuint _offset;
public Arena(nuint size)
{
if (_buffer != null)
{
return;
}
_buffer = (byte*)Malloc(size);
_size = size;
_offset = 0;
}
/// <summary>
/// Allocates a block of memory of a specified size with a given alignment. Returns a pointer to the allocated
/// memory or null if allocation fails.
/// You don't need to free the memory manually, it will be freed when the arena is disposed.
/// </summary>
/// <param name="size">Specifies the amount of memory to allocate in bytes.</param>
/// <param name="alignment">Defines the alignment requirement for the allocated memory.</param>
/// <param name="allocationOption">The option when allocating memory.</param>
/// <returns>A pointer to the allocated memory block or null if the allocation cannot be fulfilled.</returns>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption)
{
if (_buffer == null)
{
throw new ObjectDisposedException(nameof(Arena));
}
if (size == 0)
{
return null;
}
if ((alignment & (alignment - 1)) != 0)
{
throw new ArgumentException("Alignment must be a power of two.", nameof(alignment));
}
nuint currentOffset, newOffset, alignedOffset;
do
{
currentOffset = _offset;
alignedOffset = (currentOffset + alignment - 1) & ~(alignment - 1);
newOffset = alignedOffset + size;
if (newOffset > _size)
{
return null;
}
} while (Interlocked.CompareExchange(ref _offset, newOffset, currentOffset) != currentOffset);
var ptr = _buffer + alignedOffset;
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(ptr, size);
}
return ptr;
}
/// <summary>
/// Resets the arena, optionally clearing the allocated memory.
/// </summary>
/// <param name="clear">If true, the allocated memory will be cleared; otherwise, it will not be cleared.</param>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void Reset()
{
if (_buffer == null)
{
throw new ObjectDisposedException(nameof(DynamicArena));
}
_offset = 0;
}
public void Dispose()
{
if (_buffer == null)
{
return;
}
Free(_buffer);
_buffer = null;
_size = 0;
_offset = 0;
}
}

View File

@@ -1,170 +0,0 @@
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// A dynamic memory management structure that automatically grows by creating linked arenas
/// when more space is needed.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 128)]
public unsafe struct DynamicArena : IDisposable
{
[StructLayout(LayoutKind.Sequential)]
private struct ArenaNode
{
public Arena arena;
public ArenaNode* next;
}
[FieldOffset(0)]
private ArenaNode* _root;
[FieldOffset(8)]
private ArenaNode* _current;
[FieldOffset(16)]
private uint _initialSize;
[FieldOffset(20)]
private volatile int _nodeCreationLock;
/// <summary>
/// Initializes a new instance of DynamicArena with the specified initial size.
/// </summary>
/// <param name="initialSize">The initial size in bytes for the first arena block.</param>
public DynamicArena(uint initialSize)
{
Initialize(initialSize);
}
public void Initialize(uint initialSize)
{
if (_root != null)
{
return;
}
_initialSize = initialSize;
_root = (ArenaNode*)Malloc(SizeOf<ArenaNode>());
_root->arena = new Arena(initialSize);
_root->next = null;
_current = _root;
_nodeCreationLock = 0;
}
private bool TryCreateNewNode(nuint size)
{
while (Interlocked.CompareExchange(ref _nodeCreationLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
var current = _current;
if (current->next != null)
{
// Another thread created a node while we were waiting
_current = current->next;
return true;
}
var newNode = (ArenaNode*)Malloc(SizeOf<ArenaNode>());
try
{
newNode->arena = new Arena(size);
newNode->next = null;
// Atomically link the new node
current->next = newNode;
// Update current pointer
_current = newNode;
return true;
}
catch
{
Free(newNode);
return false;
}
}
finally
{
// Release the spinlock
Interlocked.Exchange(ref _nodeCreationLock, 0);
}
}
/// <summary>
/// Allocates a block of memory with specified size and alignment. Creates a new arena if current one is full.
/// </summary>
/// <param name="size">Size of the memory block to allocate in bytes.</param>
/// <param name="alignment">Alignment requirement for the memory block.</param>
/// <returns>Pointer to the allocated memory block.</returns>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption)
{
if (_root == null)
{
return null;
}
void* result = null;
var current = _current;
while (current != null)
{
result = current->arena.Allocate(size, alignment, allocationOption);
if (result != null)
{
return result;
}
if (current->next == null && !TryCreateNewNode(Math.Max(size, _initialSize)))
{
return null;
}
current = current->next;
}
_current = current;
return result;
}
/// <summary>
/// Resets all arenas in the chain, optionally clearing their memory.
/// </summary>
/// <param name="clear">If true, memory will be cleared during reset.</param>
/// <exception cref="ObjectDisposedException">Thrown if the arena has been disposed.</exception>
public void Reset()
{
var current = _root;
while (current != null)
{
current->arena.Reset();
current = current->next;
}
_current = _root;
}
public void Dispose()
{
if (_root == null)
{
return;
}
var current = _root;
while (current != null)
{
var next = current->next;
current->arena.Dispose();
Free(current);
current = next;
}
_root = null;
_current = null;
}
}

View File

@@ -1,490 +0,0 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// A lock-free, thread-safe variable-size allocator that manages memory blocks of different sizes.
/// Optimized for high-performance scenarios with frequent allocations and deallocations.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = 256)] // Cache line aligned to prevent false sharing
public unsafe struct FreeList : IDisposable
{
[StructLayout(LayoutKind.Sequential)]
private struct FreeNode
{
public FreeNode* next;
public nuint size;
}
[StructLayout(LayoutKind.Sequential)]
private struct MemoryChunk
{
public MemoryChunk* next;
public byte* memory;
public nuint size;
public nuint used; // Amount of memory used in this chunk
}
[StructLayout(LayoutKind.Explicit, Size = 32)]
private struct SizeBucket
{
[FieldOffset(0)]
public long freeCount; // Number of free blocks
[FieldOffset(8)]
public nint freeHead; // Free list head for this size
[FieldOffset(16)]
public nuint blockSize; // Fixed size for this bucket
[FieldOffset(24)]
public int creationLock;
}
[StructLayout(LayoutKind.Explicit, Size = 24)]
private struct BlockHeader
{
// Ensure the size is fixed across x86 and x64
[FieldOffset(0)]
public MemoryChunk* ownerChunk;
[FieldOffset(8)]
public nuint blockSize;
[FieldOffset(16)]
public ulong magicNumber;
}
private const int _MAX_BUCKETS = 16; // Number of size buckets
private const nuint _MIN_BLOCK_SIZE = 16; // Minimum block size
private const nuint _DEFAULT_CHUNK_SIZE = 64 * 1024; // 64KB chunks
private const ulong _MAGIC_NUMBER = 0xDEADBEEFDEADBEEF; // For validating blocks
[FieldOffset(0)]
private fixed byte _buckets[_MAX_BUCKETS * 32]; // SizeBucket array (32 bytes per bucket)
[FieldOffset(512)]
private DynamicArena _chunkArena; // 128
[FieldOffset(640)]
private MemoryChunk* _chunks; // 8
[FieldOffset(648)]
private readonly nuint _chunkSize; // 8
[FieldOffset(656)]
private readonly nuint _alignment; // 8
[FieldOffset(664)]
private long _totalAllocatedBytes; // 8
[FieldOffset(672)]
private long _totalFreeBytes; // 8
[FieldOffset(676)]
private volatile int _disposed; // 4
[FieldOffset(680)]
private volatile int _chunkCreationLock; // 4
/// <summary>
/// Gets the alignment requirement for allocations.
/// </summary>
public readonly nuint Alignment => _alignment;
/// <summary>
/// Gets the total number of allocated bytes.
/// </summary>
public readonly long TotalAllocatedBytes => Interlocked.Read(ref Unsafe.AsRef(in _totalAllocatedBytes));
/// <summary>
/// Gets the total number of free bytes available.
/// </summary>
public readonly long TotalFreeBytes => Interlocked.Read(ref Unsafe.AsRef(in _totalFreeBytes));
/// <summary>
/// Gets whether the allocator has been disposed.
/// </summary>
public readonly bool IsDisposed => _disposed != 0;
/// <summary>
/// Gets the chunk size used by this allocator.
/// </summary>
public readonly nuint ChunkSize => _chunkSize;
/// <summary>
/// Initializes a new variable-size FreeList allocator with the specified parameters.
/// </summary>
/// <param name="alignment">Alignment requirement for blocks (must be power of 2).</param>
/// <param name="chunkSize">Size of memory chunks to allocate (default: 64KB).</param>
public FreeList(nuint alignment, nuint chunkSize = _DEFAULT_CHUNK_SIZE)
{
if (alignment == 0 || (alignment & (alignment - 1)) != 0)
{
throw new ArgumentException("Alignment must be a power of 2", nameof(alignment));
}
if (chunkSize < 1024)
{
throw new ArgumentException("Chunk size must be at least 1KB", nameof(chunkSize));
}
_alignment = alignment;
_chunkSize = chunkSize;
_chunks = null;
_totalAllocatedBytes = 0;
_totalFreeBytes = 0;
_disposed = 0;
_chunkCreationLock = 0;
_chunkArena = new DynamicArena(1024);
InitializeBuckets();
}
private readonly void InitializeBuckets()
{
var buckets = GetBuckets();
var size = _MIN_BLOCK_SIZE;
for (var i = 0; i < _MAX_BUCKETS; i++)
{
buckets[i].blockSize = size;
buckets[i].freeHead = 0;
buckets[i].freeCount = 0;
size *= 2; // Exponential size increase
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly SizeBucket* GetBuckets()
{
fixed (byte* ptr = _buckets)
{
return (SizeBucket*)ptr;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly int FindBucket(nuint size)
{
var buckets = GetBuckets();
for (var i = 0; i < _MAX_BUCKETS; i++)
{
if (size <= buckets[i].blockSize)
{
return i;
}
}
return -1; // Size too large for buckets
}
/// <summary>
/// Allocates a memory block of the specified size. Thread-safe using lock-free algorithms.
/// </summary>
/// <param name="size">Size of memory to allocate in bytes.</param>
/// <param name="alignment">Alignment requirement (0 = use default).</param>
/// <param name="allocationOption">Options for allocation (e.g., clear memory).</param>
/// <returns>MemoryBlock containing allocated memory, or Invalid if allocation fails.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{
if (_disposed != 0)
{
throw new ObjectDisposedException(nameof(FreeList));
}
if (size == 0)
{
return null;
}
if (alignment == 0)
{
alignment = _alignment;
}
if ((alignment & (alignment - 1)) != 0)
{
throw new ArgumentException("Alignment must be a power of two.", nameof(alignment));
}
// Align size to alignment boundary
var alignedSize = (size + alignment - 1) & ~(alignment - 1);
alignedSize = Math.Max(alignedSize, _MIN_BLOCK_SIZE);
var totalSize = alignedSize + (nuint)sizeof(BlockHeader);
var bucketIndex = FindBucket(totalSize);
void* ptr = null;
if (bucketIndex >= 0)
{
// Try to allocate from bucket
ptr = TryPopFromBucket(bucketIndex);
if (ptr == null)
{
// Create new blocks for this bucket
if (TryCreateBlocksForBucket(bucketIndex))
{
ptr = TryPopFromBucket(bucketIndex);
}
}
}
if (ptr == null)
{
// Fallback to direct allocation from chunk
ptr = AllocateFromChunk(totalSize, alignment);
}
if (ptr != null)
{
var header = (BlockHeader*)ptr;
Interlocked.Add(ref _totalAllocatedBytes, (long)header->blockSize);
var pUserData = (byte*)ptr + sizeof(BlockHeader);
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(pUserData, alignedSize);
}
return pUserData;
}
return null;
}
/// <summary>
/// Frees a previously allocated memory block. Thread-safe using lock-free algorithms.
/// </summary>
/// <param name="block">MemoryBlock to free.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Free(void* ptr)
{
if (_disposed != 0 || ptr == null)
{
return;
}
var blockStartPtr = (byte*)ptr - sizeof(BlockHeader);
var header = (BlockHeader*)blockStartPtr;
if (header->magicNumber != _MAGIC_NUMBER)
{
return;
}
var chuck = header->ownerChunk;
if (chuck == null)
{
return;
}
var bucketIndex = FindBucket(header->blockSize);
if (bucketIndex >= 0)
{
PushToBucket(bucketIndex, blockStartPtr, header->blockSize);
}
Interlocked.Add(ref _totalAllocatedBytes, -(long)header->blockSize);
header->ownerChunk = null;
header->blockSize = 0;
header->magicNumber = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly void* TryPopFromBucket(int bucketIndex)
{
var buckets = GetBuckets();
var bucket = &buckets[bucketIndex];
nint head, newHead;
FreeNode* headPtr;
do
{
head = bucket->freeHead;
if (head == 0)
{
return null;
}
headPtr = (FreeNode*)head;
newHead = (nint)headPtr->next;
} while (Interlocked.CompareExchange(ref bucket->freeHead, newHead, head) != head);
Interlocked.Decrement(ref bucket->freeCount);
return (void*)head;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private readonly void PushToBucket(int bucketIndex, void* ptr, nuint size)
{
var buckets = GetBuckets();
var bucket = &buckets[bucketIndex];
var node = (FreeNode*)ptr;
node->size = size;
nint head;
do
{
head = bucket->freeHead;
node->next = (FreeNode*)head;
} while (Interlocked.CompareExchange(ref bucket->freeHead, (nint)node, head) != head);
Interlocked.Increment(ref bucket->freeCount);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void AssignBlockHeader(BlockHeader* header, MemoryChunk* ownerChunk, nuint blockSize)
{
header->ownerChunk = ownerChunk;
header->blockSize = blockSize;
header->magicNumber = _MAGIC_NUMBER;
}
private bool TryCreateBlocksForBucket(int bucketIndex)
{
var buckets = GetBuckets();
var bucket = &buckets[bucketIndex];
while (Interlocked.CompareExchange(ref bucket->creationLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
if (bucket->freeHead != 0)
{
return true; // Another thread did the work for us!
}
var blockSize = bucket->blockSize;
var blocksToCreate = Math.Min(_chunkSize / blockSize, 256); // Limit number of blocks
if (blocksToCreate == 0)
{
return false;
}
var totalSize = blocksToCreate * blockSize;
var memory = (byte*)AlignedAlloc(totalSize, _alignment);
if (memory == null)
{
return false;
}
var chunk = (MemoryChunk*)_chunkArena.Allocate(SizeOf<MemoryChunk>(), AlignOf<MemoryChunk>(), AllocationOption.None);
if (chunk == null)
{
AlignedFree(memory);
return false;
}
chunk->memory = memory;
chunk->size = totalSize;
chunk->used = totalSize;
chunk->next = _chunks;
_chunks = chunk;
// Add all blocks to the bucket's free list
for (nuint i = 0; i < blocksToCreate; i++)
{
var blockStartPtr = memory + (i * blockSize);
AssignBlockHeader((BlockHeader*)blockStartPtr, chunk, blockSize);
PushToBucket(bucketIndex, blockStartPtr, blockSize);
}
return true;
}
finally
{
Interlocked.Exchange(ref bucket->creationLock, 0);
}
}
private void* AllocateFromChunk(nuint size, nuint alignment)
{
while (Interlocked.CompareExchange(ref _chunkCreationLock, 1, 0) != 0)
{
Thread.SpinWait(1);
}
try
{
// Try to find space in existing chunks first
var chunk = _chunks;
while (chunk != null)
{
var available = chunk->size - chunk->used;
var alignedOffset = (chunk->used + alignment - 1) & ~(alignment - 1);
var totalNeeded = alignedOffset - chunk->used + size;
if (totalNeeded <= available)
{
var blockStartPtr = chunk->memory + alignedOffset;
// Write the header and return the pointer WITH the header
AssignBlockHeader((BlockHeader*)blockStartPtr, chunk, size);
return blockStartPtr;
}
chunk = chunk->next;
}
// Create new chunk
var newChunkSize = Math.Max(_chunkSize, size + alignment);
var newMemory = (byte*)AlignedAlloc(newChunkSize, alignment);
if (newMemory == null)
{
return null;
}
var newChunk = (MemoryChunk*)_chunkArena.Allocate(SizeOf<MemoryChunk>(), AlignOf<MemoryChunk>(), AllocationOption.None);
if (newChunk == null)
{
AlignedFree(newMemory);
return null;
}
newChunk->memory = newMemory;
newChunk->size = newChunkSize;
newChunk->used = size;
newChunk->next = _chunks;
_chunks = newChunk;
// Write the header and return the pointer WITH the header
AssignBlockHeader((BlockHeader*)newMemory, newChunk, size);
return newMemory;
}
finally
{
Interlocked.Exchange(ref _chunkCreationLock, 0);
}
}
public void Dispose()
{
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0)
{
// Free all memory chunks
var chunk = _chunks;
while (chunk != null)
{
var next = chunk->next;
AlignedFree(chunk->memory);
chunk = next;
}
_chunkArena.Dispose();
_chunks = null;
_totalAllocatedBytes = 0;
_totalFreeBytes = 0;
}
}
}

View File

@@ -1,111 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace Misaki.HighPerformance.LowLevel.Buffer;
public readonly struct MemoryHandle : IEquatable<MemoryHandle>
{
public readonly int id;
public readonly int generation;
public readonly static MemoryHandle Invalid = new(-1, -1);
public MemoryHandle(int id, int generation)
{
this.id = id;
this.generation = generation;
}
public bool Equals(MemoryHandle other)
{
return id == other.id && generation == other.generation;
}
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is MemoryHandle other && Equals(other);
}
public override int GetHashCode()
{
return id ^ generation;
}
public override string? ToString()
{
return $"MemoryHandle(Id: {id}, Generation: {generation})";
}
public static bool operator ==(MemoryHandle left, MemoryHandle right)
{
return left.Equals(right);
}
public static bool operator !=(MemoryHandle left, MemoryHandle right)
{
return !(left == right);
}
}
/// <summary>
/// A structure that encapsulates function pointers for memory allocation operations.
/// </summary>
public readonly unsafe struct AllocationHandle
{
/// <summary>
/// Gets a pointer to the state instance associated with this allocation handle.
/// </summary>
public void* State
{
get; init;
}
/// <summary>
/// Gets a function pointer for allocating memory.
/// </summary>
public AllocFunc Alloc
{
get; init;
}
/// <summary>
/// Gets a function pointer for reallocating memory.
/// </summary>
public ReallocFunc Realloc
{
get; init;
}
/// <summary>
/// Gets a function pointer for freeing allocated memory.
/// </summary>
public FreeFunc Free
{
get; init;
}
/// <summary>
/// Gets a function pointer for validating a memory handle.
/// </summary>
public IsValidFunc IsValid
{
get; init;
}
}
/// <summary>
/// Represents an state interface for managing memory allocations.
/// </summary>
/// <remarks>
/// The state must be pined to a specific memory region.
/// Otherwise the reference of the <see cref="AllocationHandle.State"/>, may become invalid and lead to undefined behavior.
/// </remarks>
public interface IAllocator
{
/// <summary>
/// Gets a reference to the allocation handle associated with this state.
/// </summary>
AllocationHandle Handle
{
get;
}
}

View File

@@ -1,77 +0,0 @@
using System.Runtime.InteropServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
/// <summary>
/// Represents an allocated memory block with metadata.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public readonly unsafe struct MemoryBlock
{
/// <summary>
/// Pointer to the actual allocated memory.
/// </summary>
public void* Ptr
{
get;
}
/// <summary>
/// The heap from which the memory was allocated.
/// </summary>
public void* Heap
{
get;
}
/// <summary>
/// Size of the allocated memory in bytes.
/// </summary>
public nuint Size
{
get;
}
/// <summary>
/// Alignment of the allocated memory.
/// </summary>
public nuint Alignment
{
get;
}
/// <summary>
/// Indicates whether this memory block is valid.
/// </summary>
public readonly bool IsValid => Ptr != null && Size > 0;
/// <summary>
/// Creates a new MemoryBlock with the specified parameters.
/// </summary>
/// <param name="ptr">Pointer to the allocated memory.</param>
/// <param name="size">Size of the allocated memory.</param>
/// <param name="alignment">Alignment of the allocated memory.</param>
public MemoryBlock(void* ptr, void* heap, nuint size, nuint alignment)
{
Ptr = ptr;
Heap = heap;
Size = size;
Alignment = alignment;
}
/// <summary>
/// Creates an invalid MemoryBlock.
/// </summary>
public static MemoryBlock Invalid => new(null, null, 0, 0);
public Span<T> AsSpan<T>()
where T : unmanaged
{
if (!IsValid)
{
throw new InvalidOperationException("Cannot create span from invalid MemoryBlock.");
}
return new Span<T>(Ptr, (int)(Size / SizeOf<T>()));
}
}

View File

@@ -1,223 +0,0 @@
using System.Runtime.CompilerServices;
namespace Misaki.HighPerformance.LowLevel.Buffer;
public unsafe partial struct Stack
{
private static void** s_pStackBuffers = null;
private static int s_stackCount = 0;
private static int s_stackCapacity = 0;
private static readonly SpinLock s_locker = new SpinLock(false);
public static void DisposeAll()
{
if (s_pStackBuffers == null)
{
return;
}
for (var i = 0; i < s_stackCount; i++)
{
Free(s_pStackBuffers[i]);
}
}
}
/// <summary>
/// Provides a stack-based memory allocator for unmanaged memory, enabling fast allocation and deallocation of memory
/// blocks within a preallocated buffer.
/// </summary>
/// <remarks>This is not a thread-safe implementation.</remarks>
public unsafe partial struct Stack : IDisposable
{
private const nuint _DEFAULT_SIZE = 1024 * 1024; // 1MB
public readonly ref struct Scope : IDisposable
{
private readonly Stack* _allocator;
private readonly AllocationHandle _handle;
private readonly nuint _originalOffset;
public readonly AllocationHandle AllocationHandle => _handle;
internal Scope(Stack* allocator, AllocationHandle handle)
{
_allocator = allocator;
_handle = handle;
_originalOffset = allocator->_offset;
_allocator->_activeScopeCount++;
}
public void Dispose()
{
if (_allocator != null)
{
_allocator->_offset = _allocator->_offset > _originalOffset ? _originalOffset : _allocator->_offset;
_allocator->_activeScopeCount--;
}
}
}
private byte* _buffer;
private nuint _size;
private nuint _offset;
private uint _activeScopeCount;
internal readonly byte* Buffer => _buffer;
public nuint Offset
{
readonly get => _offset;
internal set => _offset = value;
}
/// <summary>
/// Initializes a new instance of the StackAllocator class with a buffer of the specified size.
/// </summary>
/// <param name="size">The size, in bytes, of the memory buffer to allocate for stack-based allocations. Must be greater than zero.</param>
public Stack(nuint size)
{
if (size == 0)
{
throw new ArgumentException("Size must be greater than zero.", nameof(size));
}
Init(size);
}
private void Init(nuint size)
{
if (_buffer != null)
{
Free(_buffer);
}
_buffer = (byte*)Malloc(size);
_size = size;
_offset = 0;
_activeScopeCount = 0;
var token = false;
try
{
s_locker.Enter(ref token);
if (s_pStackBuffers == null)
{
s_pStackBuffers = (void**)Malloc((nuint)sizeof(void*) * 4u);
s_stackCapacity = 4;
}
if (s_stackCount >= s_stackCapacity)
{
var pOld = s_pStackBuffers;
var newCapacity = s_stackCapacity * 2;
var pNew = (void**)Realloc(pOld, (nuint)sizeof(void*) * (nuint)newCapacity);
s_pStackBuffers = pNew;
s_stackCapacity = newCapacity;
}
s_pStackBuffers[s_stackCount] = _buffer;
s_stackCount++;
}
finally
{
if (token)
{
s_locker.Exit();
}
}
}
private readonly void ThrowIfNoScope()
{
if (_activeScopeCount == 0)
{
throw new InvalidOperationException("Allocations can only be made within an active memory scope.");
}
}
private void EnsureInitialize()
{
if (_buffer == null || _size == 0)
{
Init(_DEFAULT_SIZE);
}
}
/// <summary>
/// Creates a new scope instance associated with the current stack context.
/// </summary>
/// <remarks>
/// The instance of <see cref="Stack"/> must be pinned or allocated on the native heap to ensure that the pointer remains valid for the lifetime of the scope.
/// </remarks>
/// <returns>A <see cref="Scope"/> object that represents a scope tied to this stack.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Scope CreateScope(AllocationHandle handle)
{
EnsureInitialize();
return new Scope((Stack*)Unsafe.AsPointer(ref this), handle);
}
/// <summary>
/// Allocates a block of memory of the specified size and alignment from the buffer.
/// </summary>
/// <param name="size">The number of bytes to allocate. Must be greater than zero and less than or equal to the remaining buffer size.</param>
/// <param name="alignment">The alignment, in bytes, for the allocated memory block. Must be a power of two and greater than zero.</param>
/// <param name="allocationOption">An option specifying additional allocation behavior, such as whether the allocated memory should be cleared. The
/// default is <see cref="AllocationOption.None"/>.</param>
/// <returns>A pointer to the beginning of the allocated memory block if successful; otherwise, <see langword="null"/> if
/// there is insufficient space in the buffer.</returns>
public void* Allocate(nuint size, nuint alignment, AllocationOption allocationOption = AllocationOption.None)
{
ThrowIfNoScope();
if (size == 0)
{
return null;
}
if ((alignment & (alignment - 1)) != 0)
{
throw new ArgumentException("Alignment must be a power of two.", nameof(alignment));
}
var alignedOffset = (_offset + alignment - 1) & ~(alignment - 1);
var newOffset = alignedOffset + size;
if (newOffset > _size)
{
throw new OutOfMemoryException("Insufficient memory in stack allocator.");
}
var ptr = _buffer + alignedOffset;
_offset = newOffset;
if (allocationOption.HasFlag(AllocationOption.Clear))
{
MemClear(ptr, size);
}
return ptr;
}
/// <summary>
/// Resets the internal offset to its initial position.
/// </summary>
public void Reset()
{
_offset = 0;
}
public void Dispose()
{
Free(_buffer);
_buffer = null;
_size = 0;
_offset = 0;
}
}