A game about forced loneliness, made by TACStudios
at master 509 lines 22 kB view raw
1using AOT; 2using System; 3using System.Runtime.CompilerServices; 4using System.Threading; 5using UnityEngine.Assertions; 6using Unity.Burst; 7using Unity.Collections.LowLevel.Unsafe; 8using Unity.Jobs.LowLevel.Unsafe; 9using Unity.Mathematics; 10 11namespace Unity.Collections 12{ 13 internal struct UnmanagedArray<T> : IDisposable where T : unmanaged 14 { 15 IntPtr m_pointer; 16 int m_length; 17 public int Length => m_length; 18 AllocatorManager.AllocatorHandle m_allocator; 19 public UnmanagedArray(int length, AllocatorManager.AllocatorHandle allocator) 20 { 21 unsafe 22 { 23 m_pointer = (IntPtr)Memory.Unmanaged.Array.Allocate<T>(length, allocator); 24 } 25 m_length = length; 26 m_allocator = allocator; 27 } 28 public void Dispose() 29 { 30 unsafe 31 { 32 Memory.Unmanaged.Free((T*)m_pointer, Allocator.Persistent); 33 } 34 } 35 public unsafe T* GetUnsafePointer() 36 { 37 return (T*)m_pointer; 38 } 39 public ref T this[int index] 40 { 41 [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 get { unsafe { return ref ((T*)m_pointer)[index]; } } 43 } 44 } 45 46 /// <summary> 47 /// An allocator that is fast like a linear allocator, is threadsafe, and automatically invalidates 48 /// all allocations made from it, when "rewound" by the user. 49 /// </summary> 50 [BurstCompile] 51 public struct RewindableAllocator : AllocatorManager.IAllocator 52 { 53 internal struct Union 54 { 55 internal long m_long; 56 57 // Number of bits used to store current position in a block to give out memory. 58 // This limits the maximum block size to 1TB (2^40). 59 const int currentBits = 40; 60 // Offset of current position in m_long 61 const int currentOffset = 0; 62 // Number of bits used to store the allocation count in a block 63 const long currentMask = (1L << currentBits) - 1; 64 65 // Number of bits used to store allocation count in a block. 66 // This limits the maximum number of allocations per block to 16 millions (2^24) 67 const int allocCountBits = 24; 68 // Offset of allocation count in m_long 69 const int allocCountOffset = currentOffset + currentBits; 70 const long allocCountMask = (1L << allocCountBits) - 1; 71 72 // Current position in a block to give out memory 73 internal long m_current 74 { 75 [MethodImpl(MethodImplOptions.AggressiveInlining)] 76 get 77 { 78 return (m_long >> currentOffset) & currentMask; 79 } 80 81 [MethodImpl(MethodImplOptions.AggressiveInlining)] 82 set 83 { 84 m_long &= ~(currentMask << currentOffset); 85 m_long |= (value & currentMask) << currentOffset; 86 } 87 } 88 89 // The number of allocations in a block 90 internal long m_allocCount 91 { 92 [MethodImpl(MethodImplOptions.AggressiveInlining)] 93 get 94 { 95 return (m_long >> allocCountOffset) & allocCountMask; 96 } 97 98 [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 set 100 { 101 m_long &= ~(allocCountMask << allocCountOffset); 102 m_long |= (value & allocCountMask) << allocCountOffset; 103 } 104 } 105 } 106 [GenerateTestsForBurstCompatibility] 107 internal unsafe struct MemoryBlock : IDisposable 108 { 109 // can't align any coarser than this many bytes 110 public const int kMaximumAlignment = 16384; 111 // pointer to contiguous memory 112 public byte* m_pointer; 113 // how many bytes of contiguous memory it points to 114 public long m_bytes; 115 // Union of current position to give out memory and allocation counts 116 public Union m_union; 117 118 public MemoryBlock(long bytes) 119 { 120 m_pointer = (byte*)Memory.Unmanaged.Allocate(bytes, kMaximumAlignment, Allocator.Persistent); 121 Assert.IsTrue(m_pointer != null, "Memory block allocation failed, system out of memory"); 122 m_bytes = bytes; 123 m_union = default; 124 } 125 126 public void Rewind() 127 { 128 m_union = default; 129 } 130 131 public void Dispose() 132 { 133 Memory.Unmanaged.Free(m_pointer, Allocator.Persistent); 134 m_pointer = null; 135 m_bytes = 0; 136 m_union = default; 137 } 138 139 public bool Contains(IntPtr ptr) 140 { 141 unsafe 142 { 143 void* pointer = (void*)ptr; 144 return (pointer >= m_pointer) && (pointer < m_pointer + m_union.m_current); 145 } 146 } 147 }; 148 149 // Log2 of Maximum memory block size. Cannot exceed MemoryBlock.Union.currentBits. 150 const int kLog2MaxMemoryBlockSize = 26; 151 152 // Maximum memory block size. Can exceed maximum memory block size if user requested more. 153 const long kMaxMemoryBlockSize = 1L << kLog2MaxMemoryBlockSize; // 64MB 154 155 /// Minimum memory block size, 128KB. 156 const long kMinMemoryBlockSize = 128 * 1024; 157 158 /// Maximum number of memory blocks. 159 const int kMaxNumBlocks = 64; 160 161 // Bit mask (bit 31) of the memory block busy flag indicating whether the block is busy rewinding. 162 const int kBlockBusyRewindMask = 0x1 << 31; 163 164 // Bit mask of the memory block busy flag indicating whether the block is busy allocating. 165 const int kBlockBusyAllocateMask = ~kBlockBusyRewindMask; 166 167 Spinner m_spinner; 168 AllocatorManager.AllocatorHandle m_handle; 169 UnmanagedArray<MemoryBlock> m_block; 170 int m_last; // highest-index block that has memory to allocate from 171 int m_used; // highest-index block that we actually allocated from, since last rewind 172 byte m_enableBlockFree; // flag indicating if allocator enables individual block free 173 byte m_reachMaxBlockSize; // flag indicating if reach maximum block size 174 175 /// <summary> 176 /// Initializes the allocator. Must be called before first use. 177 /// </summary> 178 /// <param name="initialSizeInBytes">The initial capacity of the allocator, in bytes</param> 179 /// <param name="enableBlockFree">A flag indicating if allocator enables individual block free</param> 180 public void Initialize(int initialSizeInBytes, bool enableBlockFree = false) 181 { 182 m_spinner = default; 183 m_block = new UnmanagedArray<MemoryBlock>(kMaxNumBlocks, Allocator.Persistent); 184 // Initial block size should be larger than min block size 185 var blockSize = initialSizeInBytes > kMinMemoryBlockSize ? initialSizeInBytes : kMinMemoryBlockSize; 186 m_block[0] = new MemoryBlock(blockSize); 187 m_last = m_used = 0; 188 m_enableBlockFree = enableBlockFree ? (byte)1 : (byte)0; 189 m_reachMaxBlockSize = (initialSizeInBytes >= kMaxMemoryBlockSize) ? (byte)1 : (byte)0; 190 } 191 192 /// <summary> 193 /// Property to get and set enable block free flag, a flag indicating whether the allocator should enable individual block to be freed. 194 /// </summary> 195 public bool EnableBlockFree 196 { 197 get => m_enableBlockFree != 0; 198 set => m_enableBlockFree = value ? (byte)1 : (byte)0; 199 } 200 201 /// <summary> 202 /// Retrieves the number of memory blocks that the allocator has requested from the system. 203 /// </summary> 204 public int BlocksAllocated => (int)(m_last + 1); 205 206 /// <summary> 207 /// Retrieves the size of the initial memory block, as requested in the Initialize function. 208 /// </summary> 209 public int InitialSizeInBytes => (int)(m_block[0].m_bytes); 210 211 /// <summary> 212 /// Retrieves the maximum memory block size. 213 /// </summary> 214 internal long MaxMemoryBlockSize => kMaxMemoryBlockSize; 215 216 /// <summary> 217 /// Retrieves the total bytes of the memory blocks allocated by this allocator. 218 /// </summary> 219 internal long BytesAllocated 220 { 221 get 222 { 223 long totalBytes = 0; 224 for(int i = 0; i <= m_last; i++) 225 { 226 totalBytes += m_block[i].m_bytes; 227 } 228 return totalBytes; 229 } 230 } 231 232 /// <summary> 233 /// Rewind the allocator; invalidate all allocations made from it, and potentially also free memory blocks 234 /// it has allocated from the system. 235 /// </summary> 236 public void Rewind() 237 { 238 if (JobsUtility.IsExecutingJob) 239 throw new InvalidOperationException("You cannot Rewind a RewindableAllocator from a Job."); 240 m_handle.Rewind(); // bump the allocator handle version, invalidate all dependents 241 while (m_last > m_used) // *delete* all blocks we didn't even allocate from this time around. 242 m_block[m_last--].Dispose(); 243 while (m_used > 0) // simply *rewind* all blocks we used in this update, to avoid allocating again, every update. 244 m_block[m_used--].Rewind(); 245 m_block[0].Rewind(); 246 } 247 248 /// <summary> 249 /// Dispose the allocator. This must be called to free the memory blocks that were allocated from the system. 250 /// </summary> 251 public void Dispose() 252 { 253 if (JobsUtility.IsExecutingJob) 254 throw new InvalidOperationException("You cannot Dispose a RewindableAllocator from a Job."); 255 m_used = 0; // so that we delete all blocks in Rewind() on the next line 256 Rewind(); 257 m_block[0].Dispose(); 258 m_block.Dispose(); 259 m_last = m_used = 0; 260 } 261 262 /// <summary> 263 /// All allocators must implement this property, in order to be installed in the custom allocator table. 264 /// </summary> 265 [ExcludeFromBurstCompatTesting("Uses managed delegate")] 266 public AllocatorManager.TryFunction Function => Try; 267 268 unsafe int TryAllocate(ref AllocatorManager.Block block, int startIndex, int lastIndex, long alignedSize, long alignmentMask) 269 { 270 for (int best = startIndex; best <= lastIndex; best++) 271 { 272 Union oldUnion; 273 Union readUnion = default; 274 long begin = 0; 275 bool skip = false; 276 readUnion.m_long = Interlocked.Read(ref m_block[best].m_union.m_long); 277 do 278 { 279 begin = (readUnion.m_current + alignmentMask) & ~alignmentMask; 280 if (begin + block.Bytes > m_block[best].m_bytes) 281 { 282 skip = true; 283 break; 284 } 285 oldUnion = readUnion; 286 Union newUnion = default; 287 newUnion.m_current = (begin + alignedSize) > m_block[best].m_bytes ? m_block[best].m_bytes : (begin + alignedSize); 288 newUnion.m_allocCount = readUnion.m_allocCount + 1; 289 readUnion.m_long = Interlocked.CompareExchange(ref m_block[best].m_union.m_long, newUnion.m_long, oldUnion.m_long); 290 } while (readUnion.m_long != oldUnion.m_long); 291 292 if(skip) 293 { 294 continue; 295 } 296 297 block.Range.Pointer = (IntPtr)(m_block[best].m_pointer + begin); 298 block.AllocatedItems = block.Range.Items; 299 300 Interlocked.MemoryBarrier(); 301 int oldUsed; 302 int readUsed; 303 int newUsed; 304 readUsed = m_used; 305 do 306 { 307 oldUsed = readUsed; 308 newUsed = best > oldUsed ? best : oldUsed; 309 readUsed = Interlocked.CompareExchange(ref m_used, newUsed, oldUsed); 310 } while (newUsed != oldUsed); 311 312 return AllocatorManager.kErrorNone; 313 } 314 315 return AllocatorManager.kErrorBufferOverflow; 316 } 317 318 /// <summary> 319 /// Try to allocate, free, or reallocate a block of memory. This is an internal function, and 320 /// is not generally called by the user. 321 /// </summary> 322 /// <param name="block">The memory block to allocate, free, or reallocate</param> 323 /// <returns>0 if successful. Otherwise, returns the error code from the allocator function.</returns> 324 public int Try(ref AllocatorManager.Block block) 325 { 326 if (block.Range.Pointer == IntPtr.Zero) 327 { 328 // Make the alignment multiple of cacheline size 329 var alignment = math.max(JobsUtility.CacheLineSize, block.Alignment); 330 var extra = alignment != JobsUtility.CacheLineSize ? 1 : 0; 331 var cachelineMask = JobsUtility.CacheLineSize - 1; 332 if (extra == 1) 333 { 334 alignment = (alignment + cachelineMask) & ~cachelineMask; 335 } 336 337 // Adjust the size to be multiple of alignment, add extra alignment 338 // to size if alignment is more than cacheline size 339 var mask = alignment - 1L; 340 var size = (block.Bytes + extra * alignment + mask) & ~mask; 341 342 // Check all the blocks to see if any of them have enough memory 343 var last = m_last; 344 int error = TryAllocate(ref block, 0, m_last, size, mask); 345 if (error == AllocatorManager.kErrorNone) 346 { 347 return error; 348 } 349 350 // If that fails, allocate another block that's guaranteed big enough, and allocate from it. 351 // Allocate twice as much as last time until it reaches MaxMemoryBlockSize, after that, increase 352 // the block size by MaxMemoryBlockSize. 353 m_spinner.Acquire(); 354 355 // After getting the lock, we must try to allocate again, because if many threads waited at 356 // the lock, the first one allocates and when it unlocks, it's likely that there's space for the 357 // other threads' allocations in the first thread's block. 358 error = TryAllocate(ref block, last, m_last, size, mask); 359 if (error == AllocatorManager.kErrorNone) 360 { 361 m_spinner.Release(); 362 return error; 363 } 364 365 long bytes; 366 if (m_reachMaxBlockSize == 0) 367 { 368 bytes = m_block[m_last].m_bytes << 1; 369 } 370 else 371 { 372 bytes = m_block[m_last].m_bytes + kMaxMemoryBlockSize; 373 } 374 // if user asks more, skip smaller sizes 375 bytes = math.max(bytes, size); 376 m_reachMaxBlockSize = (bytes >= kMaxMemoryBlockSize) ? (byte)1 : (byte)0; 377 m_block[m_last + 1] = new MemoryBlock(bytes); 378 Interlocked.Increment(ref m_last); 379 error = TryAllocate(ref block, m_last, m_last, size, mask); 380 m_spinner.Release(); 381 return error; 382 } 383 384 // To free memory, no-op unless allocator enables individual block to be freed 385 if (block.Range.Items == 0) 386 { 387 if (m_enableBlockFree != 0) 388 { 389 for (int blockIndex = 0; blockIndex <= m_last; ++blockIndex) 390 { 391 if (m_block[blockIndex].Contains(block.Range.Pointer)) 392 { 393 Union oldUnion; 394 Union readUnion = default; 395 readUnion.m_long = Interlocked.Read(ref m_block[blockIndex].m_union.m_long); 396 do 397 { 398 oldUnion = readUnion; 399 Union newUnion = readUnion; 400 newUnion.m_allocCount--; 401 if (newUnion.m_allocCount == 0) 402 { 403 newUnion.m_current = 0; 404 } 405 readUnion.m_long = Interlocked.CompareExchange(ref m_block[blockIndex].m_union.m_long, newUnion.m_long, oldUnion.m_long); 406 } while (readUnion.m_long != oldUnion.m_long); 407 } 408 } 409 } 410 return 0; // we could check to see if the pointer belongs to us, if we want to be strict about it. 411 } 412 413 return -1; 414 } 415 416 [BurstCompile] 417 [MonoPInvokeCallback(typeof(AllocatorManager.TryFunction))] 418 internal static int Try(IntPtr state, ref AllocatorManager.Block block) 419 { 420 unsafe { return ((RewindableAllocator*)state)->Try(ref block); } 421 } 422 423 /// <summary> 424 /// Retrieve the AllocatorHandle associated with this allocator. The handle is used as an index into a 425 /// global table, for times when a reference to the allocator object isn't available. 426 /// </summary> 427 /// <value>The AllocatorHandle retrieved.</value> 428 public AllocatorManager.AllocatorHandle Handle { get { return m_handle; } set { m_handle = value; } } 429 430 /// <summary> 431 /// Retrieve the Allocator associated with this allocator. 432 /// </summary> 433 /// <value>The Allocator retrieved.</value> 434 public Allocator ToAllocator { get { return m_handle.ToAllocator; } } 435 436 /// <summary> 437 /// Check whether this AllocatorHandle is a custom allocator. 438 /// </summary> 439 /// <value>True if this AllocatorHandle is a custom allocator.</value> 440 public bool IsCustomAllocator { get { return m_handle.IsCustomAllocator; } } 441 442 /// <summary> 443 /// Check whether this allocator will automatically dispose allocations. 444 /// </summary> 445 /// <remarks>Allocations made by Rewindable allocator are automatically disposed.</remarks> 446 /// <value>Always true</value> 447 public bool IsAutoDispose { get { return true; } } 448 449 /// <summary> 450 /// Allocate a NativeArray of type T from memory that is guaranteed to remain valid until the end of the 451 /// next Update of this World. There is no need to Dispose the NativeArray so allocated. It is not possible 452 /// to free the memory by Disposing it - it is automatically freed after the end of the next Update for this 453 /// World. 454 /// </summary> 455 /// <typeparam name="T">The element type of the NativeArray to allocate.</typeparam> 456 /// <param name="length">The length of the NativeArray to allocate, measured in elements.</param> 457 /// <returns>The NativeArray allocated by this function.</returns> 458 [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int) })] 459 public NativeArray<T> AllocateNativeArray<T>(int length) where T : unmanaged 460 { 461 var container = new NativeArray<T>(); 462 unsafe 463 { 464 container.m_Buffer = this.AllocateStruct(default(T), length); 465 } 466 container.m_Length = length; 467 container.m_AllocatorLabel = Allocator.None; 468#if ENABLE_UNITY_COLLECTIONS_CHECKS 469 container.m_MinIndex = 0; 470 container.m_MaxIndex = length - 1; 471 container.m_Safety = CollectionHelper.CreateSafetyHandle(ToAllocator); 472 CollectionHelper.SetStaticSafetyId<NativeArray<T>>(ref container.m_Safety, ref NativeArrayExtensions.NativeArrayStaticId<T>.s_staticSafetyId.Data); 473 Handle.AddSafetyHandle(container.m_Safety); 474#endif 475 return container; 476 } 477 478 /// <summary> 479 /// Allocate a NativeList of type T from memory that is guaranteed to remain valid until the end of the 480 /// next Update of this World. There is no need to Dispose the NativeList so allocated. It is not possible 481 /// to free the memory by Disposing it - it is automatically freed after the end of the next Update for this 482 /// World. The NativeList must be initialized with its maximum capacity; if it were to dynamically resize, 483 /// up to 1/2 of the total final capacity would be wasted, because the memory can't be dynamically freed. 484 /// </summary> 485 /// <typeparam name="T">The element type of the NativeList to allocate.</typeparam> 486 /// <param name="capacity">The capacity of the NativeList to allocate, measured in elements.</param> 487 /// <returns>The NativeList allocated by this function.</returns> 488 [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int) })] 489 public NativeList<T> AllocateNativeList<T>(int capacity) where T : unmanaged 490 { 491 var container = new NativeList<T>(); 492 unsafe 493 { 494 container.m_ListData = this.Allocate(default(UnsafeList<T>), 1); 495 container.m_ListData->Ptr = this.Allocate(default(T), capacity); 496 container.m_ListData->m_length = 0; 497 container.m_ListData->m_capacity = capacity; 498 container.m_ListData->Allocator = Allocator.None; 499 } 500#if ENABLE_UNITY_COLLECTIONS_CHECKS 501 container.m_Safety = CollectionHelper.CreateSafetyHandle(ToAllocator); 502 CollectionHelper.SetStaticSafetyId<NativeList<T>>(ref container.m_Safety, ref NativeList<T>.s_staticSafetyId.Data); 503 AtomicSafetyHandle.SetBumpSecondaryVersionOnScheduleWrite(container.m_Safety, true); 504 Handle.AddSafetyHandle(container.m_Safety); 505#endif 506 return container; 507 } 508 } 509}