summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/Pathfinding/PathProcessor.cs
blob: b41a159fbc49cf0d7d660db2c7a4cc4500daef94 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine.Profiling;
using UnityEngine.Assertions;

namespace Pathfinding {
#if NETFX_CORE
	using Thread = Pathfinding.WindowsStore.Thread;
#else
	using Thread = System.Threading.Thread;
#endif

	public class PathProcessor {
		public event System.Action<Path> OnPathPreSearch;
		public event System.Action<Path> OnPathPostSearch;
		public event System.Action OnQueueUnblocked;

		internal BlockableChannel<Path> queue;
		readonly AstarPath astar;
		readonly PathReturnQueue returnQueue;

		PathHandler[] pathHandlers;

		/// <summary>References to each of the pathfinding threads</summary>
		Thread[] threads;
		bool multithreaded;

		/// <summary>
		/// When no multithreading is used, the IEnumerator is stored here.
		/// When no multithreading is used, a coroutine is used instead. It is not directly called with StartCoroutine
		/// but a separate function has just a while loop which increments the main IEnumerator.
		/// This is done so other functions can step the thread forward at any time, without having to wait for Unity to update it.
		/// See: <see cref="CalculatePaths"/>
		/// See: <see cref="CalculatePathsThreaded"/>
		/// </summary>
		IEnumerator threadCoroutine;
		BlockableChannel<Path>.Receiver coroutineReceiver;

		readonly List<int> locks = new List<int>();
		int nextLockID = 0;

		static readonly Unity.Profiling.ProfilerMarker MarkerCalculatePath = new Unity.Profiling.ProfilerMarker("Calculating Path");
		static readonly Unity.Profiling.ProfilerMarker MarkerPreparePath = new Unity.Profiling.ProfilerMarker("Prepare Path");

		/// <summary>
		/// Number of parallel pathfinders.
		/// Returns the number of concurrent processes which can calculate paths at once.
		/// When using multithreading, this will be the number of threads, if not using multithreading it is always 1 (since only 1 coroutine is used).
		/// See: threadInfos
		/// See: IsUsingMultithreading
		/// </summary>
		public int NumThreads {
			get {
				return pathHandlers.Length;
			}
		}

		/// <summary>Returns whether or not multithreading is used</summary>
		public bool IsUsingMultithreading {
			get {
				return multithreaded;
			}
		}

		internal PathProcessor (AstarPath astar, PathReturnQueue returnQueue, int processors, bool multithreaded) {
			this.astar = astar;
			this.returnQueue = returnQueue;

			// Set up path queue with the specified number of receivers
			queue = new BlockableChannel<Path>();
			threads = null;
			threadCoroutine = null;
			pathHandlers = new PathHandler[0];
		}

		/// <summary>
		/// Changes the number of threads used for pathfinding.
		///
		/// If multithreading is disabled, processors must be equal to 1.
		/// </summary>
		public void SetThreadCount (int processors, bool multithreaded) {
			if (threads != null || threadCoroutine != null || pathHandlers.Length > 0) throw new System.Exception("Call StopThreads before setting the thread count");

			if (processors < 1) {
				throw new System.ArgumentOutOfRangeException("processors");
			}

			if (!multithreaded && processors != 1) {
				throw new System.Exception("Only a single non-multithreaded processor is allowed");
			}

			pathHandlers = new PathHandler[processors];
			this.multithreaded = multithreaded;

			for (int i = 0; i < processors; i++) {
				pathHandlers[i] = new PathHandler(astar.nodeStorage, i, processors);
			}
			astar.nodeStorage.SetThreadCount(processors);
			StartThreads();
		}

		void StartThreads () {
			if (threads != null || threadCoroutine != null) throw new System.Exception("Call StopThreads before starting threads");

			queue.Reopen();

			// Ensure the node storage is up to date.
			// Per-thread data may have been cleared if the AstarPath object
			// was disabled.
			astar.nodeStorage.SetThreadCount(pathHandlers.Length);

			if (multithreaded) {
				threads = new Thread[this.pathHandlers.Length];

				// Start lots of threads
				for (int i = 0; i < this.pathHandlers.Length; i++) {
					var pathHandler = pathHandlers[i];
					var receiver = queue.AddReceiver();
					threads[i] = new Thread(() => CalculatePathsThreaded(pathHandler, receiver));
#if !UNITY_SWITCH || UNITY_EDITOR
					// Note: Setting the thread name seems to crash when deploying for Switch: https://forum.arongranberg.com/t/path-processor-crashing-nintendo-switch-build/6584
					threads[i].Name = "Pathfinding Thread " + i;
#endif
					threads[i].IsBackground = true;
					threads[i].Start();
				}
			} else {
				coroutineReceiver = queue.AddReceiver();
				// Start coroutine if not using multithreading
				threadCoroutine = CalculatePaths(pathHandlers[0]);
			}
		}

		/// <summary>Prevents pathfinding from running while held</summary>
		public struct GraphUpdateLock {
			PathProcessor pathProcessor;
			int id;

			public GraphUpdateLock (PathProcessor pathProcessor, bool block) {
				this.pathProcessor = pathProcessor;
				Profiler.BeginSample("Pausing pathfinding");
				id = pathProcessor.Lock(block);
				Profiler.EndSample();
			}

			/// <summary>
			/// True while this lock is preventing the pathfinding threads from processing more paths.
			/// Note that the pathfinding threads may not be paused yet (if this lock was obtained using PausePathfinding(false)).
			/// </summary>
			public bool Held => pathProcessor != null && pathProcessor.locks.Contains(id);

			/// <summary>Allow pathfinding to start running again if no other locks are still held</summary>
			public void Release() => pathProcessor.Unlock(id);
		}

		int Lock (bool block) {
			queue.isBlocked = true;
			if (block) {
				while (!queue.allReceiversBlocked) {
					Assert.IsTrue(threads != null || threadCoroutine != null);
					if (IsUsingMultithreading) {
						Thread.Sleep(1);
					} else {
						TickNonMultithreaded();
					}
				}
			}

			nextLockID++;
			locks.Add(nextLockID);
			return nextLockID;
		}

		void Unlock (int id) {
			if (!locks.Remove(id)) {
				throw new System.ArgumentException("This lock has already been released");
			}

			// Check if there are no remaining active locks
			if (locks.Count == 0) {
				if (OnQueueUnblocked != null) OnQueueUnblocked();

				queue.isBlocked = false;
			}
		}

		/// <summary>
		/// Prevents pathfinding threads from starting to calculate any new paths.
		///
		/// Returns: A lock object. You need to call Unlock on that object to allow pathfinding to resume.
		///
		/// Note: In most cases this should not be called from user code.
		/// </summary>
		/// <param name="block">If true, this call will block until all pathfinding threads are paused.
		/// otherwise the threads will be paused as soon as they are done with what they are currently doing.</param>
		public GraphUpdateLock PausePathfinding (bool block) {
			return new GraphUpdateLock(this, block);
		}

		/// <summary>
		/// Does pathfinding calculations when not using multithreading.
		///
		/// This method should be called once per frame if <see cref="IsUsingMultithreading"/> is true.
		/// </summary>
		public void TickNonMultithreaded () {
			// Process paths
			if (threadCoroutine == null) throw new System.InvalidOperationException("Cannot tick non-multithreaded pathfinding when no coroutine has been started");

			try {
				if (!threadCoroutine.MoveNext()) {
					threadCoroutine = null;
					coroutineReceiver.Close();
				}
			} catch (System.Exception e) {
				Debug.LogException(e);
				Debug.LogError("Unhandled exception during pathfinding. Terminating.");
				queue.Close();

				// This will kill pathfinding
				threadCoroutine = null;
				coroutineReceiver.Close();
			}
		}

		/// <summary>
		/// Calls 'Join' on each of the threads to block until they have completed.
		///
		/// This will also clean up any unmanaged memory used by the threads.
		/// </summary>
		public void StopThreads () {
			// Don't accept any more path calls to this AstarPath instance.
			// This will cause all pathfinding threads to exit (if any exist)
			queue.Close();

			if (threads != null) {
				for (int i = 0; i < threads.Length; i++) {
					if (!threads[i].Join(200)) {
						Debug.LogError("Could not terminate pathfinding thread["+i+"] in 200ms, trying Thread.Abort");
						threads[i].Abort();
					}
				}
				threads = null;
			}
			if (threadCoroutine != null) {
				Assert.IsTrue(queue.numReceivers > 0);
				while (queue.numReceivers > 0) TickNonMultithreaded();
				Assert.IsNull(threadCoroutine);
			}

			Assert.AreEqual(queue.numReceivers, 0, "Not all receivers were blocked and terminated when stopping threads");

			// Dispose unmanaged data
			for (int i = 0; i < pathHandlers.Length; i++) {
				pathHandlers[i].Dispose();
			}
			pathHandlers = new PathHandler[0];
		}

		/// <summary>
		/// Cleans up all native memory managed by this instance.
		///
		/// You may use this instance again by calling SetThreadCount.
		/// </summary>
		public void Dispose () {
			StopThreads();
		}

		/// <summary>
		/// Main pathfinding method (multithreaded).
		/// This method will calculate the paths in the pathfinding queue when multithreading is enabled.
		///
		/// See: CalculatePaths
		/// See: <see cref="AstarPath.StartPath"/>
		/// </summary>
		void CalculatePathsThreaded (PathHandler pathHandler, BlockableChannel<Path>.Receiver receiver) {
			UnityEngine.Profiling.Profiler.BeginThreadProfiling("Pathfinding", "Pathfinding thread #" + (pathHandler.threadID+1));

			try {
				// Max number of ticks we are allowed to continue working in one run.
				// One tick is 1/10000 of a millisecond.
				// We need to check once in a while if the thread should be stopped.
				long maxTicks = (long)(10*10000);
				long targetTick = System.DateTime.UtcNow.Ticks + maxTicks;
				while (true) {
					// The path we are currently calculating
					if (receiver.Receive(out var path) == BlockableChannel<Path>.PopState.Closed) {
						if (astar.logPathResults == PathLog.Heavy)
							Debug.LogWarning("Shutting down pathfinding thread #" + pathHandler.threadID);
						receiver.Close();
						return;
					}
					MarkerCalculatePath.Begin();
					// Access the internal implementation methods
					IPathInternals ipath = (IPathInternals)path;


					MarkerPreparePath.Begin();
					ipath.PrepareBase(pathHandler);

					// Now processing the path
					// Will advance to Processing
					ipath.AdvanceState(PathState.Processing);

					// Call some callbacks
					if (OnPathPreSearch != null) {
						OnPathPreSearch(path);
					}

					// Tick for when the path started, used for calculating how long time the calculation took
					long startTicks = System.DateTime.UtcNow.Ticks;

					// Prepare the path
					ipath.Prepare();
					MarkerPreparePath.End();


					if (path.CompleteState == PathCompleteState.NotCalculated) {
						// For visualization purposes, we set the last computed path to p, so we can view debug info on it in the editor (scene view).
						astar.debugPathData = ipath.PathHandler;
						astar.debugPathID = path.pathID;

						// Loop while the path has not been fully calculated
						while (path.CompleteState == PathCompleteState.NotCalculated) {
							// Do some work on the path calculation.
							// The function will return when it has taken too much time
							// or when it has finished calculation
							ipath.CalculateStep(targetTick);

							targetTick = System.DateTime.UtcNow.Ticks + maxTicks;

							// Cancel function (and thus the thread) if no more paths should be accepted.
							// This is done when the A* object is about to be destroyed
							// The path is returned and then this function will be terminated
							if (queue.isClosed) {
								path.FailWithError("AstarPath object destroyed");
							}
						}

						path.duration = (System.DateTime.UtcNow.Ticks - startTicks)*0.0001F;

#if ProfileAstar
						System.Threading.Interlocked.Increment(ref AstarPath.PathsCompleted);
						System.Threading.Interlocked.Add(ref AstarPath.TotalSearchTime, System.DateTime.UtcNow.Ticks - startTicks);
#endif
					}

					// Cleans up node tagging and other things
					ipath.Cleanup();
					pathHandler.heap.Clear(pathHandler.pathNodes);


					if (path.immediateCallback != null) path.immediateCallback(path);

					if (OnPathPostSearch != null) {
						OnPathPostSearch(path);
					}

					// Push the path onto the return stack
					// It will be detected by the main Unity thread and returned as fast as possible (the next late update hopefully)
					returnQueue.Enqueue(path);

					// Will advance to ReturnQueue
					ipath.AdvanceState(PathState.ReturnQueue);

					MarkerCalculatePath.End();
				}
			} catch (System.Exception e) {
#if !NETFX_CORE
				if (e is ThreadAbortException) {
					if (astar.logPathResults == PathLog.Heavy)
						Debug.LogWarning("Shutting down pathfinding thread #" + pathHandler.threadID);
					receiver.Close();
					return;
				}
#endif

				Debug.LogException(e);
				Debug.LogError("Unhandled exception during pathfinding. Terminating.");
				// Unhandled exception, kill pathfinding
				queue.Close();
			} finally {
				UnityEngine.Profiling.Profiler.EndThreadProfiling();
			}

			Debug.LogError("Error : This part should never be reached.");
			receiver.Close();
		}

		/// <summary>
		/// Main pathfinding method.
		/// This method will calculate the paths in the pathfinding queue.
		///
		/// See: CalculatePathsThreaded
		/// See: StartPath
		/// </summary>
		IEnumerator CalculatePaths (PathHandler pathHandler) {
			// Max number of ticks before yielding/sleeping
			long maxTicks = (long)(astar.maxFrameTime*10000);
			long targetTick = System.DateTime.UtcNow.Ticks + maxTicks;

			while (true) {
				// The path we are currently calculating
				Path p = null;

				// Try to get the next path to be calculated
				bool blockedBefore = false;
				while (p == null) {
					switch (coroutineReceiver.ReceiveNoBlock(blockedBefore, out p)) {
					case BlockableChannel<Path>.PopState.Ok:
						break;
					case BlockableChannel<Path>.PopState.Wait:
						blockedBefore = true;
						yield return null;
						break;
					case BlockableChannel<Path>.PopState.Closed:
						yield break;
					}
				}

				IPathInternals ip = (IPathInternals)p;

				// Max number of ticks we are allowed to use for pathfinding in one frame
				// One tick is 1/10000 of a millisecond
				maxTicks = (long)(astar.maxFrameTime*10000);

				ip.PrepareBase(pathHandler);

				// Now processing the path
				// Will advance to Processing
				ip.AdvanceState(PathState.Processing);

				// Call some callbacks
				// It needs to be stored in a local variable to avoid race conditions
				var tmpOnPathPreSearch = OnPathPreSearch;
				if (tmpOnPathPreSearch != null) tmpOnPathPreSearch(p);

				// Tick for when the path started, used for calculating how long time the calculation took
				long startTicks = System.DateTime.UtcNow.Ticks;
				long totalTicks = 0;

				ip.Prepare();

				// Check if the Prepare call caused the path to complete
				// If this happens the path usually failed
				if (p.CompleteState == PathCompleteState.NotCalculated) {
					// For debug uses, we set the last computed path to p, so we can view debug info on it in the editor (scene view).
					astar.debugPathData = ip.PathHandler;
					astar.debugPathID = p.pathID;

					// The error can turn up in the Init function
					while (p.CompleteState == PathCompleteState.NotCalculated) {
						// Run some pathfinding calculations.
						// The function will return when it has taken too much time
						// or when it has finished calculating the path.
						ip.CalculateStep(targetTick);


						// If the path has finished calculating, we can break here directly instead of sleeping
						// Improves latency
						if (p.CompleteState != PathCompleteState.NotCalculated) break;

						totalTicks += System.DateTime.UtcNow.Ticks-startTicks;
						// Yield/sleep so other threads can work

						yield return null;

						startTicks = System.DateTime.UtcNow.Ticks;

						// Cancel function (and thus the thread) if no more paths should be accepted.
						// This is done when the A* object is about to be destroyed
						// The path is returned and then this function will be terminated (see similar IF statement higher up in the function)
						if (queue.isClosed) {
							p.FailWithError("AstarPath object destroyed");
						}

						targetTick = System.DateTime.UtcNow.Ticks + maxTicks;
					}

					totalTicks += System.DateTime.UtcNow.Ticks-startTicks;
					p.duration = totalTicks*0.0001F;

#if ProfileAstar
					System.Threading.Interlocked.Increment(ref AstarPath.PathsCompleted);
#endif
				}

				// Cleans up node tagging and other things
				ip.Cleanup();
				pathHandler.heap.Clear(pathHandler.pathNodes);


				// Call the immediate callback
				// It needs to be stored in a local variable to avoid race conditions
				var tmpImmediateCallback = p.immediateCallback;
				if (tmpImmediateCallback != null) tmpImmediateCallback(p);


				// It needs to be stored in a local variable to avoid race conditions
				var tmpOnPathPostSearch = OnPathPostSearch;
				if (tmpOnPathPostSearch != null) tmpOnPathPostSearch(p);


				// Push the path onto the return stack
				// It will be detected by the main Unity thread and returned as fast as possible (the next late update)
				returnQueue.Enqueue(p);

				ip.AdvanceState(PathState.ReturnQueue);


				// Wait a bit if we have calculated a lot of paths
				if (System.DateTime.UtcNow.Ticks > targetTick) {
					yield return null;
					targetTick = System.DateTime.UtcNow.Ticks + maxTicks;
				}
			}
		}
	}
}