using System; using System.Diagnostics; using System.Runtime.CompilerServices; using Unity.Jobs; using System.Runtime.InteropServices; namespace Unity.Collections.LowLevel.Unsafe { /// <summary> /// A fixed-size circular buffer. /// </summary> /// <typeparam name="T">The type of the elements.</typeparam> [DebuggerDisplay("Length = {Length}, Capacity = {Capacity}, IsCreated = {IsCreated}, IsEmpty = {IsEmpty}")] [DebuggerTypeProxy(typeof(UnsafeRingQueueDebugView<>))] [GenerateTestsForBurstCompatibility(GenericTypeArguments = new [] { typeof(int) })] [StructLayout(LayoutKind.Sequential)] public unsafe struct UnsafeRingQueue<T> : INativeDisposable where T : unmanaged { /// <summary> /// The internal buffer where the content is stored. /// </summary> /// <value>The internal buffer where the content is stored.</value> [NativeDisableUnsafePtrRestriction] public T* Ptr; /// <summary> /// The allocator used to create the internal buffer. /// </summary> /// <value>The allocator used to create the internal buffer.</value> public AllocatorManager.AllocatorHandle Allocator; internal readonly int m_Capacity; internal int m_Filled; internal int m_Write; internal int m_Read; /// <summary> /// Whether the queue is empty. /// </summary> /// <value>True if the queue is empty or the queue has not been constructed.</value> public readonly bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Filled == 0; } /// <summary> /// The number of elements currently in this queue. /// </summary> /// <value>The number of elements currently in this queue.</value> public readonly int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Filled; } /// <summary> /// The number of elements that fit in the internal buffer. /// </summary> /// <value>The number of elements that fit in the internal buffer.</value> public readonly int Capacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Capacity; } /// <summary> /// Initializes and returns an instance of UnsafeRingQueue which aliasing an existing buffer. /// </summary> /// <param name="ptr">An existing buffer to set as the internal buffer.</param> /// <param name="capacity">The capacity.</param> public UnsafeRingQueue(T* ptr, int capacity) { Ptr = ptr; Allocator = AllocatorManager.None; m_Capacity = capacity; m_Filled = 0; m_Write = 0; m_Read = 0; } /// <summary> /// Initializes and returns an instance of UnsafeRingQueue. /// </summary> /// <param name="capacity">The capacity.</param> /// <param name="allocator">The allocator to use.</param> /// <param name="options">Whether newly allocated bytes should be zeroed out.</param> public UnsafeRingQueue(int capacity, AllocatorManager.AllocatorHandle allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory) { Allocator = allocator; m_Capacity = capacity; m_Filled = 0; m_Write = 0; m_Read = 0; var sizeInBytes = capacity * UnsafeUtility.SizeOf<T>(); Ptr = (T*)Memory.Unmanaged.Allocate(sizeInBytes, 16, allocator); if (options == NativeArrayOptions.ClearMemory) { UnsafeUtility.MemClear(Ptr, sizeInBytes); } } internal static UnsafeRingQueue<T>* Alloc(AllocatorManager.AllocatorHandle allocator) { UnsafeRingQueue<T>* data = (UnsafeRingQueue<T>*)Memory.Unmanaged.Allocate(sizeof(UnsafeRingQueue<T>), UnsafeUtility.AlignOf<UnsafeRingQueue<T>>(), allocator); return data; } internal static void Free(UnsafeRingQueue<T>* data) { if (data == null) { throw new InvalidOperationException("UnsafeRingQueue has yet to be created or has been destroyed!"); } var allocator = data->Allocator; data->Dispose(); Memory.Unmanaged.Free(data, allocator); } /// <summary> /// Whether this queue has been allocated (and not yet deallocated). /// </summary> /// <value>True if this queue has been allocated (and not yet deallocated).</value> public readonly bool IsCreated { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Ptr != null; } /// <summary> /// Releases all resources (memory and safety handles). /// </summary> public void Dispose() { if (!IsCreated) { return; } if (CollectionHelper.ShouldDeallocate(Allocator)) { Memory.Unmanaged.Free(Ptr, Allocator); Allocator = AllocatorManager.Invalid; } Ptr = null; } /// <summary> /// Creates and schedules a job that will dispose this queue. /// </summary> /// <param name="inputDeps">The handle of a job which the new job will depend upon.</param> /// <returns>The handle of a new job that will dispose this queue. The new job depends upon inputDeps.</returns> public JobHandle Dispose(JobHandle inputDeps) { if (!IsCreated) { return inputDeps; } if (CollectionHelper.ShouldDeallocate(Allocator)) { var jobHandle = new UnsafeDisposeJob { Ptr = Ptr, Allocator = Allocator }.Schedule(inputDeps); Ptr = null; Allocator = AllocatorManager.Invalid; return jobHandle; } Ptr = null; return inputDeps; } [MethodImpl(MethodImplOptions.AggressiveInlining)] bool TryEnqueueInternal(T value) { if (m_Filled == m_Capacity) return false; Ptr[m_Write] = value; m_Write++; if (m_Write == m_Capacity) m_Write = 0; m_Filled++; return true; } /// <summary> /// Adds an element at the front of the queue. /// </summary> /// <remarks>Does nothing if the queue is full.</remarks> /// <param name="value">The value to be added.</param> /// <returns>True if the value was added.</returns> public bool TryEnqueue(T value) { return TryEnqueueInternal(value); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] static void ThrowQueueFull() { throw new InvalidOperationException("Trying to enqueue into full queue."); } /// <summary> /// Adds an element at the front of the queue. /// </summary> /// <param name="value">The value to be added.</param> /// <exception cref="InvalidOperationException">Thrown if the queue was full.</exception> public void Enqueue(T value) { if (!TryEnqueueInternal(value)) { ThrowQueueFull(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] bool TryDequeueInternal(out T item) { item = Ptr[m_Read]; if (m_Filled == 0) return false; m_Read = m_Read + 1; if (m_Read == m_Capacity) m_Read = 0; m_Filled--; return true; } /// <summary> /// Removes the element from the end of the queue. /// </summary> /// <remarks>Does nothing if the queue is empty.</remarks> /// <param name="item">Outputs the element removed.</param> /// <returns>True if an element was removed.</returns> public bool TryDequeue(out T item) { return TryDequeueInternal(out item); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] static void ThrowQueueEmpty() { throw new InvalidOperationException("Trying to dequeue from an empty queue"); } /// <summary> /// Removes the element from the end of the queue. /// </summary> /// <exception cref="InvalidOperationException">Thrown if the queue was empty.</exception> /// <returns>Returns the removed element.</returns> public T Dequeue() { if (!TryDequeueInternal(out T item)) { ThrowQueueEmpty(); } return item; } } internal sealed class UnsafeRingQueueDebugView<T> where T : unmanaged { UnsafeRingQueue<T> Data; public UnsafeRingQueueDebugView(UnsafeRingQueue<T> data) { Data = data; } public unsafe T[] Items { get { T[] result = new T[Data.Length]; var read = Data.m_Read; var capacity = Data.m_Capacity; for (var i = 0; i < result.Length; ++i) { result[i] = Data.Ptr[(read + i) % capacity]; } return result; } } } }