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; } } }