#if ENABLE_UNITY_COLLECTIONS_CHECKS #define ENABLE_UNITY_ALLOCATION_CHECKS #endif #pragma warning disable 0649 using System; using System.Diagnostics; using System.Runtime.InteropServices; using AOT; using Unity.Burst; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; using UnityEngine.Assertions; using Unity.Jobs.LowLevel.Unsafe; namespace Unity.Collections { /// <summary> /// Manages custom memory allocators. /// </summary> public static class AllocatorManager { internal static Block AllocateBlock<T>(ref this T t, int sizeOf, int alignOf, int items) where T : unmanaged, IAllocator { CheckValid(t.Handle); Block block = default; block.Range.Pointer = IntPtr.Zero; block.Range.Items = items; block.Range.Allocator = t.Handle; block.BytesPerItem = sizeOf; // Make the alignment multiple of cacheline size block.Alignment = math.max(JobsUtility.CacheLineSize, alignOf); var error = t.Try(ref block); CheckFailedToAllocate(error); return block; } internal static Block AllocateBlock<T,U>(ref this T t, U u, int items) where T : unmanaged, IAllocator where U : unmanaged { return AllocateBlock(ref t, UnsafeUtility.SizeOf<U>(), UnsafeUtility.AlignOf<U>(), items); } internal static unsafe void* Allocate<T>(ref this T t, int sizeOf, int alignOf, int items) where T : unmanaged, IAllocator { return (void*)AllocateBlock(ref t, sizeOf, alignOf, items).Range.Pointer; } internal static unsafe U* Allocate<T,U>(ref this T t, U u, int items) where T : unmanaged, IAllocator where U : unmanaged { return (U*)Allocate(ref t, UnsafeUtility.SizeOf<U>(), UnsafeUtility.AlignOf<U>(), items); } internal static unsafe void* AllocateStruct<T, U>(ref this T t, U u, int items) where T : unmanaged, IAllocator where U : struct { return (void*)Allocate(ref t, UnsafeUtility.SizeOf<U>(), UnsafeUtility.AlignOf<U>(), items); } internal static unsafe void FreeBlock<T>(ref this T t, ref Block block) where T : unmanaged, IAllocator { CheckValid(t.Handle); block.Range.Items = 0; var error = t.Try(ref block); CheckFailedToFree(error); } internal static unsafe void Free<T>(ref this T t, void* pointer, int sizeOf, int alignOf, int items) where T : unmanaged, IAllocator { if (pointer == null) return; Block block = default; block.AllocatedItems = items; block.Range.Pointer = (IntPtr)pointer; block.BytesPerItem = sizeOf; block.Alignment = alignOf; t.FreeBlock(ref block); } internal static unsafe void Free<T,U>(ref this T t, U* pointer, int items) where T : unmanaged, IAllocator where U : unmanaged { Free(ref t, pointer, UnsafeUtility.SizeOf<U>(), UnsafeUtility.AlignOf<U>(), items); } /// <summary> /// Allocates memory from an allocator. /// </summary> /// <param name="handle">A handle to the allocator.</param> /// <param name="itemSizeInBytes">The number of bytes to allocate.</param> /// <param name="alignmentInBytes">The alignment in bytes (must be a power of two).</param> /// <param name="items">The number of values to allocate space for. Defaults to 1.</param> /// <returns>A pointer to the allocated memory.</returns> public unsafe static void* Allocate(AllocatorHandle handle, int itemSizeInBytes, int alignmentInBytes, int items = 1) { return handle.Allocate(itemSizeInBytes, alignmentInBytes, items); } /// <summary> /// Allocates enough memory for an unmanaged value of a given type. /// </summary> /// <typeparam name="T">The type of value to allocate for.</typeparam> /// <param name="handle">A handle to the allocator.</param> /// <param name="items">The number of values to allocate for space for. Defaults to 1.</param> /// <returns>A pointer to the allocated memory.</returns> public unsafe static T* Allocate<T>(AllocatorHandle handle, int items = 1) where T : unmanaged { return handle.Allocate(default(T), items); } /// <summary> /// Frees an allocation. /// </summary> /// <remarks>For some allocators, the size of the allocation must be known to properly deallocate. /// Other allocators only need the pointer when deallocating and so will ignore `itemSizeInBytes`, `alignmentInBytes` and `items`.</remarks> /// <param name="handle">A handle to the allocator.</param> /// <param name="pointer">A pointer to the allocated memory.</param> /// <param name="itemSizeInBytes">The size in bytes of the allocation.</param> /// <param name="alignmentInBytes">The alignment in bytes (must be a power of two).</param> /// <param name="items">The number of values that the memory was allocated for.</param> public unsafe static void Free(AllocatorHandle handle, void* pointer, int itemSizeInBytes, int alignmentInBytes, int items = 1) { handle.Free(pointer, itemSizeInBytes, alignmentInBytes, items); } /// <summary> /// Frees an allocation. /// </summary> /// <param name="handle">A handle to the allocator.</param> /// <param name="pointer">A pointer to the allocated memory.</param> public unsafe static void Free(AllocatorHandle handle, void* pointer) { handle.Free((byte*)pointer, 1); } /// <summary> /// Frees an allocation. /// </summary> /// <remarks>For some allocators, the size of the allocation must be known to properly deallocate. /// Other allocators only need the pointer when deallocating and so will ignore `T` and `items`.</remarks> /// <typeparam name="T">The type of value that the memory was allocated for.</typeparam> /// <param name="handle">A handle to the allocator.</param> /// <param name="pointer">A pointer to the allocated memory.</param> /// <param name="items">The number of values that the memory was allocated for.</param> public unsafe static void Free<T>(AllocatorHandle handle, T* pointer, int items = 1) where T : unmanaged { handle.Free(pointer, items); } /// <summary> /// Corresponds to Allocator.Invalid. /// </summary> /// <value>Corresponds to Allocator.Invalid.</value> public static readonly AllocatorHandle Invalid = new AllocatorHandle { Index = 0 }; /// <summary> /// Corresponds to Allocator.None. /// </summary> /// <value>Corresponds to Allocator.None.</value> public static readonly AllocatorHandle None = new AllocatorHandle { Index = 1 }; /// <summary> /// Corresponds to Allocator.Temp. /// </summary> /// <value>Corresponds to Allocator.Temp.</value> public static readonly AllocatorHandle Temp = new AllocatorHandle { Index = 2 }; /// <summary> /// Corresponds to Allocator.TempJob. /// </summary> /// <value>Corresponds to Allocator.TempJob.</value> public static readonly AllocatorHandle TempJob = new AllocatorHandle { Index = 3 }; /// <summary> /// Corresponds to Allocator.Persistent. /// </summary> /// <value>Corresponds to Allocator.Persistent.</value> public static readonly AllocatorHandle Persistent = new AllocatorHandle { Index = 4 }; /// <summary> /// Corresponds to Allocator.AudioKernel. /// </summary> /// <value>Corresponds to Allocator.AudioKernel.</value> public static readonly AllocatorHandle AudioKernel = new AllocatorHandle { Index = 5 }; /// <summary> /// Used for calling an allocator function. /// </summary> public delegate int TryFunction(IntPtr allocatorState, ref Block block); /// <summary> /// Represents the allocator function used within an allocator. /// </summary> [StructLayout(LayoutKind.Sequential)] public struct AllocatorHandle : IAllocator { internal ref TableEntry TableEntry => ref SharedStatics.TableEntry.Ref.Data.ElementAt(Index); internal bool IsInstalled => ((SharedStatics.IsInstalled.Ref.Data.ElementAt(Index>>6) >> (Index&63)) & 1) != 0; internal void IncrementVersion() { #if ENABLE_UNITY_ALLOCATION_CHECKS if (IsInstalled && IsCurrent) { // When allocator version is larger than 0x7FFF, allocator.ToAllocator // returns a negative value which causes problem when comparing to Allocator.None. // So only lower 15 bits of version is valid. Version = OfficialVersion = (ushort)(++OfficialVersion & 0x7FFF); } #endif } internal void Rewind() { #if ENABLE_UNITY_ALLOCATION_CHECKS InvalidateDependents(); IncrementVersion(); #endif } internal void Install(TableEntry tableEntry) { #if ENABLE_UNITY_ALLOCATION_CHECKS // if this allocator has never been visited before, then the unsafelists for its child allocators // and child safety handles are uninitialized, which means their allocator is Allocator.Invalid. // rectify that here. if(ChildSafetyHandles.Allocator.Value != (int)Allocator.Persistent) { ChildSafetyHandles = new UnsafeList<AtomicSafetyHandle>(0, Allocator.Persistent); ChildAllocators = new UnsafeList<AllocatorHandle>(0, Allocator.Persistent); } #endif Rewind(); TableEntry = tableEntry; } #if ENABLE_UNITY_ALLOCATION_CHECKS internal ref ushort OfficialVersion => ref SharedStatics.Version.Ref.Data.ElementAt(Index); internal ref UnsafeList<AtomicSafetyHandle> ChildSafetyHandles => ref SharedStatics.ChildSafetyHandles.Ref.Data.ElementAt(Index); internal ref UnsafeList<AllocatorHandle> ChildAllocators => ref SharedStatics.ChildAllocators.Ref.Data.ElementAt(Index); internal ref AllocatorHandle Parent => ref SharedStatics.Parent.Ref.Data.ElementAt(Index); internal ref int IndexInParent => ref SharedStatics.IndexInParent.Ref.Data.ElementAt(Index); internal bool IsCurrent => (Version == 0) || (Version == OfficialVersion); internal bool IsValid => (Index < FirstUserIndex) || (IsInstalled && IsCurrent); /// <summary> /// <para>Determines if the handle is still valid, because we intend to release it if it is.</para> /// </summary> /// <param name="handle">Safety handle.</param> internal static unsafe bool CheckExists(AtomicSafetyHandle handle) { bool res = false; #if UNITY_DOTSRUNTIME // In DOTS Runtime, AtomicSaftyHandle version is at 8 bytes offset of nodePtr int* versionNode = (int*)((byte *)handle.nodePtr + sizeof(void *)); res = (handle.version == (*versionNode & AtomicSafetyNodeVersionMask.ReadWriteDisposeUnprotect)); #else int* versionNode = (int*) (void*) handle.versionNode; res = (handle.version == (*versionNode & AtomicSafetyHandle.ReadWriteDisposeCheck)); #endif return res; } internal static unsafe bool AreTheSame(AtomicSafetyHandle a, AtomicSafetyHandle b) { if(a.version != b.version) return false; #if UNITY_DOTSRUNTIME if(a.nodePtr != b.nodePtr) #else if(a.versionNode != b.versionNode) #endif return false; return true; } internal static bool AreTheSame(AllocatorHandle a, AllocatorHandle b) { if(a.Index != b.Index) return false; if(a.Version != b.Version) return false; return true; } internal bool NeedsUseAfterFreeTracking() { if(IsValid == false) return false; if(ChildSafetyHandles.Allocator.Value != (int)Allocator.Persistent) return false; return true; } /// <summary> /// For internal use only. /// </summary> /// <value>For internal use only.</value> public const int InvalidChildSafetyHandleIndex = -1; internal int AddSafetyHandle(AtomicSafetyHandle handle) { if(!NeedsUseAfterFreeTracking()) return InvalidChildSafetyHandleIndex; var result = ChildSafetyHandles.Length; ChildSafetyHandles.Add(handle); return result; } internal bool TryRemoveSafetyHandle(AtomicSafetyHandle handle, int safetyHandleIndex) { if(!NeedsUseAfterFreeTracking()) return false; if(safetyHandleIndex == InvalidChildSafetyHandleIndex) return false; safetyHandleIndex = math.min(safetyHandleIndex, ChildSafetyHandles.Length - 1); while(safetyHandleIndex >= 0) { unsafe { var safetyHandle = ChildSafetyHandles.Ptr + safetyHandleIndex; if(AreTheSame(*safetyHandle, handle)) { ChildSafetyHandles.RemoveAtSwapBack(safetyHandleIndex); return true; } } --safetyHandleIndex; } return false; } /// <summary> /// For internal use only. /// </summary> /// <value>For internal use only.</value> public const int InvalidChildAllocatorIndex = -1; internal int AddChildAllocator(AllocatorHandle handle) { if(!NeedsUseAfterFreeTracking()) return InvalidChildAllocatorIndex; var result = ChildAllocators.Length; ChildAllocators.Add(handle); handle.Parent = this; handle.IndexInParent = result; return result; } internal bool TryRemoveChildAllocator(AllocatorHandle handle, int childAllocatorIndex) { if(!NeedsUseAfterFreeTracking()) return false; if(childAllocatorIndex == InvalidChildAllocatorIndex) return false; childAllocatorIndex = math.min(childAllocatorIndex, ChildAllocators.Length - 1); while(childAllocatorIndex >= 0) { unsafe { var allocatorHandle = ChildAllocators.Ptr + childAllocatorIndex; if(AreTheSame(*allocatorHandle, handle)) { ChildAllocators.RemoveAtSwapBack(childAllocatorIndex); return true; } } --childAllocatorIndex; } return false; } // when you rewind an allocator, it invalidates and unregisters all of its child allocators - allocators that use as // backing memory, memory that was allocated from this (parent) allocator. the rewind operation was itself unmanaged, // until we added a managed global table of delegates alongside the unmanaged global table of function pointers. once // this table was added, the "unregister" extension function became managed, because it manipulates a managed array of // delegates. // a workaround (UnmanagedUnregister) was found that makes it possible for rewind to become unmanaged again: only in // the case that we rewind an allocator and invalidate all of its child allocators, we then unregister the child // allocators without touching the managed array of delegates as well. // this can "leak" delegates - it's possible for this to cause us to hold onto a GC reference to a delegate until // the end of the program, long after the delegate is no longer needed. but, there are only 65,536 such slots to // burn, and delegates are small data structures, and the leak ends when a delegate slot is reused, and most importantly, // when we've rewound an allocator while child allocators remain registered, we are likely before long to encounter // a use-before-free crash or a safety handle violation, both of which are likely to terminate the session before // anything can leak. [NotBurstCompatible] internal void InvalidateDependents() { if(!NeedsUseAfterFreeTracking()) return; for(var i = 0; i < ChildSafetyHandles.Length; ++i) { unsafe { AtomicSafetyHandle* handle = ChildSafetyHandles.Ptr + i; if(CheckExists(*handle)) AtomicSafetyHandle.Release(*handle); } } ChildSafetyHandles.Clear(); if(Parent.IsValid) Parent.TryRemoveChildAllocator(this, IndexInParent); Parent = default; IndexInParent = InvalidChildAllocatorIndex; for(var i = 0; i < ChildAllocators.Length; ++i) { unsafe { AllocatorHandle* handle = (AllocatorHandle*)ChildAllocators.Ptr + i; if(handle->IsValid) handle->UnmanagedUnregister(); // see above comment } } ChildAllocators.Clear(); } #endif /// <summary> /// Returns the AllocatorHandle of an allocator. /// </summary> /// <param name="a">The Allocator to copy.</param> /// <returns>The AllocatorHandle of an allocator.</returns> public static implicit operator AllocatorHandle(Allocator a) => new AllocatorHandle { Index = (ushort)((uint)a & 0xFFFF), Version = (ushort)((uint)a >> 16) }; /// <summary> /// This allocator's index into the global table of allocator functions. /// </summary> /// <value>This allocator's index into the global table of allocator functions.</value> public ushort Index; /// <summary> /// This allocator's version number. /// </summary> /// <remarks>An allocator function is uniquely identified by its *combination* of <see cref="Index"/> and <see cref="Version"/> together: each /// index has a version number that starts at 0; the version number is incremented each time the allocator is invalidated. Only the /// lower 15 bits of Version is in use because when allocator version is larger than 0x7FFF, allocator.ToAllocator returns a negative value /// which causes problem when comparing to Allocator.None. /// </remarks> /// <value>This allocator's version number.</value> public ushort Version; /// <summary> /// The <see cref="Index"/> cast to int. /// </summary> /// <value>The <see cref="Index"/> cast to int.</value> public int Value => Index; /// <summary> /// Allocates a block from this allocator. /// </summary> /// <typeparam name="T">The type of value to allocate for.</typeparam> /// <param name="block">Outputs the allocated block.</param> /// <param name="items">The number of values to allocate for.</param> /// <returns>0 if successful. Otherwise, returns the error code from the allocator function.</returns> public int TryAllocateBlock<T>(out Block block, int items) where T : struct { block = new Block { Range = new Range { Items = items, Allocator = this }, BytesPerItem = UnsafeUtility.SizeOf<T>(), Alignment = 1 << math.min(3, math.tzcnt(UnsafeUtility.SizeOf<T>())) }; var returnCode = Try(ref block); return returnCode; } /// <summary> /// Allocates a block with this allocator function. /// </summary> /// <typeparam name="T">The type of value to allocate for.</typeparam> /// <param name="items">The number of values to allocate for.</param> /// <returns>The allocated block.</returns> /// <exception cref="ArgumentException">Thrown if the allocator is not valid or if the allocation failed.</exception> public Block AllocateBlock<T>(int items) where T : struct { CheckValid(this); var error = TryAllocateBlock<T>(out Block block, items); CheckAllocatedSuccessfully(error); return block; } [Conditional("ENABLE_UNITY_ALLOCATION_CHECKS")] static void CheckAllocatedSuccessfully(int error) { if (error != 0) throw new ArgumentException($"Error {error}: Failed to Allocate"); } /// <summary> /// For internal use only. /// </summary> /// <value>For internal use only.</value> public TryFunction Function => default; /// <summary> /// Tries to allocate the block with this allocator. /// </summary> /// <param name="block">The block to allocate.</param> /// <returns>0 if successful. Otherwise, returns an error code.</returns> public int Try(ref Block block) { block.Range.Allocator = this; var error = AllocatorManager.Try(ref block); return error; } /// <summary> /// This handle. /// </summary> /// <value>This handle.</value> public AllocatorHandle Handle { get { return this; } set { this = value; } } /// <summary> /// Retrieve the Allocator associated with this allocator handle. /// </summary> /// <value>The Allocator retrieved.</value> public Allocator ToAllocator { get { uint lo = Index; uint hi = Version; uint value = (hi << 16) | lo; return (Allocator)value; } } /// <summary> /// Check whether this allocator is a custom allocator. /// </summary> /// <remarks>The AllocatorHandle is a custom allocator if its Index is larger or equal to `FirstUserIndex`.</remarks> /// <value>True if this AllocatorHandle is a custom allocator.</value> public bool IsCustomAllocator { get { return this.Index >= FirstUserIndex; } } /// <summary> /// Dispose the allocator. /// </summary> public void Dispose() { Rewind(); } } /// <summary> /// For internal use only. /// </summary> [StructLayout(LayoutKind.Sequential)] public struct BlockHandle { /// <summary> /// Represents the handle. /// </summary> /// <value>Represents the handle.</value> public ushort Value; } /// <summary> /// A range of allocated memory. /// </summary> /// <remarks>The name is perhaps misleading: only in combination with a <see cref="Block"/> does /// a `Range` have sufficient information to represent the number of bytes in an allocation. The reason `Range` is its own type that's separate from `Block` /// stems from some efficiency concerns in the implementation details. In most cases, a `Range` is only used in conjunction with an associated `Block`. /// </remarks> [StructLayout(LayoutKind.Sequential)] public struct Range : IDisposable { /// <summary> /// Pointer to the start of this range. /// </summary> /// <value>Pointer to the start of this range.</value> public IntPtr Pointer; // 0 /// <summary> /// Number of items allocated in this range. /// </summary> /// <remarks>The actual allocation may be larger. See <see cref="Block.AllocatedItems"/>.</remarks> /// <value>Number of items allocated in this range. </value> public int Items; // 8 /// <summary> /// The allocator function used for this range. /// </summary> /// <value>The allocator function used for this range.</value> public AllocatorHandle Allocator; // 12 /// <summary> /// Deallocates the memory represented by this range. /// </summary> /// <remarks> /// Same as disposing the <see cref="Block"/> which contains this range. /// /// Cannot be used with allocators which need the allocation size to deallocate. /// </remarks> public void Dispose() { Block block = new Block { Range = this }; block.Dispose(); this = block.Range; } } /// <summary> /// Represents an individual allocation within an allocator. /// </summary> /// <remarks>A block consists of a <see cref="Range"/> plus metadata about the type of elements for which the block was allocated.</remarks> [StructLayout(LayoutKind.Sequential)] public struct Block : IDisposable { /// <summary> /// The range of memory encompassed by this block. /// </summary> /// <value>The range of memory encompassed by this block.</value> public Range Range; /// <summary> /// Number of bytes per item. /// </summary> /// <value>Number of bytes per item.</value> public int BytesPerItem; /// <summary> /// Number of items allocated for. /// </summary> /// <value>Number of items allocated for.</value> public int AllocatedItems; /// <summary> /// Log2 of the byte alignment. /// </summary> /// <remarks>The alignment must always be power of 2. Storing the alignment as its log2 helps enforces this.</remarks> /// <value>Log2 of the byte alignment.</value> public byte Log2Alignment; /// <summary> /// This field only exists to pad the `Block` struct. Ignore it. /// </summary> /// <value>This field only exists to pad the `Block` struct. Ignore it.</value> public byte Padding0; /// <summary> /// This field only exists to pad the `Block` struct. Ignore it. /// </summary> /// <value>This field only exists to pad the `Block` struct. Ignore it.</value> public ushort Padding1; /// <summary> /// This field only exists to pad the `Block` struct. Ignore it. /// </summary> /// <value>This field only exists to pad the `Block` struct. Ignore it.</value> public uint Padding2; /// <summary> /// Number of bytes requested for this block. /// </summary> /// <remarks>The actual allocation size may be larger due to alignment.</remarks> /// <value>Number of bytes requested for this block.</value> public long Bytes => BytesPerItem * Range.Items; /// <summary> /// Number of bytes allocated for this block. /// </summary> /// <remarks>The requested allocation size may be smaller. Any excess is due to alignment</remarks> /// <value>Number of bytes allocated for this block.</value> public long AllocatedBytes => BytesPerItem * AllocatedItems; /// <summary> /// The alignment. /// </summary> /// <remarks>Must be power of 2 that's greater than or equal to 0. /// /// Set alignment *before* the allocation is made. Setting it after has no effect on the allocation.</remarks> /// <param name="value">A new alignment. If not a power of 2, it will be rounded up to the next largest power of 2.</param> /// <value>The alignment.</value> public int Alignment { get => 1 << Log2Alignment; set => Log2Alignment = (byte)(32 - math.lzcnt(math.max(1, value) - 1)); } /// <summary> /// Deallocates this block. /// </summary> /// <remarks>Same as <see cref="TryAllocate"/>.</remarks> public void Dispose() { TryFree(); } /// <summary> /// Attempts to allocate this block. /// </summary> /// <returns>0 if successful. Otherwise, returns the error code from the allocator function.</returns> public int TryAllocate() { Range.Pointer = IntPtr.Zero; return Try(ref this); } /// <summary> /// Attempts to free this block. /// </summary> /// <returns>0 if successful. Otherwise, returns the error code from the allocator function.</returns> public int TryFree() { Range.Items = 0; return Try(ref this); } /// <summary> /// Allocates this block. /// </summary> /// <exception cref="ArgumentException">Thrown if safety checks are enabled and the allocation fails.</exception> public void Allocate() { var error = TryAllocate(); CheckFailedToAllocate(error); } /// <summary> /// Frees the block. /// </summary> /// <exception cref="ArgumentException">Thrown if safety checks are enabled and the deallocation fails.</exception> public void Free() { var error = TryFree(); CheckFailedToFree(error); } [Conditional("ENABLE_UNITY_ALLOCATION_CHECKS")] void CheckFailedToAllocate(int error) { if (error != 0) throw new ArgumentException($"Error {error}: Failed to Allocate {this}"); } [Conditional("ENABLE_UNITY_ALLOCATION_CHECKS")] void CheckFailedToFree(int error) { if (error != 0) throw new ArgumentException($"Error {error}: Failed to Free {this}"); } } /// <summary> /// An allocator function pointer. /// </summary> public interface IAllocator : IDisposable { /// <summary> /// The allocator function. It can allocate, deallocate, or reallocate. /// </summary> TryFunction Function { get; } /// <summary> /// Invoke the allocator function. /// </summary> /// <param name="block">The block to allocate, deallocate, or reallocate. See <see cref="AllocatorManager.Try"/></param> /// <returns>0 if successful. Otherwise, returns the error code from the allocator function.</returns> int Try(ref Block block); /// <summary> /// This allocator. /// </summary> /// <value>This allocator.</value> AllocatorHandle Handle { get; set; } /// <summary> /// Cast the Allocator index into Allocator /// </summary> Allocator ToAllocator { get; } /// <summary> /// Check whether an allocator is a custom allocator /// </summary> bool IsCustomAllocator { get; } } /// <summary> /// Memory allocation Success status /// </summary> public const int kErrorNone = 0; /// <summary> /// Memory allocation Buffer Overflow status /// </summary> public const int kErrorBufferOverflow = -1; #if !UNITY_IOS [BurstDiscard] private static void CheckDelegate(ref bool useDelegate) { //@TODO: This should use BurstCompiler.IsEnabled once that is available as an efficient API. useDelegate = true; } private static bool UseDelegate() { bool result = false; CheckDelegate(ref result); return result; } #endif private static int allocate_block(ref Block block) { TableEntry tableEntry = default; tableEntry = block.Range.Allocator.TableEntry; var function = new FunctionPointer<TryFunction>(tableEntry.function); // this is a path for bursted caller, for non-Burst C#, it generates garbage each time we call Invoke return function.Invoke(tableEntry.state, ref block); } #if !UNITY_IOS [BurstDiscard] private static void forward_mono_allocate_block(ref Block block, ref int error) { TableEntry tableEntry = default; tableEntry = block.Range.Allocator.TableEntry; var index = block.Range.Allocator.Handle.Index; if (index >= Managed.kMaxNumCustomAllocator) { throw new ArgumentException("Allocator index into TryFunction delegate table exceeds maximum."); } ref TryFunction function = ref Managed.TryFunctionDelegates[block.Range.Allocator.Handle.Index]; error = function(tableEntry.state, ref block); } #endif internal static Allocator LegacyOf(AllocatorHandle handle) { if (handle.Value >= FirstUserIndex) return Allocator.Persistent; return (Allocator) handle.Value; } static unsafe int TryLegacy(ref Block block) { if (block.Range.Pointer == IntPtr.Zero) // Allocate { block.Range.Pointer = (IntPtr)Memory.Unmanaged.Allocate(block.Bytes, block.Alignment, LegacyOf(block.Range.Allocator)); block.AllocatedItems = block.Range.Items; return (block.Range.Pointer == IntPtr.Zero) ? -1 : 0; } if (block.Bytes == 0) // Free { if (LegacyOf(block.Range.Allocator) != Allocator.None) { Memory.Unmanaged.Free((void*)block.Range.Pointer, LegacyOf(block.Range.Allocator)); } block.Range.Pointer = IntPtr.Zero; block.AllocatedItems = 0; return 0; } // Reallocate (keep existing pointer and change size if possible. otherwise, allocate new thing and copy) return -1; } /// <summary> /// Invokes the allocator function of a block. /// </summary> /// <remarks>The allocator function is looked up from a global table. /// /// - If the block range's Pointer is null, it will allocate. /// - If the block range's Pointer is not null, it will reallocate. /// - If the block range's Items is 0, it will deallocate. /// </remarks> /// <param name="block">The block to allocate, deallocate, or reallocate.</param> /// <returns>0 if successful. Otherwise, returns the error code from the block's allocator function.</returns> public static unsafe int Try(ref Block block) { if (block.Range.Allocator.Value < FirstUserIndex) return TryLegacy(ref block); TableEntry tableEntry = default; tableEntry = block.Range.Allocator.TableEntry; var function = new FunctionPointer<TryFunction>(tableEntry.function); #if ENABLE_UNITY_ALLOCATION_CHECKS // if the allocator being passed in has a version of 0, that means "whatever the current version is." // so we patch it here, with whatever the current version is... if(block.Range.Allocator.Version == 0) block.Range.Allocator.Version = block.Range.Allocator.OfficialVersion; #endif #if !UNITY_IOS if (UseDelegate()) { int error = kErrorNone; forward_mono_allocate_block(ref block, ref error); return error; } #endif return allocate_block(ref block); } /// <summary> /// A stack allocator with no storage of its own. Uses the storage of its parent. /// </summary> [BurstCompile(CompileSynchronously = true)] internal struct StackAllocator : IAllocator, IDisposable { public AllocatorHandle Handle { get { return m_handle; } set { m_handle = value; } } public Allocator ToAllocator { get { return m_handle.ToAllocator; } } public bool IsCustomAllocator { get { return m_handle.IsCustomAllocator; } } internal AllocatorHandle m_handle; internal Block m_storage; internal long m_top; public void Initialize(Block storage) { m_storage = storage; m_top = 0; #if ENABLE_UNITY_ALLOCATION_CHECKS m_storage.Range.Allocator.AddChildAllocator(Handle); #endif } public unsafe int Try(ref Block block) { if (block.Range.Pointer == IntPtr.Zero) // Allocate { if (m_top + block.Bytes > m_storage.Bytes) { return -1; } block.Range.Pointer = (IntPtr)((byte*)m_storage.Range.Pointer + m_top); block.AllocatedItems = block.Range.Items; m_top += block.Bytes; return 0; } if (block.Bytes == 0) // Free { if ((byte*)block.Range.Pointer - (byte*)m_storage.Range.Pointer == (long)(m_top - block.AllocatedBytes)) { m_top -= block.AllocatedBytes; var blockSizeInBytes = block.AllocatedItems * block.BytesPerItem; block.Range.Pointer = IntPtr.Zero; block.AllocatedItems = 0; return 0; } return -1; } // Reallocate (keep existing pointer and change size if possible. otherwise, allocate new thing and copy) return -1; } [BurstCompile(CompileSynchronously = true)] [MonoPInvokeCallback(typeof(TryFunction))] public static unsafe int Try(IntPtr allocatorState, ref Block block) { return ((StackAllocator*)allocatorState)->Try(ref block); } public TryFunction Function => Try; public void Dispose() { m_handle.Rewind(); } } /// <summary> /// Slab allocator with no backing storage. /// </summary> [BurstCompile(CompileSynchronously = true)] internal struct SlabAllocator : IAllocator, IDisposable { public AllocatorHandle Handle { get { return m_handle; } set { m_handle = value; } } public Allocator ToAllocator { get { return m_handle.ToAllocator; } } public bool IsCustomAllocator { get { return m_handle.IsCustomAllocator; } } internal AllocatorHandle m_handle; internal Block Storage; internal int Log2SlabSizeInBytes; internal FixedList4096Bytes<int> Occupied; internal long budgetInBytes; internal long allocatedBytes; public long BudgetInBytes => budgetInBytes; public long AllocatedBytes => allocatedBytes; internal int SlabSizeInBytes { get => 1 << Log2SlabSizeInBytes; set => Log2SlabSizeInBytes = (byte)(32 - math.lzcnt(math.max(1, value) - 1)); } internal int Slabs => (int)(Storage.Bytes >> Log2SlabSizeInBytes); internal void Initialize(Block storage, int slabSizeInBytes, long budget) { #if ENABLE_UNITY_ALLOCATION_CHECKS storage.Range.Allocator.AddChildAllocator(Handle); #endif Assert.IsTrue((slabSizeInBytes & (slabSizeInBytes - 1)) == 0); Storage = storage; Log2SlabSizeInBytes = 0; Occupied = default; budgetInBytes = budget; allocatedBytes = 0; SlabSizeInBytes = slabSizeInBytes; Occupied.Length = (Slabs + 31) / 32; } public int Try(ref Block block) { if (block.Range.Pointer == IntPtr.Zero) // Allocate { if (block.Bytes + allocatedBytes > budgetInBytes) return -2; //over allocator budget if (block.Bytes > SlabSizeInBytes) return -1; for (var wordIndex = 0; wordIndex < Occupied.Length; ++wordIndex) { var word = Occupied[wordIndex]; if (word == -1) continue; for (var bitIndex = 0; bitIndex < 32; ++bitIndex) if ((word & (1 << bitIndex)) == 0) { Occupied[wordIndex] |= 1 << bitIndex; block.Range.Pointer = Storage.Range.Pointer + (int)(SlabSizeInBytes * (wordIndex * 32U + bitIndex)); block.AllocatedItems = SlabSizeInBytes / block.BytesPerItem; allocatedBytes += block.Bytes; return 0; } } return -1; } if (block.Bytes == 0) // Free { var slabIndex = ((ulong)block.Range.Pointer - (ulong)Storage.Range.Pointer) >> Log2SlabSizeInBytes; int wordIndex = (int)(slabIndex >> 5); int bitIndex = (int)(slabIndex & 31); Occupied[wordIndex] &= ~(1 << bitIndex); block.Range.Pointer = IntPtr.Zero; var blockSizeInBytes = block.AllocatedItems * block.BytesPerItem; allocatedBytes -= blockSizeInBytes; block.AllocatedItems = 0; return 0; } // Reallocate (keep existing pointer and change size if possible. otherwise, allocate new thing and copy) return -1; } [BurstCompile(CompileSynchronously = true)] [MonoPInvokeCallback(typeof(TryFunction))] public static unsafe int Try(IntPtr allocatorState, ref Block block) { return ((SlabAllocator*)allocatorState)->Try(ref block); } public TryFunction Function => Try; public void Dispose() { m_handle.Rewind(); } } internal struct TableEntry { internal IntPtr function; internal IntPtr state; } internal struct Array16<T> where T : unmanaged { internal T f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15; } internal struct Array256<T> where T : unmanaged { internal Array16<T> f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15; } internal struct Array4096<T> where T : unmanaged { internal Array256<T> f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15; } internal struct Array32768<T> : IIndexable<T> where T : unmanaged { internal Array4096<T> f0, f1, f2, f3, f4, f5, f6, f7; public int Length { get { return 32768; } set {} } public ref T ElementAt(int index) { unsafe { fixed(Array4096<T>* p = &f0) { return ref UnsafeUtility.AsRef<T>((T*)p + index); } } } } /// <summary> /// Contains arrays of the allocator function pointers. /// </summary> internal sealed class SharedStatics { internal sealed class IsInstalled { internal static readonly SharedStatic<Long1024> Ref = SharedStatic<Long1024>.GetOrCreate<IsInstalled>(); } internal sealed class TableEntry { internal static readonly SharedStatic<Array32768<AllocatorManager.TableEntry>> Ref = SharedStatic<Array32768<AllocatorManager.TableEntry>>.GetOrCreate<TableEntry>(); } #if ENABLE_UNITY_ALLOCATION_CHECKS internal sealed class Version { internal static readonly SharedStatic<Array32768<ushort>> Ref = SharedStatic<Array32768<ushort>>.GetOrCreate<Version>(); } internal sealed class ChildSafetyHandles { internal static readonly SharedStatic<Array32768<UnsafeList<AtomicSafetyHandle>>> Ref = SharedStatic<Array32768<UnsafeList<AtomicSafetyHandle>>>.GetOrCreate<ChildSafetyHandles>(); } internal sealed class ChildAllocators { internal static readonly SharedStatic<Array32768<UnsafeList<AllocatorHandle>>> Ref = SharedStatic<Array32768<UnsafeList<AllocatorHandle>>>.GetOrCreate<ChildAllocators>(); } internal sealed class Parent { internal static readonly SharedStatic<Array32768<AllocatorHandle>> Ref = SharedStatic<Array32768<AllocatorHandle>>.GetOrCreate<Parent>(); } internal sealed class IndexInParent { internal static readonly SharedStatic<Array32768<int>> Ref = SharedStatic<Array32768<int>>.GetOrCreate<IndexInParent>(); } #endif } internal static class Managed { #if !UNITY_IOS /// <summary> /// Memory allocation status /// </summary> internal const int kMaxNumCustomAllocator = 32768; internal static TryFunction[] TryFunctionDelegates = new TryFunction[kMaxNumCustomAllocator]; #endif /// <summary> /// Register TryFunction delegates for managed caller to avoid garbage collections /// </summary> /// <param name="index">Index into the TryFunction delegates table.</param> /// <param name="function">TryFunction delegate to be registered.</param> [NotBurstCompatible] public static void RegisterDelegate(int index, TryFunction function) { #if !UNITY_IOS if(index >= kMaxNumCustomAllocator) { throw new ArgumentException("index to be registered in TryFunction delegate table exceeds maximum."); } // Register TryFunction delegates for managed caller to avoid garbage collections Managed.TryFunctionDelegates[index] = function; #endif } /// <summary> /// Unregister TryFunction delegate /// </summary> /// <param name="int">Index into the TryFunction delegates table.</param> [NotBurstCompatible] public static void UnregisterDelegate(int index) { #if !UNITY_IOS if (index >= kMaxNumCustomAllocator) { throw new ArgumentException("index to be unregistered in TryFunction delegate table exceeds maximum."); } Managed.TryFunctionDelegates[index] = default; #endif } } /// <summary> /// For internal use only. /// </summary> public static void Initialize() { } /// <summary> /// Saves an allocator's function pointers at a particular index in the global function table. /// </summary> /// <param name="handle">The global function table index at which to install the allocator function.</param> /// <param name="allocatorState">IntPtr to allocator's custom state.</param> /// <param name="functionPointer">The allocator function to install in the global function table.</param> /// <param name="function">The allocator function to install in the global function table.</param> internal static void Install(AllocatorHandle handle, IntPtr allocatorState, FunctionPointer<TryFunction> functionPointer, TryFunction function) { if(functionPointer.Value == IntPtr.Zero) handle.Unregister(); else { int error = ConcurrentMask.TryAllocate(ref SharedStatics.IsInstalled.Ref.Data, handle.Value, 1); if (ConcurrentMask.Succeeded(error)) { handle.Install(new TableEntry { state = allocatorState, function = functionPointer.Value }); Managed.RegisterDelegate(handle.Index, function); } } } /// <summary> /// Saves an allocator's function pointers at a particular index in the global function table. /// </summary> /// <param name="handle">The global function table index at which to install the allocator function.</param> /// <param name="allocatorState">IntPtr to allocator's custom state.</param> /// <param name="function">The allocator function to install in the global function table.</param> internal static void Install(AllocatorHandle handle, IntPtr allocatorState, TryFunction function) { var functionPointer = (function == null) ? new FunctionPointer<TryFunction>(IntPtr.Zero) : BurstCompiler.CompileFunctionPointer(function); Install(handle, allocatorState, functionPointer, function); } /// <summary> /// Saves an allocator's function pointers in a free slot of the global function table. Thread safe. /// </summary> /// <param name="allocatorState">IntPtr to allocator's custom state.</param> /// <param name="functionPointer">Function pointer to create or save in the function table.</param> /// <returns>Returns a handle to the newly registered allocator function.</returns> internal static AllocatorHandle Register(IntPtr allocatorState, FunctionPointer<TryFunction> functionPointer) { var tableEntry = new TableEntry { state = allocatorState, function = functionPointer.Value }; var error = ConcurrentMask.TryAllocate(ref SharedStatics.IsInstalled.Ref.Data, out int offset, (FirstUserIndex+63)>>6, SharedStatics.IsInstalled.Ref.Data.Length, 1); AllocatorHandle handle = default; if(ConcurrentMask.Succeeded(error)) { handle.Index = (ushort)offset; handle.Install(tableEntry); #if ENABLE_UNITY_ALLOCATION_CHECKS handle.Version = handle.OfficialVersion; #endif } return handle; } /// <summary> /// Saves an allocator's function pointers in a free slot of the global function table. Thread safe. /// </summary> /// <typeparam name="T">The type of allocator to register.</typeparam> /// <param name="t">Reference to the allocator.</param> [NotBurstCompatible] public static unsafe void Register<T>(ref this T t) where T : unmanaged, IAllocator { var functionPointer = (t.Function == null) ? new FunctionPointer<TryFunction>(IntPtr.Zero) : BurstCompiler.CompileFunctionPointer(t.Function); t.Handle = Register((IntPtr)UnsafeUtility.AddressOf(ref t), functionPointer); Managed.RegisterDelegate(t.Handle.Index, t.Function); #if ENABLE_UNITY_ALLOCATION_CHECKS if (!t.Handle.IsValid) throw new InvalidOperationException("Allocator registration succeeded, but failed to produce valid handle."); #endif } /// <summary> /// Removes an allocator's function pointers from the global function table, without managed code /// </summary> /// <typeparam name="T">The type of allocator to unregister.</typeparam> /// <param name="t">Reference to the allocator.</param> public static void UnmanagedUnregister<T>(ref this T t) where T : unmanaged, IAllocator { if(t.Handle.IsInstalled) { t.Handle.Install(default); ConcurrentMask.TryFree(ref SharedStatics.IsInstalled.Ref.Data, t.Handle.Value, 1); } } /// <summary> /// Removes an allocator's function pointers from the global function table. /// </summary> /// <typeparam name="T">The type of allocator to unregister.</typeparam> /// <param name="t">Reference to the allocator.</param> [NotBurstCompatible] public static void Unregister<T>(ref this T t) where T : unmanaged, IAllocator { if(t.Handle.IsInstalled) { t.Handle.Install(default); ConcurrentMask.TryFree(ref SharedStatics.IsInstalled.Ref.Data, t.Handle.Value, 1); Managed.UnregisterDelegate(t.Handle.Index); } } /// <summary> /// Create a custom allocator by allocating a backing storage to store the allocator and then register it /// </summary> /// <typeparam name="T">The type of allocator to create.</typeparam> /// <param name="backingAllocator">Allocator used to allocate backing storage.</param> /// <returns>Returns reference to the newly created allocator.</returns> [NotBurstCompatible] internal static ref T CreateAllocator<T>(AllocatorHandle backingAllocator) where T : unmanaged, IAllocator { unsafe { var allocatorPtr = (T*)Memory.Unmanaged.Allocate(UnsafeUtility.SizeOf<T>(), 16, backingAllocator); *allocatorPtr = default; ref T allocator = ref UnsafeUtility.AsRef<T>(allocatorPtr); Register(ref allocator); return ref allocator; } } /// <summary> /// Destroy a custom allocator by unregistering the allocator and freeing its backing storage /// </summary> /// <typeparam name="T">The type of allocator to destroy.</typeparam> /// <param name="t">Reference to the allocator.</param> /// <param name="backingAllocator">Allocator used in allocating the backing storage.</param> [NotBurstCompatible] internal static void DestroyAllocator<T>(ref this T t, AllocatorHandle backingAllocator) where T : unmanaged, IAllocator { Unregister(ref t); unsafe { var allocatorPtr = UnsafeUtility.AddressOf<T>(ref t); Memory.Unmanaged.Free(allocatorPtr, backingAllocator); } } /// <summary> /// For internal use only. /// </summary> public static void Shutdown() { } /// <summary> /// Index in the global function table of the first user-defined allocator. /// </summary> /// <remarks>The indexes from 0 up to `FirstUserIndex` are reserved and so should not be used for your own allocators.</remarks> /// <value>Index in the global function table of the first user-defined allocator.</value> public const ushort FirstUserIndex = 64; internal static bool IsCustomAllocator(AllocatorHandle allocator) { return allocator.Index >= FirstUserIndex; } [Conditional("ENABLE_UNITY_ALLOCATION_CHECKS")] internal static void CheckFailedToAllocate(int error) { if (error != 0) throw new ArgumentException("failed to allocate"); } [Conditional("ENABLE_UNITY_ALLOCATION_CHECKS")] internal static void CheckFailedToFree(int error) { if (error != 0) throw new ArgumentException("failed to free"); } [Conditional("ENABLE_UNITY_ALLOCATION_CHECKS")] internal static void CheckValid(AllocatorHandle handle) { #if ENABLE_UNITY_ALLOCATION_CHECKS if(handle.IsValid == false) throw new ArgumentException("allocator handle is not valid."); #endif } } /// <summary> /// Provides a wrapper for custom allocator. /// </summary> [BurstCompatible(GenericTypeArguments = new[] { typeof(AllocatorManager.AllocatorHandle) })] public unsafe struct AllocatorHelper<T> : IDisposable where T : unmanaged, AllocatorManager.IAllocator { /// <summary> /// Pointer to a custom allocator. /// </summary> readonly T* m_allocator; /// <summary> /// Allocator used to allocate backing storage of T. /// </summary> AllocatorManager.AllocatorHandle m_backingAllocator; /// <summary> /// Get the custom allocator. /// </summary> public ref T Allocator => ref UnsafeUtility.AsRef<T>(m_allocator); /// <summary> /// Allocate the custom allocator from backingAllocator and register it. /// </summary> /// <param name="backingAllocator">Allocator used to allocate backing storage.</param> [NotBurstCompatible] public AllocatorHelper(AllocatorManager.AllocatorHandle backingAllocator) { ref var allocator = ref AllocatorManager.CreateAllocator<T>(backingAllocator); m_allocator = (T*)UnsafeUtility.AddressOf<T>(ref allocator); m_backingAllocator = backingAllocator; } /// <summary> /// Dispose the custom allocator backing memory and unregister it. /// </summary> [NotBurstCompatible] public void Dispose() { ref var allocator = ref UnsafeUtility.AsRef<T>(m_allocator); AllocatorManager.DestroyAllocator(ref allocator, m_backingAllocator); } } } #pragma warning restore 0649