2025-07-08 10:46:31 +00:00
using Unity.Collections ;
using UnityEngine ;
using Unity.Mathematics ;
using Unity.Jobs ;
using UnityEngine.Profiling ;
using Pathfinding.Collections ;
namespace Pathfinding {
using Pathfinding.Util ;
using Pathfinding.Jobs ;
using UnityEngine.Assertions ;
internal class GlobalNodeStorage {
readonly AstarPath astar ;
Unity . Jobs . JobHandle lastAllocationJob ;
/// <summary>
/// Holds the next node index which has not been used by any previous node.
/// See: <see cref="nodeIndexPools"/>
/// </summary>
public uint nextNodeIndex = 1 ;
/// <summary>
/// The number of nodes for which path node data has been reserved.
/// Will be at least as high as <see cref="nextNodeIndex"/>
/// </summary>
public uint reservedPathNodeData = 0 ;
/// <summary>Number of nodes that have been destroyed in total</summary>
public uint destroyedNodesVersion { get ; private set ; }
const int InitialTemporaryNodes = 256 ;
int temporaryNodeCount = InitialTemporaryNodes ;
/// <summary>
/// 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).
/// </summary>
readonly IndexedStack < uint > [ ] nodeIndexPools = new [ ] {
new IndexedStack < uint > ( ) ,
new IndexedStack < uint > ( ) ,
new IndexedStack < uint > ( ) ,
} ;
public PathfindingThreadData [ ] pathfindingThreadData = new PathfindingThreadData [ 0 ] ;
/// <summary>Maps from NodeIndex to node</summary>
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 < PathNode > pathNodes ;
#if UNITY_EDITOR
public UnsafeSpan < DebugPathNode > debugPathNodes ;
#endif
}
class IndexedStack < T > {
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 ] ;
}
/// <summary>Pop the last N elements and store them in the buffer. The items will be in insertion order.</summary>
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 < PathNode > ( Allocator . Persistent , ( int ) reservedPathNodeData + temporaryNodeCount ) ;
#if UNITY_EDITOR
pathfindingThreadData [ i ] . debugPathNodes = new UnsafeSpan < DebugPathNode > ( Allocator . Persistent , ( int ) reservedPathNodeData ) ;
pathfindingThreadData [ i ] . debugPathNodes . FillZeros ( ) ;
#endif
var pnodes = pathfindingThreadData [ i ] . pathNodes ;
pnodes . Fill ( PathNode . Default ) ;
}
}
}
/// <summary>
/// Grows temporary node storage for the given thread.
///
/// This can happen if a path traverses a lot of off-mesh links, or if it is a multi-target path with a lot of targets.
///
/// If enough nodes are created that we have a to grow the regular node storage, then the number of temporary nodes will grow to the same value on all threads.
/// </summary>
public void GrowTemporaryNodeStorage ( int threadID ) {
var threadData = pathfindingThreadData [ threadID ] ;
int currentTempNodeCount = threadData . pathNodes . Length - ( int ) reservedPathNodeData ;
Assert . IsTrue ( currentTempNodeCount > = 0 & & currentTempNodeCount < = temporaryNodeCount ) ;
// We don't want to grow this often, since we will have to re-allocate the storage for *ALL* nodes,
// not just the temporary nodes. So we use a high growth factor.
temporaryNodeCount = System . Math . Max ( temporaryNodeCount , currentTempNodeCount * 8 ) ;
var prevLength = threadData . pathNodes . Length ;
threadData . pathNodes = threadData . pathNodes . Reallocate ( Allocator . Persistent , ( int ) reservedPathNodeData + temporaryNodeCount ) ;
// Fill the new nodes with default values
threadData . pathNodes . Slice ( prevLength ) . Fill ( PathNode . Default ) ;
pathfindingThreadData [ threadID ] = threadData ;
}
/// <summary>
/// Initializes temporary path data for a node.
/// Warning: This method should not be called directly.
///
/// See: <see cref="AstarPath.InitializeNode"/>
/// </summary>
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 ) ;
}
}
/// <summary>
/// Reserves space for global node data.
///
/// Warning: Must be called only when a lock is held on this object.
/// </summary>
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 ) ;
}
/// <summary>
/// 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.
/// </summary>
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 < T > : IJob where T : GraphNode {
public T [ ] result ;
public int count ;
public GlobalNodeStorage nodeStorage ;
public uint variantsPerNode ;
public System . Func < T > 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 > ( T [ ] result , int count , System . Func < T > 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 < T > {
result = result ,
count = count ,
nodeStorage = this ,
variantsPerNode = variantsPerNode ,
createNode = createNode ,
} . ScheduleManaged ( lastAllocationJob ) ;
return lastAllocationJob ;
}
}
}