using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
namespace Goap.Goap.Resolver
{
using static UnsafeUtility;
public struct NativeHeapIndex
{
internal int TableIndex;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
internal int Version;
internal int StructureId;
#endif
}
///
/// This is a basic implementation of the MinHeap/MaxHeap data structure. It allows you
/// to insert objects into the container with a O(log(n)) cost per item, and it allows you
/// to extract the min/max from the container with a O(log(n)) cost per item.
/// This implementation provides the ability to remove items from the middle of the container
/// as well. This is a critical operation when implementing algorithms like a-star. When an
/// item is added to the container, an index is returned which can be used to later remove
/// the item no matter where it is in the heap, for the same cost of removing it if it was
/// popped normally.
/// This container is parameterized with a comparator type that defines the ordering of the
/// container. The default form of the comparator can be used, or you can specify your own.
/// The item that comes first in the ordering is the one that will be returned by the Pop
/// operation. This allows you to use the comparator to parameterize this collection into a
/// MinHeap, MaxHeap, or other type of ordered heap using your own custom type.
/// For convenience, this library contains the Min and Max comparator, which provide
/// comparisons for all built in primitives.
///
[NativeContainer]
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(NativeHeapDebugView<,>))]
[StructLayout(LayoutKind.Sequential)]
public struct NativeHeap : IDisposable
where T : unmanaged
where U : unmanaged, IComparer
{
#region API
public const int DEFAULT_CAPACITY = 128;
///
/// Returns the number of elements that this collection can hold before the internal structures
/// need to be reallocated.
///
public int Capacity
{
get
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckReadAndThrow(this.m_Safety);
#endif
return this.Data->Capacity;
}
}
set
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
if (value < this.Data->Count)
{
throw new ArgumentException(
$"Capacity of {value} cannot be smaller than count of {this.Data->Count}.");
}
#endif
var newTable = (TableValue*) Malloc(SizeOf() * value, AlignOf(),
this.Allocator);
var newHeap = (HeapNode*) Malloc(SizeOf>() * value, AlignOf>(),
this.Allocator);
var toCopy = this.Data->Capacity < value ? this.Data->Capacity : value;
MemCpy(newTable, this.Data->Table, toCopy * SizeOf());
MemCpy(newHeap, this.Data->Heap, toCopy * SizeOf>());
for (var i = 0; i < value - this.Data->Capacity; i++)
{
//For each new heap node, make sure that it has a new unique index
newHeap[i + this.Data->Capacity] = new HeapNode()
{
TableIndex = i + this.Data->Capacity,
};
#if ENABLE_UNITY_COLLECTIONS_CHECKS
//For each new table value, make sure it has a specific version
newTable[i + this.Data->Capacity] = new TableValue()
{
Version = 1,
};
#endif
}
Free(this.Data->Table, this.Allocator);
Free(this.Data->Heap, this.Allocator);
this.Data->Table = newTable;
this.Data->Heap = newHeap;
this.Data->Capacity = value;
}
}
}
///
/// Returns the number of elements currently contained inside this collection.
///
public int Count
{
get
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckReadAndThrow(this.m_Safety);
#endif
return this.Data->Count;
}
}
}
///
/// Gets or sets the comparator used for this Heap. Note that you can only set the comparator
/// when the Heap is empty.
///
public U Comparator
{
get
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckReadAndThrow(this.m_Safety);
#endif
return this.Data->Comparator;
}
}
set
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
if (this.Data->Count != 0)
{
throw new InvalidOperationException(
"Can only change the comparator of a NativeHeap when it is empty.");
}
#endif
this.Data->Comparator = value;
}
}
}
///
/// Constructs a new NativeHeap using the given Allocator. You must call Dispose on this collection
/// when you are finished with it.
///
///
/// You must specify an allocator to use for the creation of the internal data structures.
///
///
/// You can optionally specify the default number of elements this collection can contain before the internal
/// data structures need to be re-allocated.
///
///
/// You can optionally specify the comparator used to order the elements in this collection. The Pop operation will
/// always return the smallest element according to the ordering specified by this comparator.
///
public NativeHeap(Allocator allocator, int initialCapacity = DEFAULT_CAPACITY, U comparator = default) :
this(initialCapacity, comparator, allocator, disposeSentinelStackDepth: 1) { }
///
/// Disposes of this container and deallocates its memory immediately.
/// Any NativeHeapIndex structures obtained will be invalidated and cannot be used again.
///
public void Dispose()
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
DisposeSentinel.Dispose(ref this.m_Safety, ref this.m_DisposeSentinel);
#endif
this.Data->Count = 0;
this.Data->Capacity = 0;
Free(this.Data->Heap, this.Allocator);
Free(this.Data->Table, this.Allocator);
Free(this.Data, this.Allocator);
}
}
///
/// Removes all elements from this container. Any NativeHeapIndex structures obtained will be
/// invalidated and cannot be used again.
///
public void Clear()
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
for (var i = 0; i < this.Data->Count; i++)
{
var node = this.Data->Heap[i];
this.Data->Table[node.TableIndex].Version++;
}
#endif
this.Data->Count = 0;
}
}
///
/// Returns whether or not the given NativeHeapIndex is a valid index for this container. If true,
/// that index can be used to Remove the element tied to that index from the container.
/// This method will always return true if Unity safety checks is turned off.
///
public bool IsValidIndex(NativeHeapIndex index)
{
var isValid = true;
var errorCode = 0;
this.IsValidIndexInternal(index, ref isValid, ref errorCode);
return isValid;
}
///
/// Throws an ArgumentException if the provided NativeHeapIndex is not valid for this container.
/// This method will never throw if Unity safety checks is turned off.
///
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
public void AssertValidIndex(NativeHeapIndex index)
{
var isValid = true;
var errorCode = 0;
this.IsValidIndexInternal(index, ref isValid, ref errorCode);
if (isValid)
{
return;
}
switch (errorCode)
{
case VALIDATION_ERROR_WRONG_INSTANCE:
throw new ArgumentException("The provided ItemHandle was not valid for this NativeHeap. " +
"It was taken from a different instance.");
case VALIDATION_ERROR_INVALID:
throw new ArgumentException("The provided ItemHandle was not valid for this NativeHeap.");
case VALIDATION_ERROR_REMOVED:
throw new ArgumentException("The provided ItemHandle was not valid for this NativeHeap. " +
"The item it pointed to might have already been removed.");
}
}
///
/// Returns the next element that would be obtained if Pop was called. This is the first/smallest
/// item according to the ordering specified by the comparator.
/// This method is an O(1) operation.
/// This method will throw an InvalidOperationException if the collection is empty.
///
public T Peek()
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckReadAndThrow(this.m_Safety);
#endif
if (!this.TryPeek(out var t))
{
throw new InvalidOperationException("Cannot Peek NativeHeap when the count is zero.");
}
return t;
}
///
/// Returns the next element that would be obtained if Pop was called. This is the first/smallest
/// item according to the ordering specified by the comparator.
/// This method is an O(1) operation.
/// This method will return true if an element could be obtained, or false if the container is empty.
///
public bool TryPeek(out T t)
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckReadAndThrow(this.m_Safety);
#endif
if (this.Data->Count == 0)
{
t = default;
return false;
}
else
{
unsafe
{
t = this.Data->Heap[0].Item;
return true;
}
}
}
}
///
/// Removes the first/smallest element from the container and returns it.
/// This method is an O(log(n)) operation.
/// This method will throw an InvalidOperationException if the collection is empty.
///
public T Pop()
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
#endif
if (!this.TryPop(out var t))
{
throw new InvalidOperationException("Cannot Pop NativeHeap when the count is zero.");
}
return t;
}
///
/// Removes the first/smallest element from the container and returns it.
/// This method is an O(log(n)) operation.
/// This method will return true if an element could be obtained, or false if the container is empty.
///
public bool TryPop(out T t)
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
#endif
if (this.Data->Count == 0)
{
t = default;
return false;
}
var rootNode = this.Data->Heap[0];
#if ENABLE_UNITY_COLLECTIONS_CHECKS
//Update version to invalidate all existing handles
this.Data->Table[rootNode.TableIndex].Version++;
#endif
//Grab the last node off the end and remove it
var lastNodeIndex = --this.Data->Count;
var lastNode = this.Data->Heap[lastNodeIndex];
//Move the previous root to the end of the array to fill the space we just made
this.Data->Heap[lastNodeIndex] = rootNode;
//Finally insert the previously last node at the root and bubble it down
this.InsertAndBubbleDown(lastNode, 0);
t = rootNode.Item;
return true;
}
}
///
/// Inserts the provided element into the container. It may later be removed by a call to Pop,
/// TryPop, or Remove.
/// This method returns a NativeHeapIndex. This index can later be used to Remove the item from
/// the collection. Once the item is removed by any means, this NativeHeapIndex will become invalid.
/// If an item is re-added to the collection after it has been removed, Insert will return a NEW
/// index that is distinct from the previous index. Each index can only be used exactly once to
/// remove a single item.
/// This method is an O(log(n)) operation.
///
public NativeHeapIndex Insert(in T t)
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
#endif
if (this.Data->Count == this.Data->Capacity)
{
this.Capacity *= 2;
}
var node = this.Data->Heap[this.Data->Count];
node.Item = t;
var insertIndex = this.Data->Count++;
this.InsertAndBubbleUp(node, insertIndex);
return new NativeHeapIndex()
{
TableIndex = node.TableIndex,
#if ENABLE_UNITY_COLLECTIONS_CHECKS
Version = this.Data->Table[node.TableIndex].Version,
StructureId = this.Id,
#endif
};
}
}
///
/// Removes the element tied to this NativeHeapIndex from the container. The NativeHeapIndex must be
/// the result of a previous call to Insert on this container. If the item has already been removed by
/// any means, this method will throw an ArgumentException.
/// This method will invalidate the provided index. If you re-insert the removed object, you must use
/// the NEW index to remove it again.
/// This method is an O(log(n)) operation.
///
public T Remove(NativeHeapIndex index)
{
unsafe
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
this.AssertValidIndex(index);
#endif
var indexToRemove = this.Data->Table[index.TableIndex].HeapIndex;
var toRemove = this.Data->Heap[indexToRemove];
#if ENABLE_UNITY_COLLECTIONS_CHECKS
this.Data->Table[toRemove.TableIndex].Version++;
#endif
var lastNode = this.Data->Heap[--this.Data->Count];
//First we move the node to remove to the end of the heap
WriteArrayElement(this.Data->Heap, this.Data->Count, toRemove);
if (indexToRemove != 0)
{
var parentIndex = (indexToRemove - 1) / 2;
var parentNode = this.Data->Heap[parentIndex];
if (this.Data->Comparator.Compare(lastNode.Item, parentNode.Item) < 0)
{
this.InsertAndBubbleUp(lastNode, indexToRemove);
return toRemove.Item;
}
}
//If we couldn't bubble up, bubbling down instead
this.InsertAndBubbleDown(lastNode, indexToRemove);
return toRemove.Item;
}
}
#endregion
#region IMPLEMENTATION
internal const int VALIDATION_ERROR_WRONG_INSTANCE = 1;
internal const int VALIDATION_ERROR_INVALID = 2;
internal const int VALIDATION_ERROR_REMOVED = 3;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
internal unsafe static int NextId = 1;
internal unsafe int Id;
internal unsafe AtomicSafetyHandle m_Safety;
[NativeSetClassTypeToNullOnSchedule]
internal unsafe DisposeSentinel m_DisposeSentinel;
#endif
[NativeDisableUnsafePtrRestriction]
internal unsafe HeapData* Data;
internal unsafe Allocator Allocator;
internal unsafe NativeHeap(
int initialCapacity, U comparator, Allocator allocator,
int disposeSentinelStackDepth
)
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
if (initialCapacity <= 0)
{
throw new ArgumentException(nameof(initialCapacity),
"Must provide an initial capacity that is greater than zero.");
}
if (allocator == Allocator.None ||
allocator == Allocator.Invalid ||
allocator == Allocator.AudioKernel)
{
throw new ArgumentException(nameof(allocator),
"Must provide an Allocator type of Temp, TempJob, or Persistent.");
}
DisposeSentinel.Create(out this.m_Safety, out this.m_DisposeSentinel, disposeSentinelStackDepth, allocator);
this.Id = Interlocked.Increment(ref NextId);
#endif
this.Data = (HeapData*) Malloc(SizeOf>(), AlignOf>(), allocator);
this.Data->Heap = (HeapNode*) Malloc(SizeOf>() * initialCapacity, AlignOf>(),
allocator);
this.Data->Table =
(TableValue*) Malloc(SizeOf() * initialCapacity, AlignOf(), allocator);
this.Allocator = allocator;
for (var i = 0; i < initialCapacity; i++)
{
this.Data->Heap[i] = new HeapNode()
{
TableIndex = i,
};
#if ENABLE_UNITY_COLLECTIONS_CHECKS
this.Data->Table[i] = new TableValue()
{
Version = 1,
};
#endif
}
this.Data->Count = 0;
this.Data->Capacity = initialCapacity;
this.Data->Comparator = comparator;
}
internal unsafe void InsertAndBubbleDown(HeapNode node, int insertIndex)
{
while (true)
{
var indexL = insertIndex * 2 + 1;
var indexR = insertIndex * 2 + 2;
//If the left index is off the end, we are finished
if (indexL >= this.Data->Count)
{
break;
}
if (indexR >= this.Data->Count || this.Data->Comparator.Compare(this.Data->Heap[indexL].Item,
this.Data->Heap[indexR].Item) <= 0)
{
//left is smaller (or the only child)
var leftNode = this.Data->Heap[indexL];
if (this.Data->Comparator.Compare(node.Item, leftNode.Item) <= 0)
{
//Last is smaller or equal to left, we are done
break;
}
this.Data->Heap[insertIndex] = leftNode;
this.Data->Table[leftNode.TableIndex].HeapIndex = insertIndex;
insertIndex = indexL;
}
else
{
//right is smaller
var rightNode = this.Data->Heap[indexR];
if (this.Data->Comparator.Compare(node.Item, rightNode.Item) <= 0)
{
//Last is smaller than or equal to right, we are done
break;
}
this.Data->Heap[insertIndex] = rightNode;
this.Data->Table[rightNode.TableIndex].HeapIndex = insertIndex;
insertIndex = indexR;
}
}
this.Data->Heap[insertIndex] = node;
this.Data->Table[node.TableIndex].HeapIndex = insertIndex;
}
internal unsafe void InsertAndBubbleUp(HeapNode node, int insertIndex)
{
while (insertIndex != 0)
{
var parentIndex = (insertIndex - 1) / 2;
var parentNode = this.Data->Heap[parentIndex];
//If parent is actually less or equal to us, we are ok and can break out
if (this.Data->Comparator.Compare(parentNode.Item, node.Item) <= 0)
{
break;
}
//We need to swap parent down
this.Data->Heap[insertIndex] = parentNode;
//Update table to point to new heap index
this.Data->Table[parentNode.TableIndex].HeapIndex = insertIndex;
//Restart loop trying to insert at parent index
insertIndex = parentIndex;
}
this.Data->Heap[insertIndex] = node;
this.Data->Table[node.TableIndex].HeapIndex = insertIndex;
}
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
internal unsafe void IsValidIndexInternal(NativeHeapIndex index, ref bool result, ref int errorCode)
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckReadAndThrow(this.m_Safety);
if (index.StructureId != this.Id)
{
errorCode = VALIDATION_ERROR_WRONG_INSTANCE;
result = false;
return;
}
if (index.TableIndex >= this.Data->Capacity)
{
errorCode = VALIDATION_ERROR_INVALID;
result = false;
return;
}
var tableValue = this.Data->Table[index.TableIndex];
if (tableValue.Version != index.Version)
{
errorCode = VALIDATION_ERROR_REMOVED;
result = false;
return;
}
#endif
}
#endregion
}
internal unsafe class NativeHeapDebugView
where T : unmanaged
where U : unmanaged, IComparer
{
private NativeHeap _heap;
public NativeHeapDebugView(NativeHeap heap)
{
this._heap = heap;
}
public int Count => this._heap.Count;
public int Capacity => this._heap.Capacity;
public U Comparator => this._heap.Comparator;
public T[] Items
{
get
{
var items = new T[this._heap.Count];
for (var i = 0; i < items.Length; i++)
{
unsafe
{
items[i] = this._heap.Data->Heap[i].Item;
}
}
return items;
}
}
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct TableValue
{
public int HeapIndex;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
public int Version;
#endif
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct HeapData
where T : unmanaged
{
public int Count;
public int Capacity;
public unsafe HeapNode* Heap;
public unsafe TableValue* Table;
public U Comparator;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct HeapNode
where T : unmanaged
{
public T Item;
public int TableIndex;
}
}