Fix source only package issue in nuget
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
global using static Misaki.HighPerformance.LowLevel.Utilities.MemoryUtility;
|
||||
|
||||
global using unsafe AllocFunc = delegate*<void*, nuint, nuint, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption, Misaki.HighPerformance.LowLevel.Buffer.MemoryHandle*, void*>;
|
||||
global using unsafe ReallocFunc = delegate*<void*, void*, nuint, nuint, nuint, Misaki.HighPerformance.LowLevel.Buffer.AllocationOption, Misaki.HighPerformance.LowLevel.Buffer.MemoryHandle*, void*>;
|
||||
global using unsafe FreeFunc = delegate*<void*, void*, Misaki.HighPerformance.LowLevel.Buffer.MemoryHandle, void>;
|
||||
global using unsafe IsValidFunc = delegate*<void*, Misaki.HighPerformance.LowLevel.Buffer.MemoryHandle, bool>;
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Misaki.HighPerformance.LowLevel;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Struct)]
|
||||
public class NonCopyableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
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>()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
|
||||
public unsafe interface IUnsafeCollection : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the object has been created. Returns true if the object is created, otherwise false.
|
||||
/// </summary>
|
||||
bool IsCreated
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all elements from the collection. The collection will be empty after this operation.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a pointer to an unmanaged memory location. This pointer can be used for low-level memory operations.
|
||||
/// </summary>
|
||||
/// <returns>The method returns a void pointer to the unsafe memory location.</returns>
|
||||
void* GetUnsafePtr();
|
||||
}
|
||||
|
||||
public interface IUnsafeCollection<T> : IUnsafeCollection, IEnumerable<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of elements in a collection. The value is read-only.
|
||||
/// </summary>
|
||||
int Count
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the size of a collection to the specified value.
|
||||
/// </summary>
|
||||
/// <remarks>This is to adjust the element count of the collection, not the size of the underlying buffer in memory.</remarks>
|
||||
/// <param name="newSize">Specifies the new size to which the collection should be adjusted.</param>
|
||||
/// <param name="option">Specifies allocation options that may affect how memory is managed during the resize operation.</param>
|
||||
void Resize(int newSize, AllocationOption option);
|
||||
}
|
||||
|
||||
public interface IUnsafeHashCollection<T> : IEnumerable<T>, IDisposable
|
||||
where T : unmanaged
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the object has been created. Returns true if the object is created, otherwise false.
|
||||
/// </summary>
|
||||
bool IsCreated
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of elements in a collection. The value is read-only.
|
||||
/// </summary>
|
||||
int Count
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all elements from the collection. The collection will be empty after this operation.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Changes the size of a collection to the specified value.
|
||||
/// </summary>
|
||||
/// <remarks>This is to adjust the element count of the collection, not the size of the underlying buffer in memory.</remarks>
|
||||
/// <param name="newSize">Specifies the new size to which the collection should be adjusted.</param>
|
||||
/// <param name="option">Specifies allocation options that may affect how memory is managed during the resize operation.</param>
|
||||
void Resize(int newSize, AllocationOption option);
|
||||
}
|
||||
|
||||
public interface IUnTypedCollection : IUnsafeCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// The total size of the buffer in bytes.
|
||||
/// </summary>
|
||||
nuint Size
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
ref T GetElementAt<T>(nuint index)
|
||||
where T : unmanaged;
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 32 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 32 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString32"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 32)]
|
||||
public unsafe struct FixedString32
|
||||
{
|
||||
public const int MAX_LENGTH = 15;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString32.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString32(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString32(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString32.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString32(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 64 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 64 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString64"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 64)]
|
||||
public unsafe struct FixedString64
|
||||
{
|
||||
public const int MAX_LENGTH = 31;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString64.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString64(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString64(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString64.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString64(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 128 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 128 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString128"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 128)]
|
||||
public unsafe struct FixedString128
|
||||
{
|
||||
public const int MAX_LENGTH = 63;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString128.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString128(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString128(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString128.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString128(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 256 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 256 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString256"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 256)]
|
||||
public unsafe struct FixedString256
|
||||
{
|
||||
public const int MAX_LENGTH = 127;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString256.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString256(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString256(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString256.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString256(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 512 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 512 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString512"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 512)]
|
||||
public unsafe struct FixedString512
|
||||
{
|
||||
public const int MAX_LENGTH = 255;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString512.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString512(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString512(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString512.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString512(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 1024 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 1024 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString1024"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 1024)]
|
||||
public unsafe struct FixedString1024
|
||||
{
|
||||
public const int MAX_LENGTH = 511;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString1024.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString1024(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString1024(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString1024.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString1024(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 2048 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 2048 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString2048"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 2048)]
|
||||
public unsafe struct FixedString2048
|
||||
{
|
||||
public const int MAX_LENGTH = 1023;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString2048.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString2048(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString2048(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString2048.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString2048(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 4096 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 4096 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString4096"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 4096)]
|
||||
public unsafe struct FixedString4096
|
||||
{
|
||||
public const int MAX_LENGTH = 2047;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString4096.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString4096(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString4096(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString4096.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString4096(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<#@ template debug="false" hostspecific="false" language="C#" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ import namespace="System.Linq" #>
|
||||
<#@ import namespace="System.Text" #>
|
||||
<#@ import namespace="System.Collections.Generic" #>
|
||||
<#@ output extension=".gen.cs" #>
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
<# for (int i = 32; i <= 4096; i *= 2) { #>
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length <#= i #> bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length <#= i #> bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedString<#= i #>"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = <#= i #>)]
|
||||
public unsafe struct FixedString<#= i #>
|
||||
{
|
||||
public const int MAX_LENGTH = <#= (i - 2) / 2 #>;
|
||||
|
||||
private ushort _length;
|
||||
private fixed char _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
return new string(bufferPtr, 0, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedString<#= i #>.");
|
||||
}
|
||||
|
||||
_length = (ushort)value.Length;
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, valuePtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString<#= i #>(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedString<#= i #>(ReadOnlySpan<char> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedString<#= i #>.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (char* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, (uint)(_length * sizeof(char)));
|
||||
}
|
||||
}
|
||||
|
||||
public FixedString<#= i #>(char* input, ushort length)
|
||||
: this(new ReadOnlySpan<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (char* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly char* GetUnsafePtr()
|
||||
{
|
||||
return (char*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
<# } #>
|
||||
@@ -0,0 +1,918 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 32 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 32 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText32"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 32)]
|
||||
public unsafe struct FixedText32
|
||||
{
|
||||
public const int MAX_LENGTH = 30;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText32.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText32(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText32.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText32(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText32(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText32(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText32.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText32(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 64 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 64 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText64"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 64)]
|
||||
public unsafe struct FixedText64
|
||||
{
|
||||
public const int MAX_LENGTH = 62;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText64.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText64(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText64.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText64(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText64(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText64(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText64.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText64(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 128 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 128 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText128"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 128)]
|
||||
public unsafe struct FixedText128
|
||||
{
|
||||
public const int MAX_LENGTH = 126;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText128.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText128(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText128.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText128(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText128(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText128(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText128.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText128(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 256 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 256 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText256"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 256)]
|
||||
public unsafe struct FixedText256
|
||||
{
|
||||
public const int MAX_LENGTH = 254;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText256.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText256(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText256.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText256(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText256(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText256(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText256.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText256(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 512 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 512 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText512"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 512)]
|
||||
public unsafe struct FixedText512
|
||||
{
|
||||
public const int MAX_LENGTH = 510;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText512.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText512(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText512.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText512(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText512(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText512(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText512.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText512(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 1024 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 1024 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText1024"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 1024)]
|
||||
public unsafe struct FixedText1024
|
||||
{
|
||||
public const int MAX_LENGTH = 1022;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText1024.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText1024(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText1024.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText1024(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText1024(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText1024(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText1024.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText1024(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 2048 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 2048 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText2048"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 2048)]
|
||||
public unsafe struct FixedText2048
|
||||
{
|
||||
public const int MAX_LENGTH = 2046;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText2048.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText2048(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText2048.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText2048(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText2048(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText2048(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText2048.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText2048(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length 4096 bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length 4096 bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText4096"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = 4096)]
|
||||
public unsafe struct FixedText4096
|
||||
{
|
||||
public const int MAX_LENGTH = 4094;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText4096.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText4096(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText4096.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText4096(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText4096(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText4096(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText4096.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText4096(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<#@ template debug="false" hostspecific="false" language="C#" #>
|
||||
<#@ assembly name="System.Core" #>
|
||||
<#@ import namespace="System.Linq" #>
|
||||
<#@ import namespace="System.Text" #>
|
||||
<#@ import namespace="System.Collections.Generic" #>
|
||||
<#@ output extension=".gen.cs" #>
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
<# for (int i = 32; i <= 4096; i *= 2) { #>
|
||||
/// <summary>
|
||||
/// Represents a stack allocated fixed-size UTF-8 encoded string of length <#= i #> bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This struct is designed to hold data on the stack. Every copy of this struct causes a copy of the underlying data.
|
||||
/// If you need a heap allocated fixed-size UTF-8 encoded string of length <#= i #> bytes, consider using <see cref="Misaki.HighPerformance.Unsafe.Buffer.FixedText<#= i #>"/>.
|
||||
/// </remarks>
|
||||
[StructLayout(LayoutKind.Sequential, Size = <#= i #>)]
|
||||
public unsafe struct FixedText<#= i #>
|
||||
{
|
||||
public const int MAX_LENGTH = <#= i - 2 #>;
|
||||
|
||||
private ushort _length;
|
||||
private fixed byte _buffer[MAX_LENGTH];
|
||||
|
||||
public readonly ushort Length => _length;
|
||||
public string Value
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
return Encoding.UTF8.GetString(bufferPtr, _length);
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(value);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText<#= i #>.");
|
||||
}
|
||||
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
_length = (ushort)Encoding.UTF8.GetBytes(value, new Span<byte>(bufferPtr, MAX_LENGTH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText<#= i #>(ReadOnlySpan<char> input)
|
||||
{
|
||||
var maxBytes = Encoding.UTF8.GetByteCount(input);
|
||||
if (maxBytes > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input string is too long to fit in FixedText<#= i #>.");
|
||||
}
|
||||
|
||||
fixed (char* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
var actualByteCount = Encoding.UTF8.GetBytes(inputPtr, input.Length, bufferPtr, MAX_LENGTH);
|
||||
_length = (ushort)actualByteCount;
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText<#= i #>(string input)
|
||||
: this(input.AsSpan())
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText<#= i #>(char* input, ushort length)
|
||||
: this(new Span<char>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedText<#= i #>(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length > MAX_LENGTH)
|
||||
{
|
||||
throw new ArgumentException("Input byte array is too long to fit in FixedText<#= i #>.");
|
||||
}
|
||||
|
||||
_length = (ushort)input.Length;
|
||||
|
||||
fixed (byte* inputPtr = input)
|
||||
fixed (byte* bufferPtr = _buffer)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(bufferPtr, inputPtr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
public FixedText<#= i #>(byte* input, ushort length)
|
||||
: this(new ReadOnlySpan<byte>(input, length))
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Span<byte> AsSpan()
|
||||
{
|
||||
fixed (byte* ptr = _buffer)
|
||||
{
|
||||
return new(ptr, _length);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly byte* GetUnsafePtr()
|
||||
{
|
||||
return (byte*)((ushort*)Unsafe.AsPointer(in this) + 1);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
<# } #>
|
||||
@@ -0,0 +1,123 @@
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a read-only, unsafe view over a contiguous region of unmanaged memory as an array of elements of type T.
|
||||
/// Enables efficient, low-level access to memory without copying or additional safety checks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This read only collection does not own the memory it points to. The user is responsible for ensuring the memory remains valid for the lifetime of this structure.
|
||||
/// The goal of this struc is similar to <see cref="ReadOnlySpan{T}"/>, but it can be used in contexts where spans are not allowed, such as fields in structs and shared across threads.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of elements in the collection. Must be an unmanaged type.</typeparam>
|
||||
public readonly unsafe struct ReadOnlyUnsafeCollection<T> : IEnumerable<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly ReadOnlyUnsafeCollection<T> _collection;
|
||||
private int _index;
|
||||
|
||||
public readonly T Current => _collection[_index];
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(ref readonly ReadOnlyUnsafeCollection<T> array)
|
||||
{
|
||||
_collection = array;
|
||||
_index = -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
_index++;
|
||||
return _index < _collection.Count;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private readonly T* _buffer;
|
||||
private readonly int _count;
|
||||
|
||||
public int Count => _count;
|
||||
|
||||
public ref readonly T this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
CheckIndexBounds(index);
|
||||
return ref UnsafeUtility.ReadArrayElementRef<T>(_buffer, index);
|
||||
}
|
||||
}
|
||||
|
||||
public ref readonly T this[uint index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
CheckIndexBounds((int)index);
|
||||
return ref UnsafeUtility.ReadArrayElementRef<T>(_buffer, index);
|
||||
}
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new Enumerator(in this);
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public ReadOnlyUnsafeCollection(T* buffer, int count)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_count = count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Conditional("ENABLE_COLLECTION_CHECKS")]
|
||||
private readonly void CheckIndexBounds(int index)
|
||||
{
|
||||
if (index >= _count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only span that represents the valid elements in the underlying buffer.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ReadOnlySpan{T}"/> containing the elements of the buffer up to the current count.</returns>
|
||||
public ReadOnlySpan<T> AsSpan()
|
||||
{
|
||||
return new ReadOnlySpan<T>(_buffer, _count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reinterprets the underlying collection as a read-only collection of a different unmanaged type without copying the data.
|
||||
/// </summary>
|
||||
/// <typeparam name="U">The unmanaged type to reinterpret the collection elements as.</typeparam>
|
||||
/// <returns>A new ReadOnlyUnsafeCollection<U> that provides a read-only view of the same memory, interpreted as elements of type U.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the total size of the underlying collection is not a multiple of the size of type U, making the reinterpretation invalid.</exception>
|
||||
public ReadOnlyUnsafeCollection<U> Reinterpret<U>()
|
||||
where U : unmanaged
|
||||
{
|
||||
var totalSize = (nuint)(Count * sizeof(T));
|
||||
if (totalSize % (nuint)sizeof(U) != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot reinterpret collection: size mismatch.");
|
||||
}
|
||||
|
||||
var newCount = (int)(totalSize / (nuint)sizeof(U));
|
||||
return new ReadOnlyUnsafeCollection<U>((U*)_buffer, newCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
public unsafe struct UnTypedArray : IUnTypedCollection
|
||||
{
|
||||
private void* _buffer;
|
||||
private nuint _size;
|
||||
private nuint _alignment;
|
||||
|
||||
private MemoryHandle _memoryHandle;
|
||||
private AllocationHandle _allocationHandle;
|
||||
|
||||
public readonly nuint Size => _size;
|
||||
public readonly nuint Alignment => _alignment;
|
||||
|
||||
public readonly bool IsCreated
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
if (_allocationHandle.IsValid != null)
|
||||
{
|
||||
return _allocationHandle.IsValid(_allocationHandle.State, _memoryHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an UnsafeArray with a default size of 1 and uses the Persistent allocator.
|
||||
/// </summary>
|
||||
public UnTypedArray()
|
||||
: this(0, 8, Allocator.Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
public UnTypedArray(nuint size, nuint alignment, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
if (size <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(size), "Count must be greater than zero.");
|
||||
}
|
||||
|
||||
if (handle.Alloc == null)
|
||||
{
|
||||
throw new InvalidOperationException("Target allocation handle does not support allocation.");
|
||||
}
|
||||
|
||||
MemoryHandle memHandle;
|
||||
_buffer = handle.Alloc(_allocationHandle.State, size, alignment, allocationOption, &memHandle);
|
||||
_size = size;
|
||||
_alignment = alignment;
|
||||
|
||||
_memoryHandle = memHandle;
|
||||
_allocationHandle = handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of UnsafeArray with a specified number of elements and an allocation type.
|
||||
/// </summary>
|
||||
/// <param name="count">Specifies the number of elements to allocate in the array, which must be greater than zero.</param>
|
||||
/// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param>
|
||||
/// <param name="allocationOption">Determines how the memory should be allocated.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception>
|
||||
public UnTypedArray(nuint size, nuint alignment, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||
: this(size, alignment, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an UnsafeArray with a pointer to a buffer and a count of elements. This does not copy the data.
|
||||
/// </summary>
|
||||
/// <param name="buffer">A pointer to the memory location that holds the elements of the array.</param>
|
||||
/// <param name="count">The total size of the data.</param>
|
||||
/// <remarks>
|
||||
/// When using this constructor, the user is responsible for managing the memory pointed to by the buffer.
|
||||
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
|
||||
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
|
||||
/// </remarks>
|
||||
public UnTypedArray(void* buffer, uint size)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_size = size;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly ref T GetElementAt<T>(nuint index)
|
||||
where T : unmanaged
|
||||
{
|
||||
return ref UnsafeUtility.ReadArrayElementRef<T>(_buffer, index);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Resize(uint newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
if (newSize == _size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MemoryHandle memHandle = _memoryHandle;
|
||||
_buffer = _allocationHandle.Realloc(_allocationHandle.State, _buffer, _size, newSize, _alignment, option, &memHandle);
|
||||
_size = newSize;
|
||||
_memoryHandle = memHandle;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void Clear()
|
||||
{
|
||||
MemClear(_buffer, _size);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies elements from an untyped source collection to a destination span of a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of elements in the destination span, which must be unmanaged.</typeparam>
|
||||
/// <param name="destination">The typed span where the data will be copied to.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the source collection size exceeds the destination span capacity.</exception>
|
||||
public readonly void CopyTo<C, T>(Span<T> destination)
|
||||
where T : unmanaged
|
||||
{
|
||||
var destSize = (uint)destination.Length * (uint)sizeof(T);
|
||||
if (_size > destSize)
|
||||
{
|
||||
throw new ArgumentException("Source collection is larger than the destination span.");
|
||||
}
|
||||
|
||||
fixed (T* pDest = destination)
|
||||
{
|
||||
MemCpy(pDest, _buffer, _size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a range of bytes from an untyped source collection to a destination span, interpreting the bytes as elements of type T.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of elements in the destination span, which must be unmanaged.</typeparam>
|
||||
/// <param name="destination">The typed span where the elements will be placed.</param>
|
||||
/// <param name="sourceOffset">The byte offset in the source collection from which to start copying.</param>
|
||||
/// <param name="destinationIndex">The element index in the destination span where copying will begin.</param>
|
||||
/// <param name="length">The number of elements of type T to copy.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the specified range exceeds the bounds of the source collection or destination span.</exception>
|
||||
public readonly void CopyTo<T>(Span<T> destination, uint sourceOffset, uint destinationIndex, uint length)
|
||||
where T : unmanaged
|
||||
{
|
||||
var sizeOfElement = (uint)sizeof(T);
|
||||
if (sourceOffset + (length * sizeOfElement) > _size || destinationIndex + length > destination.Length)
|
||||
{
|
||||
throw new ArgumentException("Source collection or destination span is too small for the specified range.");
|
||||
}
|
||||
|
||||
fixed (T* pDest = destination)
|
||||
{
|
||||
MemCpy(pDest + destinationIndex, (byte*)_buffer + sourceOffset, length * sizeOfElement);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies elements from a typed source span to an untyped destination collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of elements in the source span, which must be unmanaged.</typeparam>
|
||||
/// <param name="source">The typed span containing the elements to be copied.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the destination collection is smaller than the source span data size.</exception>
|
||||
public readonly void CopyFrom<T>(ReadOnlySpan<T> source)
|
||||
where T : unmanaged
|
||||
{
|
||||
var sourceSize = (uint)(source.Length * sizeof(T));
|
||||
|
||||
if (_size < sourceSize)
|
||||
{
|
||||
throw new ArgumentException("Destination collection is smaller than the source span.");
|
||||
}
|
||||
|
||||
fixed (T* pSrc = source)
|
||||
{
|
||||
MemCpy(_buffer, pSrc, sourceSize);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a range of elements from a typed source span to an untyped destination collection at a specified byte offset.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of elements in the source span, which must be unmanaged.</typeparam>
|
||||
/// <param name="source">The typed span containing the elements to be copied.</param>
|
||||
/// <param name="sourceIndex">The starting element index in the source span from which to begin copying.</param>
|
||||
/// <param name="destinationOffset">The byte offset in the destination collection where the data will be placed.</param>
|
||||
/// <param name="length">The number of elements to copy from the source span.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the specified range exceeds the bounds of the source span or destination collection.</exception>
|
||||
public readonly void CopyFrom<T>(ReadOnlySpan<T> source, uint sourceIndex, uint destinationOffset, uint length)
|
||||
where T : unmanaged
|
||||
{
|
||||
var sizeOfElement = (uint)sizeof(T);
|
||||
if (sourceIndex + length > source.Length || destinationOffset + (length * sizeOfElement) > _size)
|
||||
{
|
||||
throw new ArgumentException("Source span or destination collection is too small for the specified range.");
|
||||
}
|
||||
|
||||
fixed (T* pSrc = source)
|
||||
{
|
||||
MemCpy((byte*)_buffer + destinationOffset, pSrc + sourceIndex, length * sizeOfElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts into a Span for efficient memory access.
|
||||
/// </summary>
|
||||
/// <returns>A Span that provides a view over the elements of the UnsafeCollection.</returns>
|
||||
public readonly Span<byte> AsSpan<C>()
|
||||
{
|
||||
return new(_buffer, (int)_size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a span over a contiguous region of elements in the specified unsafe collection, starting at the given index and covering the specified number of elements.
|
||||
/// </summary>
|
||||
/// <param name="start">The zero-based index of the first element in the collection to include in the span. Must be greater than or equal to zero and less than the number of elements in the collection.</param>
|
||||
/// <param name="length">The number of elements to include in the span. Must be greater than or equal to zero and the range defined by
|
||||
/// <paramref name="start"/> and <paramref name="length"/> must not exceed the bounds of the collection.</param>
|
||||
/// <returns>A <see cref="Span{byte}"/> representing the specified region of the collection.</returns>
|
||||
public readonly Span<byte> AsSpan(int start, int length)
|
||||
{
|
||||
if (start < 0 || length < 0 || (nuint)(start + length) > _size)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(start), "The specified range is out of bounds of the collection.");
|
||||
}
|
||||
|
||||
return new Span<byte>((byte*)_buffer + start, length);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_allocationHandle.Free != null)
|
||||
{
|
||||
_allocationHandle.Free(_allocationHandle.State, _buffer, _memoryHandle);
|
||||
}
|
||||
|
||||
_buffer = null;
|
||||
_size = 0;
|
||||
_alignment = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
internal class UnsafeArrayDebugView<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly UnsafeArray<T> _array;
|
||||
|
||||
public UnsafeArrayDebugView(UnsafeArray<T> array)
|
||||
{
|
||||
_array = array;
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||
public T[] Items
|
||||
{
|
||||
get
|
||||
{
|
||||
var count = _array.Count;
|
||||
var result = new T[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
result[i] = _array[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A structure for managing an array of unmanaged types with unsafe memory operations.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents a type that can be stored in an unmanaged memory context.</typeparam>
|
||||
[DebuggerTypeProxy(typeof(UnsafeArrayDebugView<>))]
|
||||
public unsafe struct UnsafeArray<T> : IUnsafeCollection<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly UnsafeArray<T>* _collection;
|
||||
private int _index;
|
||||
|
||||
public readonly ref T Current => ref _collection->_buffer[_index];
|
||||
readonly T IEnumerator<T>.Current => Current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(UnsafeArray<T>* collection)
|
||||
{
|
||||
_collection = collection;
|
||||
_index = -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
_index++;
|
||||
return _index < _collection->_count;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private T* _buffer;
|
||||
private int _count;
|
||||
private MemoryHandle _memoryHandle;
|
||||
private AllocationHandle _allocationHandle;
|
||||
|
||||
public readonly int Count => _count;
|
||||
public readonly int Length => _count;
|
||||
|
||||
public readonly ref T this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
CheckIndexBounds(index);
|
||||
return ref UnsafeUtility.ReadArrayElementRef<T>(_buffer, index);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly ref T this[uint index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
CheckIndexBounds((int)index);
|
||||
return ref UnsafeUtility.ReadArrayElementRef<T>(_buffer, index);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly bool IsCreated
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
if (_allocationHandle.IsValid != null)
|
||||
{
|
||||
return _allocationHandle.IsValid(_allocationHandle.State, _memoryHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new((UnsafeArray<T>*)UnsafeUtility.AddressOf(ref this));
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Invalid constructor, use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> or <see cref="UnsafeArray(int, ref AllocationHandle, AllocationOption)"/> instead.
|
||||
/// </summary>
|
||||
public UnsafeArray()
|
||||
: this(0, Allocator.Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of UnsafeArray with a specified number of elements and an allocation handle.
|
||||
/// </summary>
|
||||
/// <param name="count">Specifies the number of elements to allocate in the array, which must be greater than zero.</param>
|
||||
/// <param name="handle">A reference to an AllocationHandle that manages the memory allocation for the array.</param>
|
||||
/// <param name="allocationOption">Specifies how the memory should be allocated.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception>
|
||||
public UnsafeArray(int count, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count), "Count can not be less than zero.");
|
||||
}
|
||||
|
||||
if (handle.Alloc == null)
|
||||
{
|
||||
throw new InvalidOperationException("Target allocation handle does not support allocation.");
|
||||
}
|
||||
|
||||
MemoryHandle memHandle;
|
||||
var buff = handle.Alloc(handle.State, (nuint)(count * sizeof(T)), AlignOf<T>(), allocationOption, &memHandle);
|
||||
|
||||
_buffer = (T*)buff;
|
||||
_memoryHandle = memHandle;
|
||||
_allocationHandle = handle;
|
||||
_count = count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of UnsafeArray with a specified number of elements and an allocation type.
|
||||
/// </summary>
|
||||
/// <param name="count">Specifies the number of elements to allocate in the array, which must be greater than zero.</param>
|
||||
/// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param>
|
||||
/// <param name="allocationOption">Determines how the memory should be allocated.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified number of elements is less than or equal to zero.</exception>
|
||||
public UnsafeArray(int count, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||
: this(count, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an UnsafeArray with a pointer to a buffer and a count of elements. This does not copy the data.
|
||||
/// </summary>
|
||||
/// <param name="buffer">A pointer to the memory location that holds the elements of the array.</param>
|
||||
/// <param name="count">The total size of the data.</param>
|
||||
/// <remarks>
|
||||
/// When using this constructor, the user is responsible for managing the memory pointed to by the buffer.
|
||||
/// Disposing of the UnsafeArray does not free the memory and only release the reference. The memory should be freed manually when no longer needed.
|
||||
/// Use <see cref="UnsafeArray(int, Allocator, AllocationOption)"/> constructor and <see cref="MemCpy(void*, void*, nuint)"/> if you are not sure what you are doing.
|
||||
/// </remarks>
|
||||
public UnsafeArray(T* buffer, int count)
|
||||
{
|
||||
_buffer = buffer;
|
||||
_count = count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Conditional("ENABLE_COLLECTION_CHECKS")]
|
||||
private readonly void ThrowIfNotCreated()
|
||||
{
|
||||
if (!IsCreated)
|
||||
{
|
||||
throw new InvalidOperationException("The UnsafeArray is not created.");
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Conditional("ENABLE_COLLECTION_CHECKS")]
|
||||
private readonly void CheckIndexBounds(int index)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
if (index >= _count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index), "Index is out of range.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a read-only view of the current collection.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ReadOnlyUnsafeCollection{T}"/> that provides a read-only view of the elements in the current collection.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly ReadOnlyUnsafeCollection<T> AsReadOnly()
|
||||
{
|
||||
return new ReadOnlyUnsafeCollection<T>(_buffer, _count);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
if (_allocationHandle.Realloc == null)
|
||||
{
|
||||
throw new InvalidOperationException("Target allocation handle does not support reallocation.");
|
||||
}
|
||||
|
||||
if (newSize == Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MemoryHandle memHandle = _memoryHandle;
|
||||
var elemSize = SizeOf<T>();
|
||||
_buffer = (T*)_allocationHandle.Realloc(_allocationHandle.State, _buffer, (nuint)Count * elemSize, (nuint)newSize * elemSize, AlignOf<T>(), option, &memHandle);
|
||||
_memoryHandle = memHandle;
|
||||
_count = newSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void Clear()
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
MemClear(_buffer, (nuint)(Count * sizeof(T)));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
return _buffer;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly Span<T> AsSpan()
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
return new Span<T>(_buffer, _count);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly Span<T> AsSpan(int start, int length)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
return new Span<T>(_buffer + start, length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reinterprets the underlying buffer as an array of a different unmanaged type without copying the data.
|
||||
/// </summary>
|
||||
/// <typeparam name="U">The unmanaged type to reinterpret the buffer as.</typeparam>
|
||||
/// <returns>An UnsafeArray<U> that views the same memory as the original array, but as elements of type U.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the total size of the buffer in bytes is not a multiple of the size of type U.</exception>
|
||||
public readonly UnsafeArray<U> Reinterpret<U>()
|
||||
where U : unmanaged
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
var totalSize = (nuint)(Count * sizeof(T));
|
||||
if (totalSize % (nuint)sizeof(U) != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot reinterpret array: size mismatch.");
|
||||
}
|
||||
|
||||
var newCount = (int)(totalSize / (nuint)sizeof(U));
|
||||
return new UnsafeArray<U>((U*)_buffer, newCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_allocationHandle.Free != null)
|
||||
{
|
||||
_allocationHandle.Free(_allocationHandle.State, _buffer, _memoryHandle);
|
||||
}
|
||||
|
||||
_buffer = null;
|
||||
_count = 0;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,224 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
public unsafe struct UnsafeHashMap<TKey, TValue> : IUnsafeHashCollection<KeyValuePair<TKey, TValue>>
|
||||
where TKey : unmanaged, IEquatable<TKey> where TValue : unmanaged
|
||||
{
|
||||
public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
|
||||
{
|
||||
internal HashMapHelper<TKey>.Enumerator _enumerator;
|
||||
|
||||
public KeyValuePair<TKey, TValue> Current => _enumerator.GetCurrent<TValue>();
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(HashMapHelper<TKey>* data)
|
||||
{
|
||||
_enumerator = new HashMapHelper<TKey>.Enumerator(data);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext() => _enumerator.MoveNext();
|
||||
|
||||
public void Reset() => _enumerator.Reset();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private HashMapHelper<TKey> _helper;
|
||||
|
||||
public readonly int Count => _helper.Count;
|
||||
public readonly int Capacity => _helper.Capacity;
|
||||
public readonly bool IsCreated => _helper.IsCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Gets and sets values by key.
|
||||
/// </summary>
|
||||
/// <remarks>Getting a key that is not present will throw. Setting a key that is not already present will add the key.</remarks>
|
||||
/// <param name="key">The key to look up.</param>
|
||||
/// <value>The value associated with the key.</value>
|
||||
/// <exception cref="ArgumentException">For getting, thrown if the key was not present.</exception>
|
||||
public TValue this[TKey key]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_helper.TryGetValue<TValue>(key, out var result))
|
||||
{
|
||||
throw new ArgumentException($"Key: {key} is not present.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
set
|
||||
{
|
||||
var idx = _helper.Find(key);
|
||||
if (-1 != idx)
|
||||
{
|
||||
UnsafeUtility.WriteArrayElement(_helper.Buffer, idx, value);
|
||||
return;
|
||||
}
|
||||
|
||||
TryAdd(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new((HashMapHelper<TKey>*)UnsafeUtility.AddressOf(ref this));
|
||||
IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Invalid constructor, use <see cref="UnsafeHashMap(int, Allocator, AllocationOption)"/> or <see cref="UnsafeHashMap(int, ref AllocationHandle, AllocationOption)"/> instead.
|
||||
/// </summary>
|
||||
public UnsafeHashMap()
|
||||
: this(0, Allocator.Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
public UnsafeHashMap(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
_helper = new HashMapHelper<TKey>(capacity, sizeof(TValue), (int)AlignOf<TValue>(), HashMapHelper<TKey>.MINIMAL_CAPACITY, handle, allocationOption);
|
||||
}
|
||||
|
||||
public UnsafeHashMap(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||
: this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new key-value pair.
|
||||
/// </summary>
|
||||
/// <remarks>If the key is already present, this method returns false without modifying the hash map.</remarks>
|
||||
/// <param name="key">The key to add.</param>
|
||||
/// <param name="item">The value to add.</param>
|
||||
/// <returns>True if the key-value pair was added.</returns>
|
||||
public bool TryAdd(in TKey key, TValue item)
|
||||
{
|
||||
var idx = _helper.TryAdd(key);
|
||||
if (idx != -1)
|
||||
{
|
||||
UnsafeUtility.WriteArrayElement(_helper.Buffer, idx, item);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new key-value pair.
|
||||
/// </summary>
|
||||
/// <remarks>If the key is already present, this method throws without modifying the hash map.</remarks>
|
||||
/// <param name="key">The key to add.</param>
|
||||
/// <param name="item">The value to add.</param>
|
||||
/// <exception cref="ArgumentException">Thrown if the key was already present.</exception>
|
||||
public void Add(in TKey key, TValue item)
|
||||
{
|
||||
var result = TryAdd(key, item);
|
||||
if (!result)
|
||||
{
|
||||
throw new ArgumentException($"An item with the same key has already been added: {key}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a particular key and its value.
|
||||
/// </summary>
|
||||
/// <param name="item">The value to remove.</param>
|
||||
/// <returns>True if the value was present.</returns>
|
||||
public bool Remove(in TKey key)
|
||||
{
|
||||
return -1 != _helper.TryRemove(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value associated with a key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to look up.</param>
|
||||
/// <param name="item">Outputs the value associated with the key. Outputs default if the key was not present.</param>
|
||||
/// <returns>True if the key was present.</returns>
|
||||
public bool TryGetValue(in TKey key, out TValue item)
|
||||
{
|
||||
return _helper.TryGetValue(key, out item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the value associated with the specified key, or returns a default value if the key is not found.
|
||||
/// </summary>
|
||||
/// <param name="key">The key whose value to retrieve.</param>
|
||||
/// <param name="defaultValue">The value to return if the specified key does not exist. If not specified, the default value for the type is used.</param>
|
||||
/// <returns>The value associated with the specified key if the key is found; otherwise, the specified default value.</returns>
|
||||
public TValue GetValueOrDefault(in TKey key, TValue defaultValue = default)
|
||||
{
|
||||
if (_helper.TryGetValue<TValue>(key, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public ref TValue GetValueRef(in TKey key, out bool exists)
|
||||
{
|
||||
return ref _helper.GetValueRef<TValue>(key, out exists);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a given key is present in this hash map.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to look up.</param>
|
||||
/// <returns>True if the key was present.</returns>
|
||||
public bool ContainsKey(in TKey key)
|
||||
{
|
||||
return -1 != _helper.Find(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the capacity to match what it would be if it had been originally initialized with all its entries.
|
||||
/// </summary>
|
||||
public void TrimExcess() => _helper.TrimExcess();
|
||||
|
||||
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
_helper.Resize(newSize);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_helper.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an array of keys from the hash map.
|
||||
/// </summary>
|
||||
/// <returns>An array containing the keys stored in the hash map.</returns>
|
||||
public UnsafeArray<TKey> GetKeyArray(Allocator allocator) => _helper.GetKeyArray(allocator);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an array of values from the underlying hash map.
|
||||
/// </summary>
|
||||
/// <returns>An UnsafeArray containing the values stored in the hash map.</returns>
|
||||
public UnsafeArray<TValue> GetValueArray(Allocator allocator) => _helper.GetValueArray<TValue>(allocator);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an array of key-value pairs from the hash map. The keys are of type TKey and the values are of type
|
||||
/// TValue.
|
||||
/// </summary>
|
||||
/// <returns>Returns an UnsafeArray containing KeyValuePair objects.</returns>
|
||||
public UnsafeArray<KeyValuePair<TKey, TValue>> GetKeyValueArrays(Allocator allocator) => _helper.GetKeyValueArrays<TValue>(allocator);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
return _helper.Buffer;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_helper.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// A collection that provides fast, unsafe operations for managing a set of unmanaged types. It supports adding,
|
||||
/// removing, and checking for values.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents an unmanaged type that can be compared for equality.</typeparam>
|
||||
public unsafe struct UnsafeHashSet<T> : IUnsafeHashCollection<T>, IEnumerable<T>
|
||||
where T : unmanaged, IEquatable<T>
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
internal HashMapHelper<T>.Enumerator _enumerator;
|
||||
|
||||
public readonly T Current => _enumerator.buffer->_keys[_enumerator.index];
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(HashMapHelper<T>* hashMap)
|
||||
{
|
||||
_enumerator = new HashMapHelper<T>.Enumerator(hashMap);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext() => _enumerator.MoveNext();
|
||||
|
||||
public void Reset() => _enumerator.Reset();
|
||||
|
||||
public readonly void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private HashMapHelper<T> _helper;
|
||||
|
||||
public readonly int Count => _helper.Count;
|
||||
public readonly int Capacity => _helper.Capacity;
|
||||
public readonly bool IsCreated => _helper.IsCreated;
|
||||
|
||||
public Enumerator GetEnumerator() => new((HashMapHelper<T>*)UnsafeUtility.AddressOf(ref this));
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Invalid constructor. Use <see cref="UnsafeHashSet(int, Allocator, AllocationOption)"/> or <see cref="UnsafeHashSet(int, ref AllocationHandle, AllocationOption)"/> instead."/>
|
||||
/// </summary>
|
||||
public UnsafeHashSet()
|
||||
: this(0, Allocator.Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
public UnsafeHashSet(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
_helper = new HashMapHelper<T>(capacity, 0, 0, HashMapHelper<T>.MINIMAL_CAPACITY, handle, allocationOption);
|
||||
}
|
||||
|
||||
public UnsafeHashSet(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||
: this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new value (unless it is already present).
|
||||
/// </summary>
|
||||
/// <param name="item">The value to add.</param>
|
||||
/// <returns>True if the value was not already present.</returns>
|
||||
public bool Add(T item)
|
||||
{
|
||||
return -1 != _helper.TryAdd(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a particular value.
|
||||
/// </summary>
|
||||
/// <param name="item">The value to remove.</param>
|
||||
/// <returns>True if the value was present.</returns>
|
||||
public bool Remove(T item)
|
||||
{
|
||||
return -1 != _helper.TryRemove(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if a particular value is present.
|
||||
/// </summary>
|
||||
/// <param name="item">The value to check for.</param>
|
||||
/// <returns>True if the value was present.</returns>
|
||||
public bool Contains(T item)
|
||||
{
|
||||
return -1 != _helper.Find(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the capacity to match what it would be if it had been originally initialized with all its entries.
|
||||
/// </summary>
|
||||
public void TrimExcess() => _helper.TrimExcess();
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array with a copy of this set's values (in no particular order).
|
||||
/// </summary>
|
||||
/// <param name="allocator">The allocator to use.</param>
|
||||
/// <returns>An array with a copy of the set's values.</returns>
|
||||
public UnsafeArray<T> ToNativeArray(Allocator allocator)
|
||||
{
|
||||
return _helper.GetKeyArray(allocator);
|
||||
}
|
||||
|
||||
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
_helper.Resize(newSize);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_helper.Clear();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
return _helper.Buffer;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_helper.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
internal class UnsafeListDebugView<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly UnsafeList<T> _list;
|
||||
public UnsafeListDebugView(UnsafeList<T> list)
|
||||
{
|
||||
_list = list;
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||
public T[] Items
|
||||
{
|
||||
get
|
||||
{
|
||||
var array = new T[_list.Count];
|
||||
for (int i = 0; i < _list.Count; i++)
|
||||
{
|
||||
array[i] = _list[i];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A collection that allows for unsafe operations on a list of unmanaged types.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents a type that can be stored in the collection, constrained to unmanaged types for performance and safety.</typeparam>
|
||||
[DebuggerTypeProxy(typeof(UnsafeListDebugView<>))]
|
||||
public unsafe struct UnsafeList<T> : IUnsafeCollection<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly UnsafeList<T>* _collection;
|
||||
private int _index;
|
||||
|
||||
public readonly ref T Current => ref _collection->_array[_index];
|
||||
readonly T IEnumerator<T>.Current => Current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(UnsafeList<T>* collection)
|
||||
{
|
||||
_collection = collection;
|
||||
_index = -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
_index++;
|
||||
return _index < _collection->_count;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
}
|
||||
|
||||
public readonly void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A parallel writer for an UnsafeList.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <see cref="AsParallelWriter"/> to create a parallel writer for a list.
|
||||
/// </remarks>
|
||||
public unsafe struct ParallelWriter
|
||||
{
|
||||
/// <summary>
|
||||
/// The UnsafeList to write to.
|
||||
/// </summary>
|
||||
public UnsafeList<T>* listData;
|
||||
|
||||
internal ParallelWriter(UnsafeList<T>* list)
|
||||
{
|
||||
listData = list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a value to a collection without resizing it, ensuring capacity is checked before insertion.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to be added to the collection.</param>
|
||||
public void AddNoResize(T value)
|
||||
{
|
||||
var idx = Interlocked.Increment(ref listData->_count) - 1;
|
||||
listData->CheckNoResizeCapacity(idx, 1);
|
||||
UnsafeUtility.WriteArrayElement(listData->_array.GetUnsafePtr(), idx, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a specified number of elements from a pointer to a buffer without resizing the underlying storage.
|
||||
/// </summary>
|
||||
/// <param name="ptr">Points to the source data to be copied into the buffer.</param>
|
||||
/// <param name="count">Indicates the number of elements to be added from the source data.</param>
|
||||
public void AddRangeNoResize(ReadOnlySpan<T> collection, int count)
|
||||
{
|
||||
var index = Interlocked.Add(ref listData->_count, count) - count;
|
||||
listData->CheckNoResizeCapacity(index, count);
|
||||
|
||||
fixed (T* pCollection = collection)
|
||||
{
|
||||
MemCpy(UnsafeUtility.ReadArrayElementUnsafe<T>(listData->_array.GetUnsafePtr(), index), pCollection, (uint)(count * sizeof(T)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UnsafeArray<T> _array;
|
||||
|
||||
private int _count;
|
||||
|
||||
public readonly int Count => _count;
|
||||
public readonly int Capacity => _array.Count;
|
||||
public readonly bool IsCreated => _array.IsCreated;
|
||||
|
||||
public readonly ref T this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => ref _array[index];
|
||||
}
|
||||
|
||||
public readonly ref T this[uint index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => ref _array[index];
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new ((UnsafeList<T>*)UnsafeUtility.AddressOf(ref this));
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Provides a parallel writer for the current list, enabling thread-safe additions to the list.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ParallelWriter"/> instance that can be used to add items to the list in a thread-safe manner.</returns>
|
||||
public ParallelWriter AsParallelWriter() => new((UnsafeList<T>*)UnsafeUtility.AddressOf(ref this));
|
||||
|
||||
/// <summary>
|
||||
/// Converts the current list to an UnsafeArray representation.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="UnsafeArray{T}"/> instance.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly UnsafeArray<T> AsUnsafeArray() => new((T*)_array.GetUnsafePtr(), _count);
|
||||
|
||||
/// <summary>
|
||||
/// Invalid constructor, use <see cref="UnsafeList(int, Allocator, AllocationOption)"/> or <see cref="UnsafeList(int, ref AllocationHandle, AllocationOption)"/> instead.
|
||||
/// </summary>
|
||||
public UnsafeList()
|
||||
: this(0, Allocator.Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of UnsafeList with a specified number of initial capacity and an allocation handle.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Specifies the number of initial capacity to allocate in the list, which must be greater than zero.</param>
|
||||
/// <param name="handle">A reference to an AllocationHandle that manages the memory allocation for the array.</param>
|
||||
/// <param name="allocationOption">Specifies how the memory should be allocated.</param>
|
||||
public UnsafeList(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
_array = new UnsafeArray<T>(capacity, handle, allocationOption);
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of UnsafeList with a specified number of initial capacity and an allocation type.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Specifies the number of initial capacity to allocate in the list, which must be greater than zero.</param>
|
||||
/// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param>
|
||||
/// <param name="allocationOption">Determines how the memory should be allocated.</param>
|
||||
public UnsafeList(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||
: this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||
{
|
||||
}
|
||||
|
||||
private readonly void CheckNoResizeCapacity(int count)
|
||||
{
|
||||
CheckNoResizeCapacity(count, Count);
|
||||
}
|
||||
|
||||
private readonly void CheckNoResizeCapacity(int index, int count)
|
||||
{
|
||||
if (index + count > Capacity)
|
||||
{
|
||||
throw new Exception(
|
||||
$"AddNoResize assumes that list capacity is sufficient (Capacity {Capacity}, Size {Count}), requested count {count}!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly void CheckIndexCount(int index, int count)
|
||||
{
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Value for count {count} must be positive.");
|
||||
}
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Value for index {index} must be positive.");
|
||||
}
|
||||
|
||||
if (index > Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Value for index {index} is out of bounds.");
|
||||
}
|
||||
|
||||
if (index + count > Count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Value for count {count} is out of bounds.");
|
||||
}
|
||||
}
|
||||
|
||||
public readonly ReadOnlyUnsafeCollection<T> AsReadOnly()
|
||||
{
|
||||
return new ReadOnlyUnsafeCollection<T>((T*)_array.GetUnsafePtr(), _count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new element to the end of the list, resizing the internal array if necessary.
|
||||
/// </summary>
|
||||
/// <param name="value">The element to be added to the list.</param>
|
||||
public void Add(T value)
|
||||
{
|
||||
if (_count >= Capacity)
|
||||
{
|
||||
Resize(Math.Max(1, Capacity * 2));
|
||||
}
|
||||
|
||||
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified value to the collection without resizing the underlying storage.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to add to the collection.</param>
|
||||
public void AddNoResize(T value)
|
||||
{
|
||||
CheckNoResizeCapacity(1);
|
||||
|
||||
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a range of elements to the collection.
|
||||
/// </summary>
|
||||
/// <param name="values">A span containing the elements to add. The span must not exceed the specified <paramref name="count"/>.</param>
|
||||
/// <param name="count">The number of elements to add from the <paramref name="values"/> span. Must be non-negative and less than or
|
||||
/// equal to the length of <paramref name="values"/>.</param>
|
||||
public void AddRange(Span<T> values, int count)
|
||||
{
|
||||
var newSize = _count + count;
|
||||
if (newSize > Capacity)
|
||||
{
|
||||
Resize(Capacity + count);
|
||||
}
|
||||
|
||||
fixed (T* ptr = values)
|
||||
{
|
||||
MemCpy(UnsafeUtility.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), _count), ptr, (uint)(count * sizeof(T)));
|
||||
}
|
||||
|
||||
_count += count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the elements of the specified collection to the current list without resizing the underlying storage.
|
||||
/// </summary>
|
||||
/// <param name="collection">A read-only span containing the elements to add. The span must not exceed the available capacity.</param>
|
||||
public void AddRangeNoResize(ReadOnlySpan<T> collection)
|
||||
{
|
||||
CheckNoResizeCapacity(collection.Length);
|
||||
|
||||
fixed (T* pCollection = collection)
|
||||
{
|
||||
MemCpy(UnsafeUtility.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), _count), pCollection, (uint)(collection.Length * sizeof(T)));
|
||||
}
|
||||
|
||||
_count += collection.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a range of elements from a pointer to the collection without resizing the underlying storage.
|
||||
/// </summary>
|
||||
/// <param name="ptr">Points to the source data to be copied into the collection.</param>
|
||||
/// <param name="count">Indicates the number of elements to be added from the source data.</param>
|
||||
public void AddRangeNoResize(T* ptr, int count)
|
||||
{
|
||||
CheckNoResizeCapacity(count);
|
||||
|
||||
MemCpy(UnsafeUtility.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), _count), ptr, (uint)(count * sizeof(T)));
|
||||
_count += count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a range of elements from the list starting at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="start">The zero-based index at which to start removing elements.</param>
|
||||
/// <param name="length">The number of elements to remove.</param>
|
||||
public void RemoveRange(int start, int length)
|
||||
{
|
||||
CheckIndexCount(start, length);
|
||||
|
||||
if (length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var copyFrom = Math.Min(start + length, _count);
|
||||
MemCpy(UnsafeUtility.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), start),
|
||||
UnsafeUtility.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), copyFrom),
|
||||
(uint)((_count - copyFrom) * sizeof(T))
|
||||
);
|
||||
_count -= length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the element at the specified index from the collection.
|
||||
/// </summary>
|
||||
/// <param name="index">The zero-based index of the element to remove.</param>
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
RemoveRange(index, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a range of elements from the list starting at the specified index by swapping them with the last elements.
|
||||
/// </summary>
|
||||
/// <param name="start">The zero-based index at which to start removing elements.</param>
|
||||
/// <param name="length">The number of elements to remove.</param>
|
||||
public void RemoveRangeSwapBack(int start, int length)
|
||||
{
|
||||
CheckIndexCount(start, length);
|
||||
|
||||
if (length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var copyFrom = Math.Min(_count - length, start + length);
|
||||
MemCpy(UnsafeUtility.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), start),
|
||||
UnsafeUtility.ReadArrayElementUnsafe<T>(_array.GetUnsafePtr(), copyFrom),
|
||||
(uint)((_count - copyFrom) * sizeof(T))
|
||||
);
|
||||
_count -= length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the element at the specified index by swapping it with the last element and reducing the collection
|
||||
/// size.
|
||||
/// </summary>
|
||||
/// <param name="index">The zero-based index of the element to remove. Must be within the bounds of the collection.</param>
|
||||
public void RemoveAtSwapBack(int index)
|
||||
{
|
||||
RemoveRangeSwapBack(index, 1);
|
||||
}
|
||||
|
||||
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
_array.Resize(newSize, option);
|
||||
|
||||
if (_count > newSize)
|
||||
{
|
||||
_count = newSize;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_array.Clear();
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
return _array.GetUnsafePtr();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly Span<T> AsSpan()
|
||||
{
|
||||
return _array.AsSpan(0, _count);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly Span<T> AsSpan(int start, int length)
|
||||
{
|
||||
CheckIndexCount(start, length);
|
||||
return _array.AsSpan(start, length);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_array.Dispose();
|
||||
_count = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// A structure that implements a queue using unmanaged types for efficient memory management.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents the type of elements stored in the queue, which must be an unmanaged type for performance and safety.</typeparam>
|
||||
public unsafe struct UnsafeQueue<T> : IUnsafeCollection<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly UnsafeQueue<T>* _collection;
|
||||
private int _currentIndex;
|
||||
|
||||
// We assume _currentIndex will always be in range when accessed.
|
||||
public readonly ref T Current => ref _collection->_array[(_collection->_offset + _currentIndex) % _collection->Capacity];
|
||||
readonly T IEnumerator<T>.Current => Current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(UnsafeQueue<T>* collection)
|
||||
{
|
||||
_collection = collection;
|
||||
_currentIndex = -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentIndex++;
|
||||
return _currentIndex < _collection->_count;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentIndex = -1;
|
||||
}
|
||||
|
||||
public readonly void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private UnsafeArray<T> _array;
|
||||
private int _count;
|
||||
private int _offset;
|
||||
|
||||
public readonly int Count => _count;
|
||||
public readonly int Capacity => _array.Count;
|
||||
public readonly bool IsCreated => _array.IsCreated;
|
||||
|
||||
public readonly T this[int index]
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _array[index];
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
set => _array[index] = value;
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new((UnsafeQueue<T>*)UnsafeUtility.AddressOf(ref this));
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Invalid constructor. Use <see cref="UnsafeQueue(int, Allocator, AllocationOption)"/> or <see cref="UnsafeQueue(int, ref AllocationHandle, AllocationOption)"/> instead."/>
|
||||
/// </summary>
|
||||
public UnsafeQueue()
|
||||
: this(0, Allocator.Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
public UnsafeQueue(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
_array = new UnsafeArray<T>(capacity, handle, allocationOption);
|
||||
_count = 0;
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
public UnsafeQueue(int capacity, Allocator allocator, AllocationOption allocationType = AllocationOption.None)
|
||||
: this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationType)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a reference to the item at the front of the queue without removing it.
|
||||
/// </summary>
|
||||
/// <returns>A reference to the item at the front of the queue.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the queue is empty.</exception>
|
||||
public readonly ref T Peek()
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Queue is empty.");
|
||||
}
|
||||
|
||||
return ref UnsafeUtility.ReadArrayElementRef<T>(_array.GetUnsafePtr(), _offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to return the object at the top of the collection without removing it.
|
||||
/// </summary>
|
||||
/// <param name="value">The item at the front of the queue if the operation is successful; otherwise, the default value of <typeparamref name="T"/>.</param>
|
||||
/// <returns><see langword="true"/> if an object was returned successfully; otherwise, <see langword="false"/>.</returns>
|
||||
public readonly bool TryPeek(out T value)
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = _array[_offset];
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an element to the end of a collection, resizing if the current capacity is reached. The new element is
|
||||
/// stored in a circular buffer.
|
||||
/// </summary>
|
||||
/// <param name="value">The item to be added to the collection.</param>
|
||||
public void Enqueue(T value)
|
||||
{
|
||||
if (_count >= Capacity)
|
||||
{
|
||||
Resize(Math.Max(1, Capacity * 2));
|
||||
}
|
||||
|
||||
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), (_offset + _count) % Capacity, value);
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes and returns the element at the front of the queue. If the queue is empty, an exception is thrown.
|
||||
/// </summary>
|
||||
/// <returns>The element that was removed from the front of the queue.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when attempting to dequeue from an empty queue.</exception>
|
||||
public T Dequeue()
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Queue is empty.");
|
||||
}
|
||||
|
||||
var value = UnsafeUtility.ReadArrayElement<T>(_array.GetUnsafePtr(), _offset);
|
||||
_offset = (_offset + 1) % Capacity;
|
||||
_count--;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove and return an item from a collection. Returns a boolean indicating success or failure.
|
||||
/// </summary>
|
||||
/// <param name="value">The output variable that will hold the dequeued item if the operation is successful.</param>
|
||||
/// <returns>True if an item was successfully dequeued, otherwise false.</returns>
|
||||
public bool TryDequeue(out T value)
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = Dequeue();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
_array.Resize(newSize, option);
|
||||
|
||||
if (_count > newSize)
|
||||
{
|
||||
_count = newSize;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_array.Clear();
|
||||
_count = 0;
|
||||
_offset = 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
return _array.GetUnsafePtr();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_array.Dispose();
|
||||
_count = 0;
|
||||
_offset = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
internal class UnsafeSlotMapDebugView<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly UnsafeSlotMap<T> _slotMap;
|
||||
public UnsafeSlotMapDebugView(UnsafeSlotMap<T> slotMap)
|
||||
{
|
||||
_slotMap = slotMap;
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||
public T[] Items
|
||||
{
|
||||
get
|
||||
{
|
||||
var items = new List<T>(_slotMap.Count);
|
||||
var enumerator = _slotMap.GetEnumerator();
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
items.Add(enumerator.Current);
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an unsafe, high-performance slot map for storing and managing unmanaged values, supporting fast insertion,
|
||||
/// removal, and lookup by slot index and generation.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of value to store in the slot map. Must be unmanaged.</typeparam>
|
||||
[DebuggerTypeProxy(typeof(UnsafeSlotMapDebugView<>))]
|
||||
public unsafe struct UnsafeSlotMap<T> : IUnsafeCollection<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly UnsafeSlotMap<T>* _collection;
|
||||
private int _currentIndex;
|
||||
|
||||
public readonly ref T Current => ref _collection->_data[_currentIndex];
|
||||
readonly T IEnumerator<T>.Current => Current;
|
||||
readonly object? IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(UnsafeSlotMap<T>* collection)
|
||||
{
|
||||
_collection = collection;
|
||||
_currentIndex = 0;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentIndex = _collection->_validBits.NextSetBit(_currentIndex + 1);
|
||||
return _currentIndex != -1;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentIndex = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private UnsafeArray<T> _data;
|
||||
private UnsafeArray<int> _generations;
|
||||
private UnsafeQueue<int> _freeSlots;
|
||||
private UnsafeBitSet _validBits;
|
||||
|
||||
private int _count;
|
||||
private int _capacity;
|
||||
|
||||
public readonly int Count => _count;
|
||||
public readonly int Capacity => _capacity;
|
||||
|
||||
public readonly bool IsCreated => _data.IsCreated && _generations.IsCreated && _freeSlots.IsCreated && _validBits.IsCreated;
|
||||
|
||||
public Enumerator GetEnumerator() => new((UnsafeSlotMap<T>*)UnsafeUtility.AddressOf(ref this));
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Invalid constructor. Use <see cref="UnsafeSlotMap(int, Allocator, AllocationOption)"/> or <see cref="UnsafeSlotMap(int, ref AllocationHandle, AllocationOption)"/> instead."/>
|
||||
/// </summary>
|
||||
public UnsafeSlotMap()
|
||||
: this(0, Allocator.Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the UnsafeSlotMap class with the specified capacity, allocation handle, and
|
||||
/// allocation options.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The number of slots to allocate for the map. Must be greater than zero.</param>
|
||||
/// <param name="handle">A reference to the allocation handle used to manage memory for the slot map.</param>
|
||||
/// <param name="allocationOption">The allocation options to use when creating internal data structures. The default is AllocationOption.None.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when capacity is less than or equal to zero.</exception>
|
||||
public UnsafeSlotMap(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
|
||||
}
|
||||
|
||||
_data = new UnsafeArray<T>(capacity, handle, allocationOption);
|
||||
_generations = new UnsafeArray<int>(capacity, handle, allocationOption);
|
||||
_freeSlots = new UnsafeQueue<int>(capacity, handle, allocationOption);
|
||||
_validBits = new UnsafeBitSet(capacity, handle, allocationOption);
|
||||
|
||||
if (!allocationOption.HasFlag(AllocationOption.Clear))
|
||||
{
|
||||
_generations.AsSpan().Clear();
|
||||
}
|
||||
_validBits.ClearAll();
|
||||
|
||||
_count = 0;
|
||||
_capacity = capacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the UnsafeSlotMap class with the specified capacity, allocator, and allocation
|
||||
/// options.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial number of slots to allocate for the map. Must be greater than zero.</param>
|
||||
/// <param name="allocator">The allocator to use for memory management of the slot map.</param>
|
||||
/// <param name="allocationOption">The allocation option that determines how memory is allocated. The default is AllocationOption.None.</param>
|
||||
public UnsafeSlotMap(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||
: this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified item to the collection and returns the index of the slot where it was stored.
|
||||
/// </summary>
|
||||
/// <param name="item">The item to add to the collection.</param>
|
||||
/// <param name="generation">When this method returns, contains the generation number associated with the slot where the item was stored.</param>
|
||||
/// <returns>The index of the slot in which the item was stored.</returns>
|
||||
public int Add(T item, out int generation)
|
||||
{
|
||||
if (_count >= _capacity)
|
||||
{
|
||||
Resize(Math.Max(1, _capacity * 2));
|
||||
}
|
||||
|
||||
int index;
|
||||
if (_freeSlots.Count == 0)
|
||||
{
|
||||
index = _count;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = _freeSlots.Dequeue();
|
||||
}
|
||||
|
||||
_data[index] = item;
|
||||
_validBits.SetBit(index);
|
||||
|
||||
_count++;
|
||||
|
||||
generation = _generations[index];
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove the item at the specified slot index and generation from the collection.
|
||||
/// </summary>
|
||||
/// <param name="slotIndex">The zero-based index of the slot to remove. Must be within the valid range of slot indices.</param>
|
||||
/// <param name="generation">The generation value associated with the slot. Removal succeeds only if this matches the current generation of
|
||||
/// the slot.</param>
|
||||
/// <returns>true if the item was successfully removed; otherwise, false.</returns>
|
||||
public bool Remove(int slotIndex, int generation)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= _capacity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ref var gen = ref _generations[slotIndex];
|
||||
if (gen != generation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
gen++;
|
||||
_validBits.ClearBit(slotIndex);
|
||||
_freeSlots.Enqueue(slotIndex);
|
||||
|
||||
_count--;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified slot index contains a valid entry with the given generation.
|
||||
/// </summary>
|
||||
/// <param name="slotIndex">The zero-based index of the slot to check. Must be greater than or equal to 0 and less than the current capacity.</param>
|
||||
/// <param name="generation">The generation value to compare against the slot's generation.</param>
|
||||
/// <returns>true if the slot at the specified index is valid and its generation matches the specified value; otherwise, false.</returns>
|
||||
public readonly bool Contains(int slotIndex, int generation)
|
||||
{
|
||||
if (slotIndex < 0 || slotIndex >= _capacity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_validBits.IsSet(slotIndex) && _generations[slotIndex] == generation)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve the element at the specified slot index and generation.
|
||||
/// </summary>
|
||||
/// <param name="slotIndex">The zero-based index of the slot to retrieve. Must be within the valid range of slots.</param>
|
||||
/// <param name="generation">The generation identifier associated with the slot. Used to verify that the slot has not been replaced or
|
||||
/// invalidated.</param>
|
||||
/// <param name="value">When this method returns, contains the element at the specified slot and generation if found; otherwise, the
|
||||
/// default value for type <typeparamref name="T"/>.</param>
|
||||
/// <returns>true if the element at the specified slot index and generation is found; otherwise, false.</returns>
|
||||
public readonly bool TryGetElementAt(int slotIndex, int generation, out T value)
|
||||
{
|
||||
if (!Contains(slotIndex, generation))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = _data[slotIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the element stored at the specified slot index and generation.
|
||||
/// </summary>
|
||||
/// <param name="slotIndex">The zero-based index of the slot from which to retrieve the element. Must be within the valid range of allocated slots.</param>
|
||||
/// <param name="generation">The generation identifier associated with the slot. Used to ensure the element has not been replaced or removed since allocation.</param>
|
||||
/// <returns>The element of type <see cref="T"/> stored at the specified slot and generation.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="slotIndex"/> is less than zero or greater than or equal to the capacity.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the specified slot is not occupied or the generation does not match.</exception>
|
||||
public readonly T GetElementAt(int slotIndex, int generation)
|
||||
{
|
||||
if (!Contains(slotIndex, generation))
|
||||
{
|
||||
throw new InvalidOperationException("The specified slot is not occupied or the generation does not match.");
|
||||
}
|
||||
|
||||
return _data[slotIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a reference to the element at the specified slot index and generation, if it exists; otherwise, returns
|
||||
/// a null reference.
|
||||
/// </summary>
|
||||
/// <param name="slotIndex">The zero-based index of the slot to retrieve. Must be within the valid range of allocated slots.</param>
|
||||
/// <param name="generation">The expected generation value for the slot. Used to verify that the slot has not been recycled or replaced.</param>
|
||||
/// <param name="exist">When this method returns, contains <see langword="true"/> if a valid element exists at the specified slot and generation; otherwise, <see langword="false"/>.</param>
|
||||
/// <returns>A reference to the element of type <typeparamref name="T"/> at the specified slot and generation if it exists; otherwise, a null reference.</returns>
|
||||
public ref T GetElementReferenceAt(int slotIndex, int generation, out bool exist)
|
||||
{
|
||||
if (!Contains(slotIndex, generation))
|
||||
{
|
||||
exist = false;
|
||||
return ref Unsafe.NullRef<T>();
|
||||
}
|
||||
|
||||
exist = true;
|
||||
return ref _data[slotIndex];
|
||||
}
|
||||
|
||||
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
_data.Resize(newSize, option);
|
||||
_generations.Resize(newSize, option | AllocationOption.Clear);
|
||||
_freeSlots.Resize(newSize, option);
|
||||
_validBits.Resize(newSize, option);
|
||||
|
||||
_capacity = newSize;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_generations.Clear();
|
||||
_freeSlots.Clear();
|
||||
_validBits.ClearAll();
|
||||
|
||||
_count = 0;
|
||||
|
||||
Add(default, out _);
|
||||
}
|
||||
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
return (T*)_data.GetUnsafePtr() + 1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_data.Dispose();
|
||||
_generations.Dispose();
|
||||
_freeSlots.Dispose();
|
||||
_validBits.Dispose();
|
||||
|
||||
_count = 0;
|
||||
_capacity = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
internal class ConcurrentSparseSetDebugView<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly UnsafeSparseSet<T> _set;
|
||||
public ConcurrentSparseSetDebugView(UnsafeSparseSet<T> set)
|
||||
{
|
||||
_set = set;
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||
public T[] Items
|
||||
{
|
||||
get
|
||||
{
|
||||
var items = new T[_set.Count];
|
||||
var index = 0;
|
||||
foreach (var item in _set)
|
||||
{
|
||||
items[index++] = item;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A sparse set data structure that provides O(1) insertion, deletion, and lookup operations.
|
||||
/// The sparse set uses three arrays: a dense array for storing values, a sparse array for mapping indices,
|
||||
/// and a reverse array for mapping dense indices back to sparse indices.
|
||||
/// Sparse indices work like entity IDs and are automatically generated.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents a type that can be stored in the sparse set, constrained to unmanaged types for performance and safety.</typeparam>
|
||||
[DebuggerTypeProxy(typeof(ConcurrentSparseSetDebugView<>))]
|
||||
public unsafe struct UnsafeSparseSet<T> : IUnsafeCollection<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly UnsafeSparseSet<T>* _collection;
|
||||
private int _currentIndex;
|
||||
|
||||
public readonly ref T Current => ref _collection->_dense[_currentIndex];
|
||||
readonly T IEnumerator<T>.Current => Current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(UnsafeSparseSet<T>* collection)
|
||||
{
|
||||
_collection = collection;
|
||||
_currentIndex = 0;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentIndex++;
|
||||
return _currentIndex < _collection->_count;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentIndex = 0;
|
||||
}
|
||||
|
||||
public readonly void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private UnsafeArray<T> _dense;
|
||||
private UnsafeArray<int> _generations;
|
||||
private UnsafeArray<int> _sparse;
|
||||
private UnsafeArray<int> _reverse; // Maps dense index to sparse index. Since this is a general purpose sparse set, we have to include reverse array. In real world ecs, this should be replaced with entity id array.
|
||||
private UnsafeStack<int> _freeSparse;
|
||||
|
||||
private int _count;
|
||||
private int _nextId; // Next available sparse index
|
||||
private int _capacity;
|
||||
|
||||
public readonly int Count => _count;
|
||||
public readonly int Capacity => _capacity;
|
||||
public readonly bool IsCreated => _dense.IsCreated && _sparse.IsCreated && _reverse.IsCreated;
|
||||
|
||||
public Enumerator GetEnumerator() => new((UnsafeSparseSet<T>*)UnsafeUtility.AddressOf(ref this));
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an UnsafeSparseSet with a default size of 1 and uses the Persistent allocator.
|
||||
/// </summary>
|
||||
public UnsafeSparseSet()
|
||||
: this(1, Allocator.Persistent)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of UnsafeSparseSet with a specified capacity and an allocation handle.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Specifies the initial capacity of the sparse set, which must be greater than zero.</param>
|
||||
/// <param name="handle">A reference to an AllocationHandle that manages the memory allocation for the sparse set.</param>
|
||||
/// <param name="allocationOption">Specifies how the memory should be allocated.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified capacity is less than or equal to zero.</exception>
|
||||
public UnsafeSparseSet(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
|
||||
}
|
||||
|
||||
_dense = new UnsafeArray<T>(capacity, handle, allocationOption);
|
||||
_generations = new UnsafeArray<int>(capacity, handle, allocationOption);
|
||||
_sparse = new UnsafeArray<int>(capacity, handle, allocationOption);
|
||||
_reverse = new UnsafeArray<int>(capacity, handle, allocationOption);
|
||||
_freeSparse = new UnsafeStack<int>(capacity, handle, allocationOption);
|
||||
|
||||
if (!allocationOption.HasFlag(AllocationOption.Clear))
|
||||
{
|
||||
_generations.AsSpan().Clear();
|
||||
_sparse.AsSpan().Clear();
|
||||
}
|
||||
|
||||
_count = 0;
|
||||
_nextId = 0;
|
||||
_capacity = capacity;
|
||||
|
||||
_sparse.AsSpan().Fill(-1);
|
||||
_generations.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of UnsafeSparseSet with a specified capacity and an allocation type.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Specifies the initial capacity of the sparse set, which must be greater than zero.</param>
|
||||
/// <param name="allocator">Specifies the allocator to use for memory allocation, which determines the memory management strategy.</param>
|
||||
/// <param name="allocationOption">Determines how the memory should be allocated.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the specified capacity is less than or equal to zero.</exception>
|
||||
public UnsafeSparseSet(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||
: this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly ref T GetDenseReferenceUnchecked(int sparseIndex)
|
||||
{
|
||||
return ref _dense[_sparse[sparseIndex]];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a value to the sparse set and returns a unique sparse index for the value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to add to the sparse set.</param>
|
||||
/// <param name="generation">Outputs the generation number associated with the added value.</param>
|
||||
/// <returns>A unique sparse index that can be used to reference this value.</returns>
|
||||
public int Add(T value, out int generation)
|
||||
{
|
||||
if (!_freeSparse.TryPop(out var sparseIndex))
|
||||
{
|
||||
// Use the next available ID
|
||||
sparseIndex = _nextId++;
|
||||
|
||||
// Resize sparse array if necessary
|
||||
if (sparseIndex >= _sparse.Count)
|
||||
{
|
||||
ResizeSparse(sparseIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Resize dense arrays if necessary
|
||||
if (_count >= _capacity)
|
||||
{
|
||||
Resize(Math.Max(1, _capacity * 2));
|
||||
}
|
||||
|
||||
// Add the value to the dense array and update mappings
|
||||
_dense[_count] = value;
|
||||
|
||||
_sparse[sparseIndex] = _count;
|
||||
_reverse[_count] = sparseIndex;
|
||||
_count++;
|
||||
|
||||
generation = _generations[sparseIndex];
|
||||
return sparseIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the value at the specified sparse index.
|
||||
/// </summary>
|
||||
/// <param name="sparseIndex">The sparse index of the value to remove.</param>
|
||||
/// <param name="generation">The generation number associated with the sparse index to validate.</param>
|
||||
/// <returns>True if the value was removed, false if the sparse index was not found.</returns>
|
||||
public bool Remove(int sparseIndex, int generation)
|
||||
{
|
||||
if (!Contains(sparseIndex, generation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var denseIndex = _sparse[sparseIndex];
|
||||
var lastIndex = _count - 1;
|
||||
|
||||
if (denseIndex != lastIndex)
|
||||
{
|
||||
// Move the last element to the position of the removed element
|
||||
var lastValue = _dense[lastIndex];
|
||||
var lastSparseIndex = _reverse[lastIndex]; // Get sparse index of last element
|
||||
|
||||
_dense[denseIndex] = lastValue;
|
||||
_reverse[denseIndex] = lastSparseIndex;
|
||||
|
||||
// Update the sparse mapping for the moved element
|
||||
_sparse[lastSparseIndex] = denseIndex;
|
||||
}
|
||||
|
||||
// Mark the sparse index as unused and add to free list
|
||||
_sparse[sparseIndex] = -1;
|
||||
_generations[sparseIndex]++; // Increment generation to invalidate old references
|
||||
|
||||
_freeSparse.Push(sparseIndex);
|
||||
_count--;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the sparse set contains a value at the specified sparse index.
|
||||
/// </summary>
|
||||
/// <param name="sparseIndex">The sparse index to check.</param>
|
||||
/// <param name="generation">The generation number to validate against the stored generation.</param>
|
||||
/// <returns>True if the sparse index is valid and contains a value, false otherwise.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool Contains(int sparseIndex, int generation)
|
||||
{
|
||||
if (sparseIndex < 0 || sparseIndex >= _sparse.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var denseIndex = _sparse[sparseIndex];
|
||||
return denseIndex >= 0 && denseIndex < _count && _generations[denseIndex] == generation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value at the specified sparse index and generation.
|
||||
/// </summary>
|
||||
/// <param name="sparseIndex">The sparse index to retrieve the value from.</param>
|
||||
/// <param name="generation">The generation number to validate against the stored generation.</param>
|
||||
/// <param name="value">When this method returns, contains the value at the specified sparse index, if found.</param>
|
||||
/// <returns>True if the sparse index contains a value, false otherwise.</returns>
|
||||
public readonly bool TryGetValue(int sparseIndex, int generation, out T value)
|
||||
{
|
||||
if (Contains(sparseIndex, generation))
|
||||
{
|
||||
value = GetDenseReferenceUnchecked(sparseIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value at the specified sparse index and generation.
|
||||
/// </summary>
|
||||
/// <param name="sparseIndex">The sparse index to retrieve the value from.</param>
|
||||
/// <param name="generation">The generation number to validate against the stored generation.</param>
|
||||
/// <returns>The value at the specified sparse index.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the sparse index is not found.</exception>
|
||||
public readonly T GetValue(int sparseIndex, int generation)
|
||||
{
|
||||
if (!Contains(sparseIndex, generation))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(sparseIndex), "Sparse index and feneration not found in the set.");
|
||||
}
|
||||
|
||||
return GetDenseReferenceUnchecked(sparseIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets reference of the value at the specified sparse index and generation.
|
||||
/// </summary>
|
||||
/// <param name="sparseIndex">The sparse index to retrieve the value from.</param>
|
||||
/// <param name="generation">The generation number to validate against the stored generation.</param>
|
||||
/// <param name="exist">Outputs whether the sparse index exists in the set.</param>
|
||||
/// <returns>Reference of the value at the specified sparse index.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when the sparse index is not found.</exception>
|
||||
public readonly ref T GetValueReference(int sparseIndex, int generation, out bool exist)
|
||||
{
|
||||
if (!Contains(sparseIndex, generation))
|
||||
{
|
||||
exist = false;
|
||||
return ref Unsafe.NullRef<T>();
|
||||
}
|
||||
|
||||
exist = true;
|
||||
return ref GetDenseReferenceUnchecked(sparseIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the value at the specified sparse index.
|
||||
/// </summary>
|
||||
/// <param name="sparseIndex">The sparse index of the value to update.</param>
|
||||
/// <param name="generation">The generation number to validate against the stored generation.</param>
|
||||
/// <param name="value">The new value.</param>
|
||||
/// <returns>True if the value was updated, false if the sparse index was not found.</returns>
|
||||
public bool SetValue(int sparseIndex, int generation, T value)
|
||||
{
|
||||
if (!Contains(sparseIndex, generation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GetDenseReferenceUnchecked(sparseIndex) = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ResizeSparse(int newSize)
|
||||
{
|
||||
var oldSize = _sparse.Count;
|
||||
_sparse.Resize(newSize);
|
||||
_sparse.AsSpan()[oldSize..newSize].Fill(-1);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Clear()
|
||||
{
|
||||
if (!IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sparse.AsSpan().Fill(-1);
|
||||
_generations.AsSpan().Clear();
|
||||
|
||||
_count = 0;
|
||||
_nextId = 0;
|
||||
|
||||
Add(default, out _);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
if (newSize <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(newSize), "New size must be greater than zero.");
|
||||
}
|
||||
|
||||
_dense.Resize(newSize, option);
|
||||
_generations.Resize(newSize, option | AllocationOption.Clear);
|
||||
_reverse.Resize(newSize, option);
|
||||
|
||||
if (newSize > _sparse.Count)
|
||||
{
|
||||
ResizeSparse(newSize);
|
||||
}
|
||||
|
||||
_capacity = newSize;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
return (T*)_dense.GetUnsafePtr() + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the current sparse set to an UnsafeArray representation using its dense array.
|
||||
/// </summary>
|
||||
/// <returns>Returns a new UnsafeArray instance initialized with the dense array's pointer and count.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly UnsafeArray<T> AsUnsafeArray()
|
||||
{
|
||||
return new UnsafeArray<T>((T*)_dense.GetUnsafePtr(), _count);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
_dense.Dispose();
|
||||
_generations.Dispose();
|
||||
_sparse.Dispose();
|
||||
_reverse.Dispose();
|
||||
_freeSparse.Dispose();
|
||||
|
||||
_count = 0;
|
||||
_nextId = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Collections;
|
||||
|
||||
internal class UnsafeStackDebugView<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly UnsafeStack<T> _stack;
|
||||
public UnsafeStackDebugView(UnsafeStack<T> stack)
|
||||
{
|
||||
_stack = stack;
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||
public unsafe T[] Items
|
||||
{
|
||||
get
|
||||
{
|
||||
var items = new T[_stack.Count];
|
||||
var pItems = (T*)_stack.GetUnsafePtr();
|
||||
for (int i = 0; i < _stack.Count; i++)
|
||||
{
|
||||
items[i] = pItems[i];
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a high-performance, unsafe stack data structure for unmanaged types, supporting manual memory management
|
||||
/// and allocation control.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements stored in the stack. Must be an unmanaged type.</typeparam>
|
||||
[DebuggerTypeProxy(typeof(UnsafeStackDebugView<>))]
|
||||
public unsafe struct UnsafeStack<T> : IUnsafeCollection<T>
|
||||
where T : unmanaged
|
||||
{
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
private readonly UnsafeStack<T>* _collection;
|
||||
private int _index;
|
||||
|
||||
public readonly ref T Current => ref _collection->_array[_index];
|
||||
readonly T IEnumerator<T>.Current => Current;
|
||||
readonly object IEnumerator.Current => Current;
|
||||
|
||||
public Enumerator(UnsafeStack<T>* collection)
|
||||
{
|
||||
_collection = collection;
|
||||
_index = collection->Count;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
_index--;
|
||||
return _index >= 0;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = _collection->Count;
|
||||
}
|
||||
|
||||
public readonly void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private UnsafeArray<T> _array;
|
||||
private int _count;
|
||||
|
||||
public readonly int Count => _count;
|
||||
public readonly int Capacity => _array.Count;
|
||||
public readonly bool IsCreated => _array.IsCreated;
|
||||
|
||||
public Enumerator GetEnumerator() => new((UnsafeStack<T>*)UnsafeUtility.AddressOf(ref this));
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Invalid constructor, use <see cref="UnsafeStack(int, Allocator, AllocationOption)"/> or <see cref="UnsafeStack(int, ref AllocationHandle, AllocationOption)"/> instead.
|
||||
/// </summary>
|
||||
public UnsafeStack()
|
||||
: this(0, Allocator.Invalid)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the UnsafeStack class with the specified initial capacity and allocation options.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The number of elements the stack can initially hold. Must be greater than zero.</param>
|
||||
/// <param name="handle">A reference to an AllocationHandle used to manage the underlying memory allocation for the stack.</param>
|
||||
/// <param name="allocationOption">Specifies additional options for memory allocation. The default is AllocationOption.None.</param>
|
||||
public UnsafeStack(int capacity, AllocationHandle handle, AllocationOption allocationOption = AllocationOption.None)
|
||||
{
|
||||
_array = new UnsafeArray<T>(capacity, handle, allocationOption);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the UnsafeStack class with the specified initial capacity, allocator, and
|
||||
/// allocation options.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial number of elements that the stack can hold. Must be greater than zero.</param>
|
||||
/// <param name="allocator">The allocator to use for memory management of the stack's storage.</param>
|
||||
/// <param name="allocationOption">The allocation option that determines how memory is allocated for the stack. The default is AllocationOption.None.</param>
|
||||
public UnsafeStack(int capacity, Allocator allocator, AllocationOption allocationOption = AllocationOption.None)
|
||||
: this(capacity, AllocationManager.GetAllocationHandle(allocator), allocationOption)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an element to the top of the stack.
|
||||
/// </summary>
|
||||
/// <param name="value">The element to add to the stack.</param>
|
||||
public void Push(T value)
|
||||
{
|
||||
if (_count >= Capacity)
|
||||
{
|
||||
Resize(Math.Max(1, Capacity * 2));
|
||||
}
|
||||
|
||||
UnsafeUtility.WriteArrayElement(_array.GetUnsafePtr(), _count, value);
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes and returns the object at the top of the stack.
|
||||
/// </summary>
|
||||
/// <returns>The object removed from the top of the stack.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the stack is empty.</exception>
|
||||
public T Pop()
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Stack is empty.");
|
||||
}
|
||||
|
||||
_count--;
|
||||
return _array[_count];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove and return the object at the top of the stack.
|
||||
/// </summary>
|
||||
/// <param name="value">When this method returns, contains the object removed from the top of the stack, if the operation succeeded;
|
||||
/// otherwise, the default value of <typeparamref name="T"/>.</param>
|
||||
/// <returns>true if an object was successfully removed and returned from the stack; otherwise, false.</returns>
|
||||
public bool TryPop(out T value)
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
_count--;
|
||||
value = _array[_count];
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the item at the top of the stack without removing it.
|
||||
/// </summary>
|
||||
/// <returns>The item of type <typeparamref name="T"/> at the top of the stack.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the stack is empty.</exception>
|
||||
public readonly T Peek()
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Stack is empty.");
|
||||
}
|
||||
|
||||
return _array[_count - 1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to return the item at the top of the stack without removing it.
|
||||
/// </summary>
|
||||
/// <param name="value">When this method returns, contains the item at the top of the stack if the stack is not empty; otherwise, the default value of <typeparamref name="T"/>.</param>
|
||||
/// <returns><see langword="true"/> if an item was successfully returned; otherwise, <see langword="false"/>.</returns>
|
||||
public readonly bool TryPeek(out T value)
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = _array[_count - 1];
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Resize(int newSize, AllocationOption option = AllocationOption.None)
|
||||
{
|
||||
_array.Resize(newSize, option);
|
||||
|
||||
if (_count > newSize)
|
||||
{
|
||||
_count = newSize;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_array.Clear();
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void* GetUnsafePtr()
|
||||
{
|
||||
return _array.GetUnsafePtr();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_array.Dispose();
|
||||
_count = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel;
|
||||
|
||||
/// <summary>
|
||||
/// A structure that encapsulates a function pointer and provides methods to convert between
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This structure used marshalling to convert between function pointers and delegates, which is not ideal for high-performance scenarios.
|
||||
/// Use this only when necessary, and prefer using <c>delegate* unmanaged</c> for better performance.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The delegate type that the function pointer represents.</typeparam>
|
||||
public readonly struct FunctionPointer<T>
|
||||
where T : Delegate
|
||||
{
|
||||
private readonly nint _ptr;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the native function pointer associated with this function pointer instance.
|
||||
/// </summary>
|
||||
public readonly nint Pointer => _ptr;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delegate instance associated with the specified function pointer.
|
||||
/// </summary>
|
||||
/// <remarks>This property uses <see
|
||||
/// cref="Marshal.GetDelegateForFunctionPointer{TDelegate}"/> to convert the function
|
||||
/// pointer to a delegate. Ensure that the function pointer is valid and compatible with the delegate type
|
||||
/// <typeparamref name="T"/>.</remarks>
|
||||
public T Delegate => Marshal.GetDelegateForFunctionPointer<T>(_ptr);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of this function pointer with the following native pointer.
|
||||
/// </summary>
|
||||
/// <param name="ptr"></param>
|
||||
public FunctionPointer(T func)
|
||||
{
|
||||
_ptr = Marshal.GetFunctionPointerForDelegate<T>(func);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel;
|
||||
|
||||
/// <summary>
|
||||
/// An exception that is thrown when a memory leak is detected.
|
||||
/// </summary>
|
||||
/// <param name="Infos">An array of AllocationInfo containing details about the memory leaks.</param>
|
||||
public class MemoryLeakException : Exception
|
||||
{
|
||||
private readonly string _message;
|
||||
|
||||
public override string Message => _message;
|
||||
|
||||
public MemoryLeakException(ReadOnlySpan<AllocationInfo> infos)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine($"Found {infos.Length} memory lakes!");
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
stringBuilder.AppendLine(GetMessage(info.StackTrace));
|
||||
}
|
||||
|
||||
_message = stringBuilder.ToString();
|
||||
}
|
||||
|
||||
public MemoryLeakException(string message)
|
||||
{
|
||||
_message = message;
|
||||
}
|
||||
|
||||
private static string GetMessage(StackTrace? stackTrace)
|
||||
{
|
||||
if (stackTrace == null)
|
||||
{
|
||||
return "No stack trace available.";
|
||||
}
|
||||
|
||||
var stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine("Memory leak detected at: ");
|
||||
|
||||
for (var i = 0; i < stackTrace.FrameCount; i++)
|
||||
{
|
||||
var frame = stackTrace.GetFrame(i);
|
||||
if (frame != null)
|
||||
{
|
||||
stringBuilder.AppendLine($"File: {frame.GetFileName()}, Method: {DiagnosticMethodInfo.Create(frame)?.Name}, Line: {frame.GetFileLineNumber()}");
|
||||
}
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
225
Misaki.HighPerformance.LowLevel/contentFiles/cs/any/Ptr.cs
Normal file
225
Misaki.HighPerformance.LowLevel/contentFiles/cs/any/Ptr.cs
Normal file
@@ -0,0 +1,225 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a strongly-typed, read-only pointer to an unmanaged value of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When a pointer is wrapped in this struct, it indicates that the code does not intend to manage the lifetime of the data being pointed to.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The unmanaged type to which the pointer refers.</typeparam>
|
||||
public readonly unsafe struct SharedPtr<T> : IEquatable<SharedPtr<T>>
|
||||
where T : unmanaged
|
||||
{
|
||||
private readonly T* _value;
|
||||
|
||||
public SharedPtr(T* value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public T* Get()
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
|
||||
public bool Equals(SharedPtr<T> other)
|
||||
{
|
||||
return _value == other._value;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is SharedPtr<T> ptr && Equals(ptr);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return ((nint)_value).GetHashCode();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator T*(SharedPtr<T> ptr)
|
||||
{
|
||||
return ptr._value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator SharedPtr<T>(T* value)
|
||||
{
|
||||
return new SharedPtr<T>(value);
|
||||
}
|
||||
|
||||
public static bool operator ==(SharedPtr<T> left, SharedPtr<T> right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SharedPtr<T> left, SharedPtr<T> right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides exclusive ownership and management of an unmanaged pointer to a value of type <typeparamref name="T"/>.
|
||||
/// Ensures that the pointer is not shared and can be safely transferred or detached.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="UniquePtr{T}"/> is designed to encapsulate a raw pointer, enforcing unique ownership semantics similar to C++'s std::unique_ptr.
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The unmanaged type of the value to which the pointer refers.</typeparam>
|
||||
[NonCopyable]
|
||||
public unsafe struct UniquePtr<T> : IEquatable<UniquePtr<T>>
|
||||
where T : unmanaged
|
||||
{
|
||||
private T* _value;
|
||||
|
||||
public UniquePtr(T* value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public readonly T* Get()
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
|
||||
public readonly SharedPtr<T> Share()
|
||||
{
|
||||
return new SharedPtr<T>(_value);
|
||||
}
|
||||
|
||||
public T* Detach()
|
||||
{
|
||||
var temp = _value;
|
||||
_value = null;
|
||||
return temp;
|
||||
}
|
||||
|
||||
public readonly bool Equals(UniquePtr<T> other)
|
||||
{
|
||||
return _value == other._value;
|
||||
}
|
||||
|
||||
public override readonly bool Equals(object? obj)
|
||||
{
|
||||
return obj is SharedPtr<T> ptr && Equals(ptr);
|
||||
}
|
||||
|
||||
public override readonly int GetHashCode()
|
||||
{
|
||||
return ((nint)_value).GetHashCode();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator T*(UniquePtr<T> ptr)
|
||||
{
|
||||
return ptr._value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static implicit operator UniquePtr<T>(T* value)
|
||||
{
|
||||
return new UniquePtr<T>(value);
|
||||
}
|
||||
|
||||
public static bool operator ==(UniquePtr<T> left, UniquePtr<T> right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(UniquePtr<T> left, UniquePtr<T> right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
public ref struct Ref<T> : IEquatable<Ref<T>>
|
||||
{
|
||||
private ref T _value;
|
||||
|
||||
public Ref(ref T value)
|
||||
{
|
||||
_value = ref value;
|
||||
}
|
||||
|
||||
public ref T Get()
|
||||
{
|
||||
return ref _value;
|
||||
}
|
||||
|
||||
public bool Equals(Ref<T> other)
|
||||
{
|
||||
return Unsafe.AreSame(ref _value, ref other._value);
|
||||
}
|
||||
|
||||
[Obsolete("Equals() on Ref will always throw an exception. Use the equality operator instead.")]
|
||||
#pragma warning disable CS0809 // Obsolete member overrides non-obsolete member
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
[Obsolete("GetHashCode() on Ref will always throw an exception.")]
|
||||
public override int GetHashCode()
|
||||
#pragma warning restore CS0809 // Obsolete member overrides non-obsolete member
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public static bool operator ==(Ref<T> left, Ref<T> right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(Ref<T> left, Ref<T> right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a wrapper for a value type that implements <see cref="IDisposable"/>, ensuring proper disposal of the contained value.
|
||||
/// </summary>
|
||||
/// <remarks>The <see cref="Owner{T}"/> class manages the lifetime of the contained value by calling its
|
||||
/// <see cref="IDisposable.Dispose"/> method when the wrapper is disposed or finalized. After disposal, accessing the value
|
||||
/// will throw an <see cref="ObjectDisposedException"/>.</remarks>
|
||||
/// <typeparam name="T">The value type to wrap. Must be a struct that implements <see cref="IDisposable"/>.</typeparam>
|
||||
public class Owner<T> : IDisposable
|
||||
where T : struct, IDisposable
|
||||
{
|
||||
private T _value;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public Owner(T value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
~Owner()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public ref T Get()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
return ref _value;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_value.Dispose();
|
||||
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Utilities;
|
||||
|
||||
public unsafe struct HashMapHelper<TKey> : IDisposable
|
||||
where TKey : unmanaged, IEquatable<TKey>
|
||||
{
|
||||
internal unsafe struct Enumerator
|
||||
{
|
||||
public HashMapHelper<TKey>* buffer;
|
||||
public int index;
|
||||
public int bucketIndex;
|
||||
public int nextIndex;
|
||||
|
||||
public Enumerator(HashMapHelper<TKey>* data)
|
||||
{
|
||||
buffer = data;
|
||||
index = -1;
|
||||
bucketIndex = 0;
|
||||
nextIndex = -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext()
|
||||
{
|
||||
return buffer->MoveNext(ref bucketIndex, ref nextIndex, out index);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
index = -1;
|
||||
bucketIndex = 0;
|
||||
nextIndex = -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public KeyValuePair<TKey, TValue> GetCurrent<TValue>()
|
||||
where TValue : unmanaged
|
||||
{
|
||||
return new KeyValuePair<TKey, TValue>(buffer->_keys[index], UnsafeUtility.ReadArrayElementRef<TValue>(buffer->_buffer, index));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TKey GetCurrentKey()
|
||||
{
|
||||
if (index != -1)
|
||||
{
|
||||
return buffer->_keys[index];
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// This buffer has 4 parts: TValue, TKey, Next, Buckets.
|
||||
private byte* _buffer;
|
||||
|
||||
internal TKey* _keys;
|
||||
internal int* _next;
|
||||
internal int* _buckets;
|
||||
|
||||
private int _count;
|
||||
private int _capacity;
|
||||
private int _bucketCapacity;
|
||||
private int _allocatedIndex;
|
||||
private int _firstFreeIndex;
|
||||
|
||||
private readonly int _alignment;
|
||||
private readonly int _sizeOfTValue;
|
||||
private readonly int _log2MinGrowth;
|
||||
|
||||
private MemoryHandle _memoryHandle;
|
||||
private AllocationHandle _allocationHandle;
|
||||
|
||||
public const int MINIMAL_CAPACITY = 64;
|
||||
|
||||
public readonly byte* Buffer => _buffer;
|
||||
|
||||
public readonly int Count => _count;
|
||||
|
||||
public readonly int Capacity => _capacity;
|
||||
|
||||
public readonly bool IsEmpty => !IsCreated || _count == 0;
|
||||
|
||||
public readonly bool IsCreated
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
if (_allocationHandle.IsValid != null)
|
||||
{
|
||||
return _allocationHandle.IsValid(_allocationHandle.State, _memoryHandle);
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int CalculateDataSize(int capacity, int bucketCapacity, int sizeOfTValue, out int outKeyOffset, out int outNextOffset, out int outBucketOffset)
|
||||
{
|
||||
var sizeOfTKey = sizeof(TKey);
|
||||
var sizeOfInt = sizeof(int);
|
||||
|
||||
var valuesSize = sizeOfTValue * capacity;
|
||||
var keysSize = sizeOfTKey * capacity;
|
||||
var nextSize = sizeOfInt * capacity;
|
||||
var bucketSize = sizeOfInt * bucketCapacity;
|
||||
var totalSize = valuesSize + keysSize + nextSize + bucketSize;
|
||||
|
||||
outKeyOffset = 0 + valuesSize;
|
||||
outNextOffset = outKeyOffset + keysSize;
|
||||
outBucketOffset = outNextOffset + nextSize;
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
public HashMapHelper(int capacity, int sizeOfTValue, int alignOfTValue, uint minGrowth, AllocationHandle handle, AllocationOption allocationOption)
|
||||
{
|
||||
if (capacity <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be greater than zero.");
|
||||
}
|
||||
|
||||
if (sizeOfTValue < 0 || alignOfTValue < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(sizeOfTValue), "Size or alignment of TValue can not be less than zero.");
|
||||
}
|
||||
|
||||
_capacity = CalcCapacityCeilPow2(capacity);
|
||||
_bucketCapacity = _capacity * 2;
|
||||
|
||||
var alignOfKey = (int)AlignOf<TKey>();
|
||||
var alignOfInt = (int)AlignOf<int>();
|
||||
var maxDataAlign = Math.Max(Math.Max(alignOfTValue, alignOfKey), alignOfInt);
|
||||
|
||||
_alignment = maxDataAlign;
|
||||
_sizeOfTValue = sizeOfTValue;
|
||||
_log2MinGrowth = BitOperations.Log2(minGrowth);
|
||||
|
||||
_allocationHandle = handle;
|
||||
|
||||
var totalSize = CalculateDataSize(_capacity, _bucketCapacity, sizeOfTValue,
|
||||
out var keyOffset, out var nextOffset, out var bucketOffset);
|
||||
|
||||
AllocateBuffer(totalSize, keyOffset, nextOffset, bucketOffset, allocationOption);
|
||||
Clear();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Conditional("ENABLE_COLLECTION_CHECKS")]
|
||||
private readonly void ThrowIfNotCreated()
|
||||
{
|
||||
if (!IsCreated)
|
||||
{
|
||||
throw new InvalidOperationException("The HashMapHelper is not created.");
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int CeilPow2(int x)
|
||||
{
|
||||
x -= 1;
|
||||
x |= x >> 1;
|
||||
x |= x >> 2;
|
||||
x |= x >> 4;
|
||||
x |= x >> 8;
|
||||
x |= x >> 16;
|
||||
return x + 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly int CalcCapacityCeilPow2(int capacity)
|
||||
{
|
||||
capacity = Math.Max(Math.Max(1, _count), capacity);
|
||||
var newCapacity = Math.Max(capacity, 1 << _log2MinGrowth);
|
||||
var result = CeilPow2(newCapacity);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly int GetBucket(in TKey key)
|
||||
{
|
||||
var h = (uint)key.GetHashCode();
|
||||
return (int)(h & (uint)(_bucketCapacity - 1));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private readonly void CheckIndexOutOfBounds(int idx)
|
||||
{
|
||||
if ((uint)idx >= (uint)_capacity)
|
||||
{
|
||||
throw new InvalidOperationException($"Index {idx} is out of bounds for the hash map with capacity {_capacity}.");
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void AllocateBuffer(int totalSize, int keyOffset, int nextOffset, int bucketOffset, AllocationOption allocationOption)
|
||||
{
|
||||
if (_allocationHandle.Alloc == null)
|
||||
{
|
||||
throw new InvalidOperationException("Target allocation handle does not support allocation.");
|
||||
}
|
||||
|
||||
MemoryHandle memHandle;
|
||||
var buf = (byte*)_allocationHandle.Alloc(_allocationHandle.State, (uint)totalSize, (nuint)_alignment, allocationOption, &memHandle);
|
||||
|
||||
_buffer = buf;
|
||||
_keys = (TKey*)(_buffer + keyOffset);
|
||||
_next = (int*)(_buffer + nextOffset);
|
||||
_buckets = (int*)(_buffer + bucketOffset);
|
||||
_memoryHandle = memHandle;
|
||||
}
|
||||
|
||||
private void ResizeExact(int newCapacity, int newBucketCapacity)
|
||||
{
|
||||
var totalSize = CalculateDataSize(newCapacity, newBucketCapacity, _sizeOfTValue,
|
||||
out var keyOffset, out var nextOffset, out var bucketOffset);
|
||||
|
||||
var oldBuffer = _buffer;
|
||||
var oldKeys = _keys;
|
||||
var oldNext = _next;
|
||||
var oldBuckets = _buckets;
|
||||
var oldBucketCapacity = _bucketCapacity;
|
||||
var oldMemoryHandle = _memoryHandle;
|
||||
|
||||
AllocateBuffer(totalSize, keyOffset, nextOffset, bucketOffset, AllocationOption.None);
|
||||
_capacity = newCapacity;
|
||||
_bucketCapacity = newBucketCapacity;
|
||||
|
||||
Clear();
|
||||
|
||||
for (int i = 0, num = oldBucketCapacity; i < num; ++i)
|
||||
{
|
||||
for (var idx = oldBuckets[i]; idx != -1; idx = oldNext[idx])
|
||||
{
|
||||
var newIdx = TryAdd(oldKeys[idx]);
|
||||
MemCpy(_buffer + _sizeOfTValue * newIdx, oldBuffer + _sizeOfTValue * idx, (nuint)_sizeOfTValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (_allocationHandle.Free != null)
|
||||
{
|
||||
_allocationHandle.Free(_allocationHandle.State, oldBuffer, oldMemoryHandle);
|
||||
}
|
||||
}
|
||||
|
||||
public void Resize(int newCapacity)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
newCapacity = Math.Max(newCapacity, _count);
|
||||
var newBucketCapacity = CeilPow2(newCapacity * 2);
|
||||
|
||||
if (_capacity == newCapacity && _bucketCapacity == newBucketCapacity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ResizeExact(newCapacity, newBucketCapacity);
|
||||
}
|
||||
|
||||
public void TrimExcess()
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
var capacity = CalcCapacityCeilPow2(_count);
|
||||
ResizeExact(capacity, capacity * 2);
|
||||
}
|
||||
|
||||
public int Find(in TKey key)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
if (_allocatedIndex <= 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// First find the slot based on the hash
|
||||
var bucket = GetBucket(key);
|
||||
var entryIdx = _buckets[bucket];
|
||||
|
||||
if ((uint)entryIdx < (uint)_capacity)
|
||||
{
|
||||
var nextPtrs = _next;
|
||||
while (!UnsafeUtility.ReadArrayElement<TKey>(_keys, entryIdx).Equals(key))
|
||||
{
|
||||
entryIdx = nextPtrs[entryIdx];
|
||||
if ((uint)entryIdx >= (uint)_capacity)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return entryIdx;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int TryAdd(in TKey key)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
var k = key;
|
||||
if (Find(in key) != -1)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Allocate an entry from the free list
|
||||
int idx;
|
||||
int* next;
|
||||
|
||||
if (_allocatedIndex >= _capacity && _firstFreeIndex < 0)
|
||||
{
|
||||
var newCap = CalcCapacityCeilPow2(_capacity + (1 << _log2MinGrowth));
|
||||
Resize(newCap);
|
||||
}
|
||||
|
||||
idx = _firstFreeIndex;
|
||||
|
||||
if (idx >= 0)
|
||||
{
|
||||
_firstFreeIndex = _next[idx];
|
||||
}
|
||||
else
|
||||
{
|
||||
idx = _allocatedIndex++;
|
||||
}
|
||||
|
||||
CheckIndexOutOfBounds(idx);
|
||||
|
||||
UnsafeUtility.WriteArrayElement(_keys, idx, key);
|
||||
var bucket = GetBucket(key);
|
||||
|
||||
// Add the index to the hash-map
|
||||
next = _next;
|
||||
next[idx] = _buckets[bucket];
|
||||
_buckets[bucket] = idx;
|
||||
_count++;
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
public int TryRemove(in TKey key)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
if (_capacity == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var removed = 0;
|
||||
|
||||
// First find the slot based on the hash
|
||||
var bucket = GetBucket(key);
|
||||
|
||||
var prevEntry = -1;
|
||||
var entryIdx = _buckets[bucket];
|
||||
|
||||
while (entryIdx >= 0 && entryIdx < _capacity)
|
||||
{
|
||||
if (UnsafeUtility.ReadArrayElement<TKey>(_keys, entryIdx).Equals(key))
|
||||
{
|
||||
removed++;
|
||||
|
||||
// Found matching element, remove it
|
||||
if (prevEntry < 0)
|
||||
{
|
||||
_buckets[bucket] = _next[entryIdx];
|
||||
}
|
||||
else
|
||||
{
|
||||
_next[prevEntry] = _next[entryIdx];
|
||||
}
|
||||
|
||||
// And free the index
|
||||
var nextIdx = _next[entryIdx];
|
||||
_next[entryIdx] = _firstFreeIndex;
|
||||
_firstFreeIndex = entryIdx;
|
||||
entryIdx = nextIdx;
|
||||
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
prevEntry = entryIdx;
|
||||
entryIdx = _next[entryIdx];
|
||||
}
|
||||
}
|
||||
|
||||
_count -= removed;
|
||||
return 0 != removed ? removed : -1;
|
||||
}
|
||||
|
||||
public bool TryGetValue<TValue>(in TKey key, out TValue item)
|
||||
where TValue : unmanaged
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
var idx = Find(key);
|
||||
|
||||
if (idx != -1)
|
||||
{
|
||||
item = UnsafeUtility.ReadArrayElement<TValue>(_buffer, idx);
|
||||
return true;
|
||||
}
|
||||
|
||||
item = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public ref TValue GetValueRef<TValue>(in TKey key, out bool exists)
|
||||
where TValue : unmanaged
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
var idx = Find(key);
|
||||
if (idx != -1)
|
||||
{
|
||||
exists = true;
|
||||
return ref UnsafeUtility.ReadArrayElementRef<TValue>(_buffer, idx);
|
||||
}
|
||||
|
||||
exists = false;
|
||||
return ref Unsafe.NullRef<TValue>();
|
||||
}
|
||||
|
||||
public bool MoveNextSearch(ref int bucketIndex, ref int nextIndex, out int index)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
for (int i = bucketIndex, num = _bucketCapacity; i < num; ++i)
|
||||
{
|
||||
var idx = _buckets[i];
|
||||
|
||||
if (idx != -1)
|
||||
{
|
||||
index = idx;
|
||||
bucketIndex = i + 1;
|
||||
nextIndex = _next[idx];
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
index = -1;
|
||||
bucketIndex = _bucketCapacity;
|
||||
nextIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool MoveNext(ref int bucketIndex, ref int nextIndex, out int index)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
if (nextIndex != -1)
|
||||
{
|
||||
index = nextIndex;
|
||||
nextIndex = _next[nextIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
return MoveNextSearch(ref bucketIndex, ref nextIndex, out index);
|
||||
}
|
||||
|
||||
internal UnsafeArray<TKey> GetKeyArray(Allocator allocator)
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
var result = new UnsafeArray<TKey>(_count, allocator);
|
||||
|
||||
for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; i++)
|
||||
{
|
||||
var bucket = _buckets[i];
|
||||
|
||||
while (bucket != -1)
|
||||
{
|
||||
result[count++] = UnsafeUtility.ReadArrayElement<TKey>(_keys, bucket);
|
||||
bucket = _next[bucket];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal UnsafeArray<TValue> GetValueArray<TValue>(Allocator allocator)
|
||||
where TValue : unmanaged
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
var result = new UnsafeArray<TValue>(_count, allocator);
|
||||
|
||||
for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; ++i)
|
||||
{
|
||||
var bucket = _buckets[i];
|
||||
|
||||
while (bucket != -1)
|
||||
{
|
||||
result[count++] = UnsafeUtility.ReadArrayElement<TValue>(_buffer, bucket);
|
||||
bucket = _next[bucket];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public UnsafeArray<KeyValuePair<TKey, TValue>> GetKeyValueArrays<TValue>(Allocator allocator)
|
||||
where TValue : unmanaged
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
var result = new UnsafeArray<KeyValuePair<TKey, TValue>>(_count, allocator);
|
||||
|
||||
for (int i = 0, count = 0, max = result.Count, capacity = _bucketCapacity; i < capacity && count < max; i++)
|
||||
{
|
||||
var bucket = _buckets[i];
|
||||
|
||||
while (bucket != -1)
|
||||
{
|
||||
result[count] = new(UnsafeUtility.ReadArrayElement<TKey>(_keys, bucket),
|
||||
UnsafeUtility.ReadArrayElement<TValue>(_buffer, bucket));
|
||||
|
||||
count++;
|
||||
bucket = _next[bucket];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ThrowIfNotCreated();
|
||||
|
||||
MemSet(_buckets, 0xff, (nuint)_bucketCapacity * sizeof(int));
|
||||
MemSet(_next, 0xff, (nuint)_capacity * sizeof(int));
|
||||
|
||||
_count = 0;
|
||||
_firstFreeIndex = -1;
|
||||
_allocatedIndex = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!IsCreated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_allocationHandle.Free != null)
|
||||
{
|
||||
_allocationHandle.Free(_allocationHandle.State, _buffer, _memoryHandle);
|
||||
}
|
||||
|
||||
_buffer = null;
|
||||
_keys = null;
|
||||
_next = null;
|
||||
_buckets = null;
|
||||
|
||||
_count = 0;
|
||||
_capacity = 0;
|
||||
_bucketCapacity = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Utilities;
|
||||
|
||||
public static unsafe partial class MemoryUtility
|
||||
{
|
||||
[DoesNotReturn]
|
||||
private static void ThrowMustBeNullTerminatedString()
|
||||
{
|
||||
throw new ArgumentException("Arg_MustBeNullTerminatedString");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static Vector128<byte> LoadVector128(ref byte start, nuint offset)
|
||||
=> Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.AddByteOffset(ref start, offset));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static Vector256<byte> LoadVector256(ref byte start, nuint offset)
|
||||
=> Unsafe.ReadUnaligned<Vector256<byte>>(ref Unsafe.AddByteOffset(ref start, offset));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static nuint GetByteVector128SpanLength(nuint offset, int length)
|
||||
=> (uint)((length - (int)offset) & ~(Vector128<byte>.Count - 1));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static nuint GetByteVector256SpanLength(nuint offset, int length)
|
||||
=> (uint)((length - (int)offset) & ~(Vector256<byte>.Count - 1));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static nuint GetByteVector512SpanLength(nuint offset, int length)
|
||||
=> (uint)((length - (int)offset) & ~(Vector512<byte>.Count - 1));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe nuint UnalignedCountVector128(byte* searchSpace)
|
||||
{
|
||||
var unaligned = (nint)searchSpace & (Vector128<byte>.Count - 1);
|
||||
return (uint)((Vector128<byte>.Count - unaligned) & (Vector128<byte>.Count - 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for the first occurrence of a null byte (0x00) in a given byte array.
|
||||
/// </summary>
|
||||
/// <param name="searchSpace">A pointer to the byte array where the search will be performed.</param>
|
||||
/// <returns>Returns the index of the first null byte found in the array..</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if the byte array is not null-terminated.</exception>"
|
||||
public static unsafe int IndexOfNullByte(byte* searchSpace)
|
||||
{
|
||||
const int Length = int.MaxValue;
|
||||
const uint uValue = 0; // Use uint for comparisons to avoid unnecessary 8->32 extensions
|
||||
nuint offset = 0; // Use nuint for arithmetic to avoid unnecessary 64->32->64 truncations
|
||||
var lengthToExamine = (nuint)(uint)Length;
|
||||
|
||||
if (Vector128.IsHardwareAccelerated)
|
||||
{
|
||||
// Avx2 branch also operates on Sse2 sizes, so check is combined.
|
||||
lengthToExamine = UnalignedCountVector128(searchSpace);
|
||||
}
|
||||
|
||||
SequentialScan:
|
||||
while (lengthToExamine >= 8)
|
||||
{
|
||||
lengthToExamine -= 8;
|
||||
|
||||
if (uValue == searchSpace[offset])
|
||||
goto Found;
|
||||
if (uValue == searchSpace[offset + 1])
|
||||
goto Found1;
|
||||
if (uValue == searchSpace[offset + 2])
|
||||
goto Found2;
|
||||
if (uValue == searchSpace[offset + 3])
|
||||
goto Found3;
|
||||
if (uValue == searchSpace[offset + 4])
|
||||
goto Found4;
|
||||
if (uValue == searchSpace[offset + 5])
|
||||
goto Found5;
|
||||
if (uValue == searchSpace[offset + 6])
|
||||
goto Found6;
|
||||
if (uValue == searchSpace[offset + 7])
|
||||
goto Found7;
|
||||
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
if (lengthToExamine >= 4)
|
||||
{
|
||||
lengthToExamine -= 4;
|
||||
|
||||
if (uValue == searchSpace[offset])
|
||||
goto Found;
|
||||
if (uValue == searchSpace[offset + 1])
|
||||
goto Found1;
|
||||
if (uValue == searchSpace[offset + 2])
|
||||
goto Found2;
|
||||
if (uValue == searchSpace[offset + 3])
|
||||
goto Found3;
|
||||
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
while (lengthToExamine > 0)
|
||||
{
|
||||
lengthToExamine -= 1;
|
||||
|
||||
if (uValue == searchSpace[offset])
|
||||
goto Found;
|
||||
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// We get past SequentialScan only if IsHardwareAccelerated is true; and remain length is greater than Vector length.
|
||||
// However, we still have the redundant check to allow the JIT to see that the code is unreachable and eliminate it when the platform does not
|
||||
// have hardware accelerated. After processing Vector lengths we return to SequentialScan to finish any remaining.
|
||||
if (Vector512.IsHardwareAccelerated)
|
||||
{
|
||||
if (offset < Length)
|
||||
{
|
||||
if ((((uint)searchSpace + offset) & (nuint)(Vector256<byte>.Count - 1)) != 0)
|
||||
{
|
||||
// Invert currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
|
||||
// with no upper bound e.g. String.strlen.
|
||||
// Start with a check on Vector128 to align to Vector256, before moving to processing Vector256.
|
||||
// This ensures we do not fault across memory pages while searching for an end of string.
|
||||
var search = Vector128.Load(searchSpace + offset);
|
||||
|
||||
// Same method as below
|
||||
var matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
|
||||
if (matches == 0)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector128<byte>.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
}
|
||||
}
|
||||
|
||||
if ((((uint)searchSpace + offset) & (nuint)(Vector512<byte>.Count - 1)) != 0)
|
||||
{
|
||||
// Invert currently aligned to Vector512 (is aligned to Vector256); this can cause a problem for searches
|
||||
// with no upper bound e.g. String.strlen.
|
||||
// Start with a check on Vector256 to align to Vector512, before moving to processing Vector256.
|
||||
// This ensures we do not fault across memory pages while searching for an end of string.
|
||||
var search = Vector256.Load(searchSpace + offset);
|
||||
|
||||
// Same method as below
|
||||
var matches = Vector256.Equals(Vector256<byte>.Zero, search).ExtractMostSignificantBits();
|
||||
if (matches == 0)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector256<byte>.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
}
|
||||
}
|
||||
lengthToExamine = GetByteVector512SpanLength(offset, Length);
|
||||
if (lengthToExamine > offset)
|
||||
{
|
||||
do
|
||||
{
|
||||
var search = Vector512.Load(searchSpace + offset);
|
||||
var matches = Vector512.Equals(Vector512<byte>.Zero, search).ExtractMostSignificantBits();
|
||||
// Note that MoveMask has converted the equal vector elements into a set of bit flags,
|
||||
// So the bit position in 'matches' corresponds to the element offset.
|
||||
if (matches == 0)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector512<byte>.Count;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
} while (lengthToExamine > offset);
|
||||
}
|
||||
|
||||
lengthToExamine = GetByteVector256SpanLength(offset, Length);
|
||||
if (lengthToExamine > offset)
|
||||
{
|
||||
var search = Vector256.Load(searchSpace + offset);
|
||||
|
||||
// Same method as above
|
||||
var matches = Vector256.Equals(Vector256<byte>.Zero, search).ExtractMostSignificantBits();
|
||||
if (matches == 0)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector256<byte>.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
}
|
||||
}
|
||||
|
||||
lengthToExamine = GetByteVector128SpanLength(offset, Length);
|
||||
if (lengthToExamine > offset)
|
||||
{
|
||||
var search = Vector128.Load(searchSpace + offset);
|
||||
|
||||
// Same method as above
|
||||
var matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
|
||||
if (matches == 0)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector128<byte>.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
}
|
||||
}
|
||||
|
||||
if (offset < Length)
|
||||
{
|
||||
lengthToExamine = (Length - offset);
|
||||
goto SequentialScan;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Vector256.IsHardwareAccelerated)
|
||||
{
|
||||
if (offset < Length)
|
||||
{
|
||||
if ((((uint)searchSpace + offset) & (nuint)(Vector256<byte>.Count - 1)) != 0)
|
||||
{
|
||||
// Invert currently aligned to Vector256 (is aligned to Vector128); this can cause a problem for searches
|
||||
// with no upper bound e.g. String.strlen.
|
||||
// Start with a check on Vector128 to align to Vector256, before moving to processing Vector256.
|
||||
// This ensures we do not fault across memory pages while searching for an end of string.
|
||||
var search = Vector128.Load(searchSpace + offset);
|
||||
|
||||
// Same method as below
|
||||
var matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
|
||||
if (matches == 0)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector128<byte>.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
}
|
||||
}
|
||||
|
||||
lengthToExamine = GetByteVector256SpanLength(offset, Length);
|
||||
if (lengthToExamine > offset)
|
||||
{
|
||||
do
|
||||
{
|
||||
var search = Vector256.Load(searchSpace + offset);
|
||||
var matches = Vector256.Equals(Vector256<byte>.Zero, search).ExtractMostSignificantBits();
|
||||
// Note that MoveMask has converted the equal vector elements into a set of bit flags,
|
||||
// So the bit position in 'matches' corresponds to the element offset.
|
||||
if (matches == 0)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector256<byte>.Count;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
} while (lengthToExamine > offset);
|
||||
}
|
||||
|
||||
lengthToExamine = GetByteVector128SpanLength(offset, Length);
|
||||
if (lengthToExamine > offset)
|
||||
{
|
||||
var search = Vector128.Load(searchSpace + offset);
|
||||
|
||||
// Same method as above
|
||||
var matches = Vector128.Equals(Vector128<byte>.Zero, search).ExtractMostSignificantBits();
|
||||
if (matches == 0)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector128<byte>.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
}
|
||||
}
|
||||
|
||||
if (offset < Length)
|
||||
{
|
||||
lengthToExamine = (Length - offset);
|
||||
goto SequentialScan;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Vector128.IsHardwareAccelerated)
|
||||
{
|
||||
if (offset < Length)
|
||||
{
|
||||
lengthToExamine = GetByteVector128SpanLength(offset, Length);
|
||||
|
||||
while (lengthToExamine > offset)
|
||||
{
|
||||
var search = Vector128.Load(searchSpace + offset);
|
||||
|
||||
// Same method as above
|
||||
var compareResult = Vector128.Equals(Vector128<byte>.Zero, search);
|
||||
if (compareResult == Vector128<byte>.Zero)
|
||||
{
|
||||
// Zero flags set so no matches
|
||||
offset += (nuint)Vector128<byte>.Count;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find bitflag offset of first match and add to current offset
|
||||
var matches = compareResult.ExtractMostSignificantBits();
|
||||
return (int)(offset + (uint)BitOperations.TrailingZeroCount(matches));
|
||||
}
|
||||
|
||||
if (offset < Length)
|
||||
{
|
||||
lengthToExamine = (Length - offset);
|
||||
goto SequentialScan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThrowMustBeNullTerminatedString();
|
||||
Found: // Workaround for https://github.com/dotnet/runtime/issues/8795
|
||||
return (int)offset;
|
||||
Found1:
|
||||
return (int)(offset + 1);
|
||||
Found2:
|
||||
return (int)(offset + 2);
|
||||
Found3:
|
||||
return (int)(offset + 3);
|
||||
Found4:
|
||||
return (int)(offset + 4);
|
||||
Found5:
|
||||
return (int)(offset + 5);
|
||||
Found6:
|
||||
return (int)(offset + 6);
|
||||
Found7:
|
||||
return (int)(offset + 7);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Utilities;
|
||||
|
||||
public static unsafe partial class MemoryUtility
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct AlignOfHelper<T>
|
||||
where T : struct
|
||||
{
|
||||
public byte dummy;
|
||||
public T data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a block of memory of the specified size in bytes.
|
||||
/// </summary>
|
||||
/// <param name="size">Specifies the number of bytes to allocate in memory.</param>
|
||||
/// <returns>Returns a pointer to the allocated memory block.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void* Malloc(nuint size)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return NativeMemory.Alloc(size);
|
||||
#else
|
||||
return Marshal.AllocHGlobal((IntPtr)size).ToPointer();
|
||||
#endif
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a block of memory of the specified size in bytes and initializes it to zero.
|
||||
/// </summary>
|
||||
/// <param name="size">Specifies the number of bytes to allocate in memory.</param>
|
||||
/// <returns>Returns a pointer to the allocated and zero-initialized memory block.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void* Calloc(nuint size)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return NativeMemory.AllocZeroed(size);
|
||||
#else
|
||||
var ptr = Marshal.AllocHGlobal((IntPtr)size).ToPointer();
|
||||
Unsafe.InitBlock(ptr, 0, (uint)size);
|
||||
return ptr;
|
||||
#endif
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a block of memory with a specified size and alignment.
|
||||
/// </summary>
|
||||
/// <param name="size">Specifies the total number of bytes to allocate for the memory block.</param>
|
||||
/// <param name="alignment">Defines the required alignment for the allocated memory address.</param>
|
||||
/// <returns>Returns a pointer to the allocated memory block.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void* AlignedAlloc(nuint size, nuint alignment)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return NativeMemory.AlignedAlloc(size, alignment);
|
||||
#else
|
||||
return Marshal.AllocHGlobal((IntPtr)(size + alignment - 1)).ToPointer();
|
||||
#endif
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes a previously allocated memory block to a new size. It returns a pointer to the reallocated memory.
|
||||
/// </summary>
|
||||
/// <param name="ptr">The pointer to the memory block that needs to be resized.</param>
|
||||
/// <param name="size">The new size for the memory block after resizing.</param>
|
||||
/// <returns>A pointer to the reallocated memory block, or null if the operation fails.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void* Realloc(void* ptr, nuint size)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return NativeMemory.Realloc(ptr, size);
|
||||
#else
|
||||
return Marshal.ReAllocHGlobal((IntPtr)ptr, (IntPtr)size).ToPointer();
|
||||
#endif
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reallocates memory to a specified size with a given alignment. It returns a pointer to the newly allocated
|
||||
/// memory.
|
||||
/// </summary>
|
||||
/// <param name="ptr">The pointer to the existing memory block that needs to be reallocated.</param>
|
||||
/// <param name="size">The new size for the memory allocation.</param>
|
||||
/// <param name="alignment">The required alignment for the new memory allocation.</param>
|
||||
/// <returns>A pointer to the reallocated memory block, or null if the allocation fails.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void* AlignedRealloc(void* ptr, nuint size, nuint alignment)
|
||||
{
|
||||
try
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return NativeMemory.AlignedRealloc(ptr, size, alignment);
|
||||
#else
|
||||
var newPtr = Marshal.AllocHGlobal((IntPtr)(size + alignment - 1)).ToPointer();
|
||||
if (newPtr == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ptr != null)
|
||||
{
|
||||
Unsafe.CopyBlock(newPtr, ptr, (uint)size);
|
||||
Marshal.FreeHGlobal((IntPtr)ptr);
|
||||
}
|
||||
|
||||
return newPtr;
|
||||
#endif
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the allocated memory pointed to by the given pointer. This helps in managing memory usage effectively.
|
||||
/// </summary>
|
||||
/// <param name="ptr">The pointer to the memory block that needs to be freed.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Free(void* ptr)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
NativeMemory.Free(ptr);
|
||||
#else
|
||||
Marshal.FreeHGlobal((IntPtr)ptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases memory that was allocated with alignment requirements. It ensures proper deallocation of aligned memory
|
||||
/// blocks.
|
||||
/// </summary>
|
||||
/// <param name="ptr">The pointer to the memory block that needs to be freed.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void AlignedFree(void* ptr)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
NativeMemory.AlignedFree(ptr);
|
||||
#else
|
||||
Marshal.FreeHGlobal((IntPtr)ptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears a block of memory by setting it to zero. It initializes a specified number of bytes at a given memory
|
||||
/// address.
|
||||
/// </summary>
|
||||
/// <param name="ptr">Specifies the memory address where the clearing operation will begin.</param>
|
||||
/// <param name="size">Indicates the number of bytes to be cleared in the memory block.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MemClear(void* ptr, nuint size)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
NativeMemory.Clear(ptr, size);
|
||||
#else
|
||||
Unsafe.InitBlock(ptr, 0, (uint)size);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a block of memory to a specified byte value for a given size.
|
||||
/// </summary>
|
||||
/// <param name="ptr">The memory address where the byte value will be set.</param>
|
||||
/// <param name="size">The number of bytes to set to the specified value.</param>
|
||||
/// <param name="value">The byte value to which the memory block will be initialized.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MemSet(void* ptr, byte value, nuint size)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
NativeMemory.Fill(ptr, size, value);
|
||||
#else
|
||||
Unsafe.InitBlock(ptr, value, (uint)size);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a block of memory from a source location to a destination location.
|
||||
/// </summary>
|
||||
/// <param name="source">Indicates the memory address from which data will be copied.</param>
|
||||
/// <param name="destination">Specifies the memory address where the copied data will be stored.</param>
|
||||
/// <param name="size">Defines the number of bytes to be copied from the source to the destination.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MemCpy(void* destination, void* source, nuint size)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
NativeMemory.Copy(source, destination, size);
|
||||
#else
|
||||
Unsafe.CopyBlock(destination, source, (uint)size);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a block of memory from a source location to a destination location, handling overlapping regions correctly.
|
||||
/// </summary>
|
||||
/// <param name="destination">Indicates the memory address where the data will be moved to.</param>
|
||||
/// <param name="source">Specifies the memory address from which data will be moved.</param>
|
||||
/// <param name="size">Defines the number of bytes to be moved from the source to the destination.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MemMove(void* destination, void* source, nuint size)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
// NativeMemory.Copy use memmove internally.
|
||||
NativeMemory.Copy(source, destination, size);
|
||||
#else
|
||||
Unsafe.CopyBlock(destination, source, (uint)size);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two blocks of memory byte by byte for a specified length.
|
||||
/// </summary>
|
||||
/// <param name="ptr1">A pointer to the first block of memory to compare.</param>
|
||||
/// <param name="ptr2">A pointer to the second block of memory to compare.</param>
|
||||
/// <param name="size">The number of bytes to compare. Must not exceed the length of either memory block.</param>
|
||||
/// <returns>A signed integer that indicates the relative order of the memory blocks: less than zero if the first differing
|
||||
/// byte in ptr1 is less than the corresponding byte in ptr2; zero if all compared bytes are equal; greater than
|
||||
/// zero if the first differing byte in ptr1 is greater than the corresponding byte in ptr2.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int MemCmp(void* ptr1, void* ptr2, nuint size)
|
||||
{
|
||||
if (ptr1 == ptr2)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var span1 = new ReadOnlySpan<byte>(ptr1, (int)size);
|
||||
var span2 = new ReadOnlySpan<byte>(ptr2, (int)size);
|
||||
|
||||
return span1.SequenceCompareTo(span2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the size in bytes of a specified unmanaged type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents an unmanaged type for which the size is being calculated.</typeparam>
|
||||
/// <returns>Returns the size of the specified type as an unsigned integer.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static nuint SizeOf<T>()
|
||||
where T : unmanaged
|
||||
{
|
||||
return (nuint)sizeof(T);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the alignment size of a specified unmanaged type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents an unmanaged type for which the alignment size is being calculated.</typeparam>
|
||||
/// <returns>Returns the difference in size between a helper structure and the specified type.</returns>
|
||||
public static nuint AlignOf<T>()
|
||||
where T : unmanaged
|
||||
{
|
||||
return (nuint)(sizeof(AlignOfHelper<T>) - sizeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the alignment size of a specified struct.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents a value type that is used to determine the alignment size.</typeparam>
|
||||
/// <returns>Returns the size difference in bytes as an integer.</returns>
|
||||
public static int MarshalAlignOf<T>()
|
||||
where T : struct
|
||||
{
|
||||
return Marshal.SizeOf<AlignOfHelper<T>>() - Marshal.SizeOf<T>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||
using Misaki.HighPerformance.LowLevel.Collections;
|
||||
using Misaki.HighPerformance.LowLevel.Collections.Contracts;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for copying elements between unsafe collections and spans, converting collections to
|
||||
/// arrays or lists, and searching for values.
|
||||
/// </summary>
|
||||
public static unsafe class UnsafeCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Copies elements from a source UnsafeCollection to a destination Span, ensuring both have the same size.
|
||||
/// </summary>
|
||||
/// <typeparam name="C">The type of the destination collection, which must implement <see cref="IUnsafeCollection{T}"/>.</typeparam>
|
||||
/// <typeparam name="T">Specifies the type of elements being copied, which must be unmanaged.</typeparam>
|
||||
/// <param name="source">Represents the source collection from which elements are copied.</param>
|
||||
/// <param name="destination">Represents the target span where elements are copied to.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the sizes of the source collection and destination span do not match.</exception>
|
||||
public static void CopyTo<C, T>(this C source, Span<T> destination)
|
||||
where C: IUnsafeCollection<T> where T : unmanaged
|
||||
{
|
||||
if (source.Count > destination.Length)
|
||||
{
|
||||
throw new ArgumentException("Source collection is larger than the destination span.");
|
||||
}
|
||||
|
||||
fixed (T* pDest = destination)
|
||||
{
|
||||
MemCpy(pDest, source.GetUnsafePtr(), (uint)(source.Count * sizeof(T)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a range of elements from a source collection to a destination span, ensuring both are adequately sized.
|
||||
/// </summary>
|
||||
/// <typeparam name="C">The type of the destination collection, which must implement <see cref="IUnsafeCollection{T}"/>.</typeparam>
|
||||
/// <typeparam name="T">Specifies the type of elements being copied, which must be a value type.</typeparam>
|
||||
/// <param name="source">The collection from which elements are copied.</param>
|
||||
/// <param name="destination">The span where the elements will be copied to.</param>
|
||||
/// <param name="sourceIndex">The starting index in the source collection for the copy operation.</param>
|
||||
/// <param name="destinationIndex">The starting index in the destination span where the elements will be placed.</param>
|
||||
/// <param name="length">The number of elements to copy from the source to the destination.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the specified range exceeds the bounds of the source collection or destination span.</exception>
|
||||
public static void CopyTo<C, T>(this C source, Span<T> destination, uint sourceIndex, uint destinationIndex, uint length)
|
||||
where C : IUnsafeCollection<T> where T : unmanaged
|
||||
{
|
||||
if (sourceIndex + length > source.Count || destinationIndex + length > destination.Length)
|
||||
{
|
||||
throw new ArgumentException("Source collection or destination span is too small for the specified range.");
|
||||
}
|
||||
|
||||
fixed (T* pDest = destination)
|
||||
{
|
||||
MemCpy(pDest + destinationIndex, (byte*)source.GetUnsafePtr() + sourceIndex * sizeof(T), (uint)(length * sizeof(T)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies elements from a source span to a destination unsafe collection, ensuring both have the same size.
|
||||
/// </summary>
|
||||
/// <typeparam name="C">The type of the destination collection, which must implement <see cref="IUnsafeCollection{T}"/>.</typeparam>
|
||||
/// <typeparam name="T">Specifies the type of elements being copied, which must be unmanaged.</typeparam>
|
||||
/// <param name="destination">Represents the unsafe collection that will receive the copied elements.</param>
|
||||
/// <param name="source">Represents the span containing the elements to be copied to the unsafe collection.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the source span and destination collection have different sizes.</exception>
|
||||
public static void CopyFrom<C, T>(this C destination, ReadOnlySpan<T> source)
|
||||
where C : IUnsafeCollection<T> where T : unmanaged
|
||||
{
|
||||
if (destination.Count < source.Length)
|
||||
{
|
||||
throw new ArgumentException("Destination collection is smaller than the source span.");
|
||||
}
|
||||
|
||||
fixed (T* pSrc = source)
|
||||
{
|
||||
MemCpy(destination.GetUnsafePtr(), pSrc, (uint)(source.Length * sizeof(T)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a specified range of elements from a source span to a destination collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="C">The type of the destination collection, which must implement <see cref="IUnsafeCollection{T}"/>.</typeparam>
|
||||
/// <typeparam name="T">Represents the type of elements being copied, which must be unmanaged.</typeparam>
|
||||
/// <param name="destination">The collection where elements will be copied to.</param>
|
||||
/// <param name="source">The span containing the elements to be copied.</param>
|
||||
/// <param name="sourceIndex">The starting index in the source span from which to begin copying.</param>
|
||||
/// <param name="destinationIndex">The starting index in the destination collection where the elements will be placed.</param>
|
||||
/// <param name="length">The number of elements to copy from the source span to the destination collection.</param>
|
||||
/// <exception cref="ArgumentException">Thrown when the specified range exceeds the bounds of the source span or destination collection.</exception>
|
||||
public static void CopyFrom<C, T>(this C destination, ReadOnlySpan<T> source, uint sourceIndex, uint destinationIndex, uint length)
|
||||
where C : IUnsafeCollection<T> where T : unmanaged
|
||||
{
|
||||
if (sourceIndex + length > source.Length || destinationIndex + length > destination.Count)
|
||||
{
|
||||
throw new ArgumentException("Source span or destination collection is too small for the specified range.");
|
||||
}
|
||||
|
||||
fixed (T* pSrc = source)
|
||||
{
|
||||
MemCpy((byte*)destination.GetUnsafePtr() + destinationIndex * sizeof(T), pSrc + sourceIndex, (uint)(length * sizeof(T)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a managed array to an UnsafeArray by copying its elements to unmanaged memory.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the array, which must be unmanaged.</typeparam>
|
||||
/// <param name="source">The managed array to convert.</param>
|
||||
/// <param name="allocator">The allocator to use for memory allocation of the UnsafeArray.</param>
|
||||
/// <returns>A new UnsafeArray containing a copy of the source array elements.</returns>
|
||||
public static UnsafeArray<T> ToUnsafeArray<T>(this T[] source, Allocator allocator)
|
||||
where T : unmanaged
|
||||
{
|
||||
var array = new UnsafeArray<T>(source.Length, allocator);
|
||||
fixed (T* pSrc = source)
|
||||
{
|
||||
MemCpy(array.GetUnsafePtr(), pSrc, (uint)(source.Length * sizeof(T)));
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a managed List to an UnsafeList by copying its elements to unmanaged memory.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the list, which must be unmanaged.</typeparam>
|
||||
/// <param name="source">The managed List to convert.</param>
|
||||
/// <param name="allocator">The allocator to use for memory allocation of the UnsafeList.</param>
|
||||
/// <returns>A new UnsafeList containing a copy of the source list elements.</returns>
|
||||
public static UnsafeList<T> ToUnsafeList<T>(this List<T> source, Allocator allocator)
|
||||
where T : unmanaged
|
||||
{
|
||||
var list = new UnsafeList<T>(source.Count, allocator);
|
||||
fixed (T* pSrc = CollectionsMarshal.AsSpan(source))
|
||||
{
|
||||
MemCpy(list.GetUnsafePtr(), pSrc, (uint)(source.Count * sizeof(T)));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new array containing all elements from the specified unsafe collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="C">The type of the source collection, which must implement <see cref="IUnsafeCollection{T}"/>.</typeparam>
|
||||
/// <typeparam name="T">The type of elements contained in the collection. Must be an unmanaged type.</typeparam>
|
||||
/// <param name="source">The collection whose elements will be copied to the new array. Must not be null.</param>
|
||||
/// <returns>An array containing all elements from <paramref name="source"/> in their current order.</returns>
|
||||
public static T[] ToArray<C, T>(this C source)
|
||||
where C : IUnsafeCollection<T> where T : unmanaged
|
||||
{
|
||||
return new Span<T>(source.GetUnsafePtr(), source.Count).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="List{T}"/> containing the elements of the specified unsafe collection.
|
||||
/// </summary>
|
||||
/// <typeparam name="C">The type of the source collection, which must implement <see cref="IUnsafeCollection{T}"/>.</typeparam>
|
||||
/// <typeparam name="T">The type of elements contained in the collection. Must be an unmanaged type.</typeparam>
|
||||
/// <param name="source">The unsafe collection whose elements are to be copied to the new list. Must not be null.</param>
|
||||
/// <returns>A <see cref="List{T}"/> containing all elements from <paramref name="source"/> in their original order.</returns>
|
||||
public static List<T> ToList<C, T>(this C source)
|
||||
where C : IUnsafeCollection<T> where T : unmanaged
|
||||
{
|
||||
var list = new List<T>(source.Count);
|
||||
var span = new Span<T>(source.GetUnsafePtr(), source.Count);
|
||||
span.CopyTo(CollectionsMarshal.AsSpan(list));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Misaki.HighPerformance.LowLevel.Utilities;
|
||||
|
||||
public static unsafe class UnsafeUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a pointer to a reference of a specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of the reference to be created from the pointer.</typeparam>
|
||||
/// <param name="ptr">Represents the memory address to be converted into a reference.</param>
|
||||
/// <returns>Returns a reference of the specified type pointing to the given memory address.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T AsRef<T>(void* ptr)
|
||||
where T : unmanaged
|
||||
{
|
||||
return ref *(T*)ptr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the address of a specified variable in memory.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Represents the type of the variable whose address is being retrieved.</typeparam>
|
||||
/// <param name="value">The variable whose memory address is to be obtained.</param>
|
||||
/// <returns>A pointer to the memory address of the specified variable.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void* AddressOf<T>(ref T value)
|
||||
where T : unmanaged
|
||||
{
|
||||
return Unsafe.AsPointer(ref value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an element from an unmanaged array at a specified index using a pointer.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of elements in the unmanaged array.</typeparam>
|
||||
/// <param name="ptr">Points to the start of the unmanaged array from which the element is read.</param>
|
||||
/// <param name="index">Indicates the position of the element to be accessed within the array.</param>
|
||||
/// <returns>Returns a pointer to the element located at the specified index.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T* ReadArrayElementUnsafe<T>(void* ptr, nint index)
|
||||
where T : unmanaged
|
||||
{
|
||||
return (T*)((byte*)ptr + index * sizeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an element from an unmanaged array at a specified index using a pointer.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of elements in the unmanaged array.</typeparam>
|
||||
/// <param name="ptr">Points to the start of the unmanaged array from which the element is read.</param>
|
||||
/// <param name="index">Indicates the position of the element to be accessed within the array.</param>
|
||||
/// <returns>Returns a pointer to the element located at the specified index.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T* ReadArrayElementUnsafe<T>(void* ptr, nuint index)
|
||||
where T : unmanaged
|
||||
{
|
||||
return (T*)((byte*)ptr + index * (nuint)sizeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an element from an unmanaged array using a pointer and index, returning a reference to the element.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of the elements in the unmanaged array.</typeparam>
|
||||
/// <param name="ptr">Points to the start of the unmanaged array from which the element is read.</param>
|
||||
/// <param name="index">Indicates the position of the element to be accessed in the array.</param>
|
||||
/// <returns>A reference to the specified element in the unmanaged array.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T ReadArrayElementRef<T>(void* ptr, nint index)
|
||||
where T : unmanaged
|
||||
{
|
||||
return ref AsRef<T>(ReadArrayElementUnsafe<T>(ptr, index));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an element from an unmanaged array using a pointer and index, returning a reference to the element.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of the elements in the unmanaged array.</typeparam>
|
||||
/// <param name="ptr">Points to the start of the unmanaged array from which the element is read.</param>
|
||||
/// <param name="index">Indicates the position of the element to be accessed in the array.</param>
|
||||
/// <returns>A reference to the specified element in the unmanaged array.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ref T ReadArrayElementRef<T>(void* ptr, nuint index)
|
||||
where T : unmanaged
|
||||
{
|
||||
return ref AsRef<T>(ReadArrayElementUnsafe<T>(ptr, index));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an element from an array at a specified index using a pointer to the array.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of the elements in the array, which must be unmanaged.</typeparam>
|
||||
/// <param name="ptr">Points to the start of the array from which an element will be read.</param>
|
||||
/// <param name="index">Indicates the position of the element to be accessed within the array.</param>
|
||||
/// <returns>The element located at the specified index in the array.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T ReadArrayElement<T>(void* ptr, nint index)
|
||||
where T : unmanaged
|
||||
{
|
||||
return *ReadArrayElementUnsafe<T>(ptr, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an element from an array at a specified index using a pointer to the array.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of the elements in the array, which must be unmanaged.</typeparam>
|
||||
/// <param name="ptr">Points to the start of the array from which an element will be read.</param>
|
||||
/// <param name="index">Indicates the position of the element to be accessed within the array.</param>
|
||||
/// <returns>The element located at the specified index in the array.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T ReadArrayElement<T>(void* ptr, nuint index)
|
||||
where T : unmanaged
|
||||
{
|
||||
return *ReadArrayElementUnsafe<T>(ptr, index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value to a specified index of an unmanaged array using a pointer.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of the value being written to the array, which must be an unmanaged type.</typeparam>
|
||||
/// <param name="ptr">Points to the beginning of the unmanaged array where the value will be written.</param>
|
||||
/// <param name="index">Indicates the position in the array where the value should be stored.</param>
|
||||
/// <param name="value">Represents the value to be written to the specified index of the array.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteArrayElement<T>(void* ptr, nint index, T value)
|
||||
where T : unmanaged
|
||||
{
|
||||
*ReadArrayElementUnsafe<T>(ptr, index) = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a value to a specified index of an unmanaged array using a pointer.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Specifies the type of the value being written to the array, which must be an unmanaged type.</typeparam>
|
||||
/// <param name="ptr">Points to the beginning of the unmanaged array where the value will be written.</param>
|
||||
/// <param name="index">Indicates the position in the array where the value should be stored.</param>
|
||||
/// <param name="value">Represents the value to be written to the specified index of the array.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void WriteArrayElement<T>(void* ptr, nuint index, T value)
|
||||
where T : unmanaged
|
||||
{
|
||||
*ReadArrayElementUnsafe<T>(ptr, index) = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a pointer to the first element of the specified span. This method enables direct, unsafe access to the underlying data of the span.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the span. Must be an unmanaged type.</typeparam>
|
||||
/// <param name="span">The span whose underlying data pointer is to be obtained.</param>
|
||||
/// <returns>A pointer to the first element of the span. If the span is empty, the returned pointer is undefined and must not be dereferenced.</returns>
|
||||
public static T* GetUnsafePtr<T>(this Span<T> span)
|
||||
where T : unmanaged
|
||||
{
|
||||
fixed (T* ptr = span)
|
||||
{
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a pointer to the first element of the specified span. This method enables direct, unsafe access to the underlying data of the span.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the span. Must be an unmanaged type.</typeparam>
|
||||
/// <param name="span">The span whose underlying data pointer is to be obtained.</param>
|
||||
/// <returns>A pointer to the first element of the span. If the span is empty, the returned pointer is undefined and must not be dereferenced.</returns>
|
||||
public static T* GetUnsafePtr<T>(this ReadOnlySpan<T> span)
|
||||
where T : unmanaged
|
||||
{
|
||||
fixed (T* ptr = span)
|
||||
{
|
||||
return ptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user