using Unity.Collections;
using UnityEngine;
using Unity.Mathematics;
using Unity.Jobs;
using UnityEngine.Profiling;
namespace Pathfinding {
using Pathfinding.Util;
using Pathfinding.Jobs;
internal class GlobalNodeStorage {
readonly AstarPath astar;
Unity.Jobs.JobHandle lastAllocationJob;
///
/// Holds the next node index which has not been used by any previous node.
/// See:
///
public uint nextNodeIndex = 1;
///
/// The number of nodes for which path node data has been reserved.
/// Will be at least as high as
///
uint reservedPathNodeData = 0;
/// Number of nodes that have been destroyed in total
public uint destroyedNodesVersion { get; private set; }
#if ASTAR_MORE_MULTI_TARGET_PATH_TARGETS
public const int MaxTemporaryNodes = 4096;
#else
public const int MaxTemporaryNodes = 256;
#endif
///
/// Holds indices for nodes that have been destroyed.
/// To avoid trashing a lot of memory structures when nodes are
/// frequently deleted and created, node indices are reused.
///
/// There's one pool for each possible number of node variants (1, 2 and 3).
///
readonly IndexedStack[] nodeIndexPools = new [] {
new IndexedStack(),
new IndexedStack(),
new IndexedStack(),
};
public PathfindingThreadData[] pathfindingThreadData = new PathfindingThreadData[0];
/// Maps from NodeIndex to node
GraphNode[] nodes = new GraphNode[0];
public GlobalNodeStorage (AstarPath astar) {
this.astar = astar;
}
public GraphNode GetNode(uint nodeIndex) => nodes[nodeIndex];
#if UNITY_EDITOR
public struct DebugPathNode {
public uint g;
public uint h;
public uint parentIndex;
public ushort pathID;
public byte fractionAlongEdge;
}
#endif
public struct PathfindingThreadData {
public UnsafeSpan pathNodes;
#if UNITY_EDITOR
public UnsafeSpan debugPathNodes;
#endif
}
class IndexedStack {
T[] buffer = new T[4];
public int Count { get; private set; }
public void Push (T v) {
if (Count == buffer.Length) {
Util.Memory.Realloc(ref buffer, buffer.Length * 2);
}
buffer[Count] = v;
Count++;
}
public void Clear () {
Count = 0;
}
public T Pop () {
Count--;
return buffer[Count];
}
/// Pop the last N elements and store them in the buffer. The items will be in insertion order.
public void PopMany (T[] resultBuffer, int popCount) {
if (popCount > Count) throw new System.IndexOutOfRangeException();
System.Array.Copy(buffer, Count - popCount, resultBuffer, 0, popCount);
Count -= popCount;
}
}
void DisposeThreadData () {
if (pathfindingThreadData.Length > 0) {
for (int i = 0; i < pathfindingThreadData.Length; i++) {
unsafe {
pathfindingThreadData[i].pathNodes.Free(Allocator.Persistent);
#if UNITY_EDITOR
pathfindingThreadData[i].debugPathNodes.Free(Allocator.Persistent);
#endif
}
}
pathfindingThreadData = new PathfindingThreadData[0];
}
}
public void SetThreadCount (int threadCount) {
if (pathfindingThreadData.Length != threadCount) {
DisposeThreadData();
pathfindingThreadData = new PathfindingThreadData[threadCount];
for (int i = 0; i < pathfindingThreadData.Length; i++) {
// Allocate per-thread data.
// We allocate using UnsafeSpans because this code may run inside jobs, and Unity does not allow us to allocate NativeArray memory
// using the persistent allocator inside jobs.
pathfindingThreadData[i].pathNodes = new UnsafeSpan(Allocator.Persistent, (int)reservedPathNodeData + MaxTemporaryNodes);
#if UNITY_EDITOR
pathfindingThreadData[i].debugPathNodes = new UnsafeSpan(Allocator.Persistent, (int)reservedPathNodeData);
pathfindingThreadData[i].debugPathNodes.FillZeros();
#endif
var pnodes = pathfindingThreadData[i].pathNodes;
pnodes.Fill(PathNode.Default);
}
}
}
///
/// Initializes temporary path data for a node.
/// Warning: This method should not be called directly.
///
/// See:
///
public void InitializeNode (GraphNode node) {
var variants = node.PathNodeVariants;
// Graphs may initialize nodes from different threads,
// so we need to lock.
// Luckily, uncontested locks are really really cheap in C#
lock (this) {
if (nodeIndexPools[variants-1].Count > 0) {
node.NodeIndex = nodeIndexPools[variants-1].Pop();
} else {
// Highest node index in the new list of nodes
node.NodeIndex = nextNodeIndex;
nextNodeIndex += (uint)variants;
ReserveNodeIndices(nextNodeIndex);
}
for (int i = 0; i < variants; i++) {
nodes[node.NodeIndex + i] = node;
}
astar.hierarchicalGraph.OnCreatedNode(node);
}
}
///
/// Reserves space for global node data.
///
/// Warning: Must be called only when a lock is held on this object.
///
void ReserveNodeIndices (uint nextNodeIndex) {
if (nextNodeIndex <= reservedPathNodeData) return;
reservedPathNodeData = math.ceilpow2(nextNodeIndex);
// Allocate more internal pathfinding data for the new nodes
astar.hierarchicalGraph.ReserveNodeIndices(reservedPathNodeData);
var threadCount = pathfindingThreadData.Length;
DisposeThreadData();
SetThreadCount(threadCount);
Memory.Realloc(ref nodes, (int)reservedPathNodeData);
}
///
/// Destroyes the given node.
/// This is to be called after the node has been disconnected from the graph so that it cannot be reached from any other nodes.
/// It should only be called during graph updates, that is when the pathfinding threads are either not running or paused.
///
/// Warning: This method should not be called by user code. It is used internally by the system.
///
public void DestroyNode (GraphNode node) {
var nodeIndex = node.NodeIndex;
if (nodeIndex == GraphNode.DestroyedNodeIndex) return;
destroyedNodesVersion++;
int variants = node.PathNodeVariants;
nodeIndexPools[variants - 1].Push(nodeIndex);
for (int i = 0; i < variants; i++) {
nodes[nodeIndex + i] = null;
}
for (int t = 0; t < pathfindingThreadData.Length; t++) {
var threadData = pathfindingThreadData[t];
for (uint i = 0; i < variants; i++) {
// This is not required for pathfinding, but not clearing it may confuse gizmo drawing for a fraction of a second.
// Especially when 'Show Search Tree' is enabled
threadData.pathNodes[nodeIndex + i].pathID = 0;
#if UNITY_EDITOR
threadData.debugPathNodes[nodeIndex + i].pathID = 0;
#endif
}
}
astar.hierarchicalGraph.OnDestroyedNode(node);
}
public void OnDisable () {
lastAllocationJob.Complete();
nextNodeIndex = 1;
reservedPathNodeData = 0;
for (int i = 0; i < nodeIndexPools.Length; i++) nodeIndexPools[i].Clear();
nodes = new GraphNode[0];
DisposeThreadData();
}
struct JobAllocateNodes : IJob where T : GraphNode {
public T[] result;
public int count;
public GlobalNodeStorage nodeStorage;
public uint variantsPerNode;
public System.Func createNode;
public bool allowBoundsChecks => false;
public void Execute () {
Profiler.BeginSample("Allocating nodes");
var hierarchicalGraph = nodeStorage.astar.hierarchicalGraph;
lock (nodeStorage) {
var pool = nodeStorage.nodeIndexPools[variantsPerNode-1];
uint nextNodeIndex = nodeStorage.nextNodeIndex;
// Allocate the actual nodes
for (uint i = 0; i < count; i++) {
var node = result[i] = createNode();
// Get a new node index. Re-use one from a previously destroyed node if possible
if (pool.Count > 0) {
node.NodeIndex = pool.Pop();
} else {
node.NodeIndex = nextNodeIndex;
nextNodeIndex += variantsPerNode;
}
}
// Allocate more internal pathfinding data for the new nodes
nodeStorage.ReserveNodeIndices(nextNodeIndex);
// Mark the node indices as used
nodeStorage.nextNodeIndex = nextNodeIndex;
for (int i = 0; i < count; i++) {
var node = result[i];
hierarchicalGraph.AddDirtyNode(node);
nodeStorage.nodes[node.NodeIndex] = node;
}
}
Profiler.EndSample();
}
}
public Unity.Jobs.JobHandle AllocateNodesJob(T[] result, int count, System.Func createNode, uint variantsPerNode) where T : GraphNode {
// Get all node indices that we are going to recycle and store them in a new buffer.
// It's best to store them in a new buffer to avoid multithreading issues.
UnityEngine.Assertions.Assert.IsTrue(variantsPerNode > 0 && variantsPerNode <= 3);
// It may be tempting to use a parallel job for this
// but it seems like allocation (new) in C# uses some kind of locking.
// Therefore it is not faster (it may even be slower) to try to allocate the nodes in multiple threads in parallel.
// The job will use locking internally for safety, but it's still nice to set appropriate dependencies, to avoid lots of worker threads
// just stalling because they are waiting for a lock, in case this method is called multiple times in parallel.
lastAllocationJob = new JobAllocateNodes {
result = result,
count = count,
nodeStorage = this,
variantsPerNode = variantsPerNode,
createNode = createNode,
}.ScheduleManaged(lastAllocationJob);
return lastAllocationJob;
}
}
}