// #define VALIDATE_BINARY_HEAP
#pragma warning disable 162
#pragma warning disable 429
using Unity.Mathematics;
using Unity.Collections;
using Unity.Burst;
using Unity.Burst.CompilerServices;
namespace Pathfinding {
using Pathfinding.Util;
///
/// Binary heap implementation.
/// Binary heaps are really fast for ordering nodes in a way that
/// makes it possible to get the node with the lowest F score.
/// Also known as a priority queue.
///
/// This has actually been rewritten as a 4-ary heap
/// for performance, but it's the same principle.
///
/// See: http://en.wikipedia.org/wiki/Binary_heap
/// See: https://en.wikipedia.org/wiki/D-ary_heap
///
[BurstCompile]
public struct BinaryHeap {
/// Number of items in the tree
public int numberOfItems;
/// The tree will grow by at least this factor every time it is expanded
public const float GrowthFactor = 2;
///
/// Number of children of each node in the tree.
/// Different values have been tested and 4 has been empirically found to perform the best.
/// See: https://en.wikipedia.org/wiki/D-ary_heap
///
const int D = 4;
///
/// Sort nodes by G score if there is a tie when comparing the F score.
/// Disabling this will improve pathfinding performance with around 2.5%
/// but may break ties between paths that have the same length in a less
/// desirable manner (only relevant for grid graphs).
///
const bool SortGScores = true;
public const ushort NotInHeap = 0xFFFF;
/// Internal backing array for the heap
private UnsafeSpan heap;
/// True if the heap does not contain any elements
public bool isEmpty => numberOfItems <= 0;
/// Item in the heap
private struct HeapNode {
public uint pathNodeIndex;
/// Bitpacked F and G scores
public ulong sortKey;
public HeapNode (uint pathNodeIndex, uint g, uint f) {
this.pathNodeIndex = pathNodeIndex;
this.sortKey = ((ulong)f << 32) | (ulong)g;
}
public uint F {
get => (uint)(sortKey >> 32);
set => sortKey = (sortKey & 0xFFFFFFFFUL) | ((ulong)value << 32);
}
public uint G => (uint)sortKey;
}
///
/// Rounds up v so that it has remainder 1 when divided by D.
/// I.e it is of the form n*D + 1 where n is any non-negative integer.
///
static int RoundUpToNextMultipleMod1 (int v) {
// I have a feeling there is a nicer way to do this
return v + (4 - ((v-1) % D)) % D;
}
/// Create a new heap with the specified initial capacity
public BinaryHeap (int capacity) {
// Make sure the size has remainder 1 when divided by D
// This allows us to always guarantee that indices used in the Remove method
// will never throw out of bounds exceptions
capacity = RoundUpToNextMultipleMod1(capacity);
heap = new UnsafeSpan(Unity.Collections.Allocator.Persistent, capacity);
numberOfItems = 0;
}
public void Dispose () {
unsafe {
AllocatorManager.Free(Allocator.Persistent, heap.ptr, heap.Length);
}
}
/// Removes all elements from the heap
public void Clear (UnsafeSpan pathNodes) {
// Clear all heap indices, and references to the heap nodes
for (int i = 0; i < numberOfItems; i++) {
pathNodes[heap[i].pathNodeIndex].heapIndex = NotInHeap;
}
numberOfItems = 0;
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public uint GetPathNodeIndex(int heapIndex) => heap[heapIndex].pathNodeIndex;
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public uint GetG(int heapIndex) => heap[heapIndex].G;
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public uint GetF(int heapIndex) => heap[heapIndex].F;
public void SetH (int heapIndex, uint h) {
heap[heapIndex].F = heap[heapIndex].G + h;
}
/// Expands to a larger backing array when the current one is too small
static void Expand (ref UnsafeSpan heap) {
// 65533 == 1 mod 4 and slightly smaller than 1<<16 = 65536
int newSize = math.max(heap.Length+4, math.min(65533, (int)math.round(heap.Length*GrowthFactor)));
// Make sure the size has remainder 1 when divided by D
// This allows us to always guarantee that indices used in the Remove method
// will never throw out of bounds exceptions
newSize = RoundUpToNextMultipleMod1(newSize);
// Check if the heap is really large
// Also note that heaps larger than this are not supported
// since PathNode.heapIndex is a ushort and can only store
// values up to 65535 (NotInHeap = 65535 is reserved however)
if (newSize > (1<<16) - 2) {
throw new System.Exception("Binary Heap Size really large (>65534). A heap size this large is probably the cause of pathfinding running in an infinite loop. ");
}
var newHeap = new UnsafeSpan(Unity.Collections.Allocator.Persistent, newSize);
newHeap.CopyFrom(heap);
unsafe {
AllocatorManager.Free(Allocator.Persistent, heap.ptr, heap.Length);
}
#if ASTARDEBUG
UnityEngine.Debug.Log("Resizing binary heap to "+newSize);
#endif
heap = newHeap;
}
/// Adds a node to the heap
public void Add (UnsafeSpan nodes, uint pathNodeIndex, uint g, uint f) {
Add(ref this, ref nodes, pathNodeIndex, g, f);
}
[BurstCompile]
static void Add (ref BinaryHeap binaryHeap, ref UnsafeSpan nodes, uint pathNodeIndex, uint g, uint f) {
ref var numberOfItems = ref binaryHeap.numberOfItems;
ref var heap = ref binaryHeap.heap;
// Check if node is already in the heap
ref var node = ref nodes[pathNodeIndex];
if (node.heapIndex != NotInHeap) {
var activeNode = new HeapNode(pathNodeIndex, g, f);
UnityEngine.Assertions.Assert.AreEqual(activeNode.pathNodeIndex, pathNodeIndex);
DecreaseKey(heap, nodes, activeNode, node.heapIndex);
Validate(ref nodes, ref binaryHeap);
} else {
if (numberOfItems == heap.Length) {
Expand(ref heap);
}
Validate(ref nodes, ref binaryHeap);
DecreaseKey(heap, nodes, new HeapNode(pathNodeIndex, g, f), (ushort)numberOfItems);
numberOfItems++;
Validate(ref nodes, ref binaryHeap);
}
}
static void DecreaseKey (UnsafeSpan heap, UnsafeSpan nodes, HeapNode node, ushort index) {
// This is where 'obj' is in the binary heap logically speaking
// (for performance reasons we don't actually store it there until
// we know the final index, that's just a waste of CPU cycles)
uint bubbleIndex = index;
while (bubbleIndex != 0) {
// Parent node of the bubble node
uint parentIndex = (bubbleIndex-1) / D;
Hint.Assume(parentIndex < heap.length);
Hint.Assume(bubbleIndex < heap.length);
if (node.sortKey < heap[parentIndex].sortKey) {
// Swap the bubble node and parent node
// (we don't really need to store the bubble node until we know the final index though
// so we do that after the loop instead)
heap[bubbleIndex] = heap[parentIndex];
nodes[heap[bubbleIndex].pathNodeIndex].heapIndex = (ushort)bubbleIndex;
bubbleIndex = parentIndex;
} else {
break;
}
}
Hint.Assume(bubbleIndex < heap.length);
heap[bubbleIndex] = node;
nodes[node.pathNodeIndex].heapIndex = (ushort)bubbleIndex;
}
/// Returns the node with the lowest F score from the heap
public uint Remove (UnsafeSpan nodes, out uint g, out uint f) {
return Remove(ref nodes, ref this, out g, out f);
}
[BurstCompile]
static uint Remove (ref UnsafeSpan nodes, ref BinaryHeap binaryHeap, [NoAlias] out uint removedG, [NoAlias] out uint removedF) {
ref var numberOfItems = ref binaryHeap.numberOfItems;
var heap = binaryHeap.heap;
if (numberOfItems == 0) {
throw new System.InvalidOperationException("Removing item from empty heap");
}
// This is the smallest item in the heap.
// Mark it as removed from the heap.
Hint.Assume(0UL < heap.length);
uint returnIndex = heap[0].pathNodeIndex;
nodes[returnIndex].heapIndex = NotInHeap;
removedG = heap[0].G;
removedF = heap[0].F;
numberOfItems--;
if (numberOfItems == 0) {
return returnIndex;
}
// Last item in the heap array
Hint.Assume((uint)numberOfItems < heap.length);
var swapItem = heap[numberOfItems];
uint swapIndex = 0;
ulong comparisonKey = swapItem.sortKey;
// Trickle upwards
while (true) {
var parent = swapIndex;
uint pd = parent * D + 1;
// If this holds, then the indices used
// below are guaranteed to not throw an index out of bounds
// exception since we choose the size of the array in that way
if (pd < numberOfItems) {
// Find the child node with the smallest F score, or if equal, the smallest G score.
// The sorting key is the tuple (F,G).
// However, to be able to easily get the smallest index, we steal the lowest 2 bits of G
// and use it for the child index (0..3) instead.
// This means that tie-breaking will not be perfect, but in all practical cases it will
// yield exactly the same result since G scores typically differ by more than 4 anyway.
Hint.Assume(pd+0 < heap.length);
ulong l0 = (heap[pd+0].sortKey & ~0x3UL) | 0;
Hint.Assume(pd+1 < heap.length);
ulong l1 = (heap[pd+1].sortKey & ~0x3UL) | 1;
Hint.Assume(pd+2 < heap.length);
ulong l2 = (heap[pd+2].sortKey & ~0x3UL) | 2;
Hint.Assume(pd+3 < heap.length);
ulong l3 = (heap[pd+3].sortKey & ~0x3UL) | 3;
ulong smallest = l0;
// Not all children may exist, so we need to check that the index is valid
if (pd+1 < numberOfItems) smallest = math.min(smallest, l1);
if (pd+2 < numberOfItems) smallest = math.min(smallest, l2);
if (pd+3 < numberOfItems) smallest = math.min(smallest, l3);
if (smallest < comparisonKey) {
swapIndex = pd + (uint)(smallest & 0x3UL);
// One if the parent's children are smaller or equal, swap them
// (actually we are just pretenting we swapped them, we hold the swapItem
// in local variable and only assign it once we know the final index)
Hint.Assume(parent < heap.length);
Hint.Assume(swapIndex < heap.length);
heap[parent] = heap[swapIndex];
Hint.Assume(swapIndex < heap.length);
nodes[heap[swapIndex].pathNodeIndex].heapIndex = (ushort)parent;
} else {
break;
}
} else {
break;
}
}
// Assign element to the final position
Hint.Assume(swapIndex < heap.length);
heap[swapIndex] = swapItem;
nodes[swapItem.pathNodeIndex].heapIndex = (ushort)swapIndex;
// For debugging
Validate(ref nodes, ref binaryHeap);
return returnIndex;
}
[System.Diagnostics.Conditional("VALIDATE_BINARY_HEAP")]
static void Validate (ref UnsafeSpan nodes, ref BinaryHeap binaryHeap) {
for (int i = 1; i < binaryHeap.numberOfItems; i++) {
int parentIndex = (i-1)/D;
if (binaryHeap.heap[parentIndex].F > binaryHeap.heap[i].F) {
throw new System.Exception("Invalid state at " + i + ":" + parentIndex + " ( " + binaryHeap.heap[parentIndex].F + " > " + binaryHeap.heap[i].F + " ) ");
}
if (binaryHeap.heap[parentIndex].sortKey > binaryHeap.heap[i].sortKey) {
throw new System.Exception("Invalid state at " + i + ":" + parentIndex + " ( " + binaryHeap.heap[parentIndex].F + " > " + binaryHeap.heap[i].F + " ) ");
}
if (nodes[binaryHeap.heap[i].pathNodeIndex].heapIndex != i) {
throw new System.Exception("Invalid heap index");
}
}
}
///
/// Rebuilds the heap by trickeling down all items.
/// Usually called after the hTarget on a path has been changed
///
public void Rebuild (UnsafeSpan nodes) {
#if ASTARDEBUG
int changes = 0;
#endif
for (int i = 2; i < numberOfItems; i++) {
int bubbleIndex = i;
var node = heap[i];
uint nodeF = node.F;
while (bubbleIndex != 1) {
int parentIndex = bubbleIndex / D;
if (nodeF < heap[parentIndex].F) {
heap[bubbleIndex] = heap[parentIndex];
nodes[heap[bubbleIndex].pathNodeIndex].heapIndex = (ushort)bubbleIndex;
heap[parentIndex] = node;
nodes[heap[parentIndex].pathNodeIndex].heapIndex = (ushort)parentIndex;
bubbleIndex = parentIndex;
#if ASTARDEBUG
changes++;
#endif
} else {
break;
}
}
}
#if ASTARDEBUG
UnityEngine.Debug.Log("+++ Rebuilt Heap - "+changes+" changes +++");
#endif
}
}
}