A game about forced loneliness, made by TACStudios
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}