using System.Collections.Generic; using System.Threading; using UnityEngine; #if UNITY_5_5_OR_NEWER using UnityEngine.Profiling; #endif namespace Pathfinding { using UnityEngine.Assertions; #if NETFX_CORE using Thread = Pathfinding.WindowsStore.Thread; #else using Thread = System.Threading.Thread; #endif class GraphUpdateProcessor { public event System.Action OnGraphsUpdated; /// Holds graphs that can be updated readonly AstarPath astar; #if !UNITY_WEBGL /// /// Reference to the thread which handles async graph updates. /// See: ProcessGraphUpdatesAsync /// Thread graphUpdateThread; #endif /// Used for IsAnyGraphUpdateInProgress bool anyGraphUpdateInProgress; #if UNITY_2017_3_OR_NEWER && !UNITY_WEBGL CustomSampler asyncUpdateProfilingSampler; #endif /// /// Queue containing all waiting graph update queries. Add to this queue by using \link AddToQueue \endlink. /// See: AddToQueue /// readonly Queue graphUpdateQueue = new Queue(); /// Queue of all async graph updates waiting to be executed readonly Queue graphUpdateQueueAsync = new Queue(); /// Queue of all non-async graph update post events waiting to be executed readonly Queue graphUpdateQueuePost = new Queue(); /// Queue of all non-async graph updates waiting to be executed readonly Queue graphUpdateQueueRegular = new Queue(); readonly System.Threading.ManualResetEvent asyncGraphUpdatesComplete = new System.Threading.ManualResetEvent(true); #if !UNITY_WEBGL readonly System.Threading.AutoResetEvent graphUpdateAsyncEvent = new System.Threading.AutoResetEvent(false); readonly System.Threading.AutoResetEvent exitAsyncThread = new System.Threading.AutoResetEvent(false); #endif /// Returns if any graph updates are waiting to be applied public bool IsAnyGraphUpdateQueued { get { return graphUpdateQueue.Count > 0; } } /// Returns if any graph updates are in progress public bool IsAnyGraphUpdateInProgress { get { return anyGraphUpdateInProgress; } } /// Order type for updating graphs enum GraphUpdateOrder { GraphUpdate, // FloodFill } /// Holds a single update that needs to be performed on a graph struct GUOSingle { public GraphUpdateOrder order; public IUpdatableGraph graph; public GraphUpdateObject obj; } public GraphUpdateProcessor (AstarPath astar) { this.astar = astar; } /// Work item which can be used to apply all queued updates public AstarWorkItem GetWorkItem () { return new AstarWorkItem(QueueGraphUpdatesInternal, ProcessGraphUpdates); } public void EnableMultithreading () { #if !UNITY_WEBGL if (graphUpdateThread == null || !graphUpdateThread.IsAlive) { #if UNITY_2017_3_OR_NEWER && !UNITY_WEBGL asyncUpdateProfilingSampler = CustomSampler.Create("Graph Update"); #endif graphUpdateThread = new Thread(ProcessGraphUpdatesAsync); graphUpdateThread.IsBackground = true; // Set the thread priority for graph updates // Unless compiling for windows store or windows phone which does not support it #if !UNITY_WINRT graphUpdateThread.Priority = System.Threading.ThreadPriority.Lowest; #endif graphUpdateThread.Start(); } #endif } public void DisableMultithreading () { #if !UNITY_WEBGL if (graphUpdateThread != null && graphUpdateThread.IsAlive) { // Resume graph update thread, will cause it to terminate exitAsyncThread.Set(); if (!graphUpdateThread.Join(5*1000)) { Debug.LogError("Graph update thread did not exit in 5 seconds"); } graphUpdateThread = null; } #endif } /// /// Update all graphs using the GraphUpdateObject. /// This can be used to, e.g make all nodes in an area unwalkable, or set them to a higher penalty. /// The graphs will be updated as soon as possible (with respect to AstarPath.batchGraphUpdates) /// /// See: FlushGraphUpdates /// public void AddToQueue (GraphUpdateObject ob) { // Put the GUO in the queue graphUpdateQueue.Enqueue(ob); } /// Schedules graph updates internally void QueueGraphUpdatesInternal () { while (graphUpdateQueue.Count > 0) { GraphUpdateObject ob = graphUpdateQueue.Dequeue(); if (ob.internalStage != GraphUpdateObject.STAGE_PENDING) { Debug.LogError("Expected remaining graph updates to be pending"); continue; } ob.internalStage = 0; foreach (IUpdatableGraph g in astar.data.GetUpdateableGraphs()) { NavGraph gr = g as NavGraph; if (ob.nnConstraint == null || ob.nnConstraint.SuitableGraph(astar.data.GetGraphIndex(gr), gr)) { var guo = new GUOSingle(); guo.order = GraphUpdateOrder.GraphUpdate; guo.obj = ob; guo.graph = g; ob.internalStage += 1; graphUpdateQueueRegular.Enqueue(guo); } } } GraphModifier.TriggerEvent(GraphModifier.EventType.PreUpdate); anyGraphUpdateInProgress = true; } /// /// Updates graphs. /// Will do some graph updates, possibly signal another thread to do them. /// Will only process graph updates added by QueueGraphUpdatesInternal /// /// Returns: True if all graph updates have been done and pathfinding (or other tasks) may resume. /// False if there are still graph updates being processed or waiting in the queue. /// /// If true, all graph updates will be processed before this function returns. The return value /// will be True. bool ProcessGraphUpdates (bool force) { Assert.IsTrue(anyGraphUpdateInProgress); if (force) { asyncGraphUpdatesComplete.WaitOne(); } else { #if !UNITY_WEBGL if (!asyncGraphUpdatesComplete.WaitOne(0)) { return false; } #endif } Assert.AreEqual(graphUpdateQueueAsync.Count, 0, "Queue should be empty at this stage"); ProcessPostUpdates(); if (!ProcessRegularUpdates(force)) { return false; } GraphModifier.TriggerEvent(GraphModifier.EventType.PostUpdate); if (OnGraphsUpdated != null) OnGraphsUpdated(); Assert.AreEqual(graphUpdateQueueRegular.Count, 0, "QueueRegular should be empty at this stage"); Assert.AreEqual(graphUpdateQueueAsync.Count, 0, "QueueAsync should be empty at this stage"); Assert.AreEqual(graphUpdateQueuePost.Count, 0, "QueuePost should be empty at this stage"); anyGraphUpdateInProgress = false; return true; } bool ProcessRegularUpdates (bool force) { while (graphUpdateQueueRegular.Count > 0) { GUOSingle s = graphUpdateQueueRegular.Peek(); GraphUpdateThreading threading = s.graph.CanUpdateAsync(s.obj); #if UNITY_WEBGL // Never use multithreading in WebGL threading &= ~GraphUpdateThreading.SeparateThread; #else // When not playing or when not using a graph update thread (or if it has crashed), everything runs in the Unity thread if (force || !Application.isPlaying || graphUpdateThread == null || !graphUpdateThread.IsAlive) { // Remove the SeparateThread flag threading &= ~GraphUpdateThreading.SeparateThread; } #endif if ((threading & GraphUpdateThreading.UnityInit) != 0) { // Process async graph updates first. // Next call to this function will process this object so it is not dequeued now if (StartAsyncUpdatesIfQueued()) { return false; } s.graph.UpdateAreaInit(s.obj); } if ((threading & GraphUpdateThreading.SeparateThread) != 0) { // Move GUO to async queue to be updated by another thread graphUpdateQueueRegular.Dequeue(); graphUpdateQueueAsync.Enqueue(s); // Don't start any more async graph updates because this update // requires a Unity thread function to run after it has been completed // but before the next update is started if ((threading & GraphUpdateThreading.UnityPost) != 0) { if (StartAsyncUpdatesIfQueued()) { return false; } } } else { // Unity Thread if (StartAsyncUpdatesIfQueued()) { return false; } graphUpdateQueueRegular.Dequeue(); try { s.graph.UpdateArea(s.obj); } catch (System.Exception e) { Debug.LogError("Error while updating graphs\n"+e); } if ((threading & GraphUpdateThreading.UnityPost) != 0) { s.graph.UpdateAreaPost(s.obj); } s.obj.internalStage -= 1; UnityEngine.Assertions.Assert.IsTrue(s.obj.internalStage >= 0); } } if (StartAsyncUpdatesIfQueued()) { return false; } return true; } /// /// Signal the graph update thread to start processing graph updates if there are any in the queue. /// Returns: True if the other thread was signaled. /// bool StartAsyncUpdatesIfQueued () { if (graphUpdateQueueAsync.Count > 0) { #if UNITY_WEBGL throw new System.Exception("This should not happen in WebGL"); #else asyncGraphUpdatesComplete.Reset(); graphUpdateAsyncEvent.Set(); return true; #endif } return false; } void ProcessPostUpdates () { while (graphUpdateQueuePost.Count > 0) { GUOSingle s = graphUpdateQueuePost.Dequeue(); GraphUpdateThreading threading = s.graph.CanUpdateAsync(s.obj); if ((threading & GraphUpdateThreading.UnityPost) != 0) { try { s.graph.UpdateAreaPost(s.obj); } catch (System.Exception e) { Debug.LogError("Error while updating graphs (post step)\n"+e); } } s.obj.internalStage -= 1; UnityEngine.Assertions.Assert.IsTrue(s.obj.internalStage >= 0); } } #if !UNITY_WEBGL /// /// Graph update thread. /// Async graph updates will be executed by this method in another thread. /// void ProcessGraphUpdatesAsync () { #if UNITY_2017_3_OR_NEWER Profiler.BeginThreadProfiling("Pathfinding", "Threaded Graph Updates"); #endif var handles = new [] { graphUpdateAsyncEvent, exitAsyncThread }; while (true) { // Wait for the next batch or exit event var handleIndex = WaitHandle.WaitAny(handles); if (handleIndex == 1) { // Exit even was fired // Abort thread and clear queue while (graphUpdateQueueAsync.Count > 0) { var s = graphUpdateQueueAsync.Dequeue(); s.obj.internalStage = GraphUpdateObject.STAGE_ABORTED; } asyncGraphUpdatesComplete.Set(); #if UNITY_2017_3_OR_NEWER Profiler.EndThreadProfiling(); #endif return; } while (graphUpdateQueueAsync.Count > 0) { #if UNITY_2017_3_OR_NEWER asyncUpdateProfilingSampler.Begin(); #endif // Note that no locking is required here because the main thread // cannot access it until asyncGraphUpdatesComplete is signaled GUOSingle aguo = graphUpdateQueueAsync.Dequeue(); try { if (aguo.order == GraphUpdateOrder.GraphUpdate) { aguo.graph.UpdateArea(aguo.obj); graphUpdateQueuePost.Enqueue(aguo); } else { throw new System.NotSupportedException("" + aguo.order); } } catch (System.Exception e) { Debug.LogError("Exception while updating graphs:\n"+e); } #if UNITY_2017_3_OR_NEWER asyncUpdateProfilingSampler.End(); #endif } // Done asyncGraphUpdatesComplete.Set(); } } #endif } }