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
|
using System.Collections.Generic;
using UnityEngine;
namespace Pathfinding {
using Pathfinding.Jobs;
using Pathfinding.Util;
using Unity.Jobs;
using Unity.Profiling;
using UnityEngine.Assertions;
using UnityEngine.Profiling;
/// <summary>
/// Promise representing a graph update.
///
/// This is used internally by the system to represent a graph update.
/// Generally you shouldn't need to care about it, unless you are implementing your own graph type.
/// </summary>
public interface IGraphUpdatePromise {
/// <summary>
/// Returns the progress of the update.
///
/// This should be a value between 0 and 1.
/// </summary>
float Progress => 0.0f;
/// <summary>
/// Coroutine to prepare an update asynchronously.
///
/// If a JobHandle is returned, it will be awaited before the coroutine is ticked again, and before the <see cref="Apply"/> method is called.
///
/// After this coroutine has finished, the <see cref="Apply"/> method will be called.
///
/// Note: No changes must be made to the graph in this method. Those should only be done in the <see cref="Apply"/> method.
///
/// May return null if no async work is required.
/// </summary>
IEnumerator<JobHandle> Prepare() => null;
/// <summary>
/// Applies the update in a single atomic update.
///
/// It is done as a single atomic update (from the main thread's perspective) to ensure
/// that even if one does an async scan or update, the graph will always be in a valid state.
/// This guarantees that things like GetNearest will still work during an async scan.
///
/// Warning: Must only be called after the <see cref="Prepare"/> method has finished.
/// </summary>
// TODO: Pass in a JobHandle and allow returning a JobHandle?
void Apply(IGraphUpdateContext context);
}
/// <summary>
/// Helper functions for graph updates.
///
/// A context is passed to graphs when they are updated, and to work items when they are executed.
/// The <see cref="IWorkItemContext"/> interface inherits from this interface.
/// </summary>
public interface IGraphUpdateContext {
/// <summary>
/// Mark a particular region of the world as having been changed.
///
/// This should be used whenever graphs are changed.
///
/// This is used to recalculate off-mesh links that touch these bounds, and it will also ensure <see cref="GraphModifier"/> events are callled.
///
/// The bounding box should cover the surface of all nodes that have been updated.
/// It is fine to use a larger bounding box than necessary (even an infinite one), though this may be slower, since more off-mesh links need to be recalculated.
/// You can even use an infinitely large bounding box if you don't want to bother calculating a more accurate one.
/// You can also call this multiple times to dirty multiple bounding boxes.
/// </summary>
void DirtyBounds(Bounds bounds);
}
class GraphUpdateProcessor {
/// <summary>Holds graphs that can be updated</summary>
readonly AstarPath astar;
/// <summary>Used for IsAnyGraphUpdateInProgress</summary>
bool anyGraphUpdateInProgress;
/// <summary>
/// Queue containing all waiting graph update queries. Add to this queue by using \link AddToQueue \endlink.
/// See: AddToQueue
/// </summary>
readonly Queue<GraphUpdateObject> graphUpdateQueue = new Queue<GraphUpdateObject>();
readonly List<(IGraphUpdatePromise, IEnumerator<JobHandle>)> pendingPromises = new List<(IGraphUpdatePromise, IEnumerator<JobHandle>)>();
readonly List<GraphUpdateObject> pendingGraphUpdates = new List<GraphUpdateObject>();
/// <summary>Returns if any graph updates are waiting to be applied</summary>
public bool IsAnyGraphUpdateQueued { get { return graphUpdateQueue.Count > 0; } }
/// <summary>Returns if any graph updates are in progress</summary>
public bool IsAnyGraphUpdateInProgress { get { return anyGraphUpdateInProgress; } }
public GraphUpdateProcessor (AstarPath astar) {
this.astar = astar;
}
/// <summary>Work item which can be used to apply all queued updates</summary>
public AstarWorkItem GetWorkItem () {
return new AstarWorkItem(QueueGraphUpdatesInternal, ProcessGraphUpdates);
}
/// <summary>
/// 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
/// </summary>
public void AddToQueue (GraphUpdateObject ob) {
// Put the GUO in the queue
graphUpdateQueue.Enqueue(ob);
}
/// <summary>
/// Discards all queued graph updates.
///
/// Graph updates that are already in progress will not be discarded.
/// </summary>
public void DiscardQueued () {
while (graphUpdateQueue.Count > 0) {
graphUpdateQueue.Dequeue().internalStage = GraphUpdateObject.STAGE_ABORTED;
}
}
/// <summary>Schedules graph updates internally</summary>
void QueueGraphUpdatesInternal (IWorkItemContext context) {
Assert.IsTrue(pendingGraphUpdates.Count == 0);
while (graphUpdateQueue.Count > 0) {
var ob = graphUpdateQueue.Dequeue();
pendingGraphUpdates.Add(ob);
if (ob.internalStage != GraphUpdateObject.STAGE_PENDING) {
Debug.LogError("Expected remaining graph update to be pending");
continue;
}
}
foreach (IUpdatableGraph g in astar.data.GetUpdateableGraphs()) {
NavGraph gr = g as NavGraph;
var updates = ListPool<GraphUpdateObject>.Claim();
for (int i = 0; i < pendingGraphUpdates.Count; i++) {
GraphUpdateObject ob = pendingGraphUpdates[i];
if (ob.nnConstraint == null || ob.nnConstraint.SuitableGraph((int)gr.graphIndex, gr)) {
updates.Add(ob);
}
}
if (updates.Count > 0) {
var promise = g.ScheduleGraphUpdates(updates);
if (promise != null) {
var it = promise.Prepare();
pendingPromises.Add((promise, it));
} else {
ListPool<GraphUpdateObject>.Release(ref updates);
}
} else {
ListPool<GraphUpdateObject>.Release(ref updates);
}
}
context.PreUpdate();
anyGraphUpdateInProgress = true;
}
static readonly ProfilerMarker MarkerSleep = new ProfilerMarker(ProfilerCategory.Loading, "Sleep");
static readonly ProfilerMarker MarkerCalculate = new ProfilerMarker("Calculating Graph Update");
static readonly ProfilerMarker MarkerApply = new ProfilerMarker("Applying Graph Update");
/// <summary>
/// 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.
/// </summary>
/// <param name="context">Helper methods for the work items.</param>
/// <param name="force">If true, all graph updates will be processed before this function returns. The return value
/// will be True.</param>
bool ProcessGraphUpdates (IWorkItemContext context, bool force) {
Assert.IsTrue(anyGraphUpdateInProgress);
if (pendingPromises.Count > 0) {
if (!ProcessGraphUpdatePromises(pendingPromises, context, force)) {
return false;
}
pendingPromises.Clear();
}
anyGraphUpdateInProgress = false;
for (int i = 0; i < pendingGraphUpdates.Count; i++) {
pendingGraphUpdates[i].internalStage = GraphUpdateObject.STAGE_APPLIED;
}
pendingGraphUpdates.Clear();
return true;
}
public static bool ProcessGraphUpdatePromises (List<(IGraphUpdatePromise, IEnumerator<JobHandle>)> promises, IGraphUpdateContext context, bool force = false) {
TimeSlice timeSlice = TimeSlice.MillisFromNow(2f);
TimeSlice idleTimeSlice = default;
while (true) {
int firstNonFinished = -1;
bool anyMainThreadProgress = false;
for (int i = 0; i < promises.Count; i++) {
var(promise, it) = promises[i];
if (it == null) continue;
if (force) {
it.Current.Complete();
} else {
if (it.Current.IsCompleted) {
it.Current.Complete();
} else {
if (firstNonFinished == -1) firstNonFinished = i;
continue;
}
}
anyMainThreadProgress = true;
MarkerCalculate.Begin();
try {
if (it.MoveNext()) {
if (firstNonFinished == -1) firstNonFinished = i;
} else promises[i] = (promise, null);
} catch (System.Exception e) {
Debug.LogError(new System.Exception("Error while updating graphs.", e));
promises[i] = (null, null);
}
MarkerCalculate.End();
}
if (firstNonFinished == -1) {
break;
} else if (!force) {
if (timeSlice.expired) {
return false;
} else if (anyMainThreadProgress) {
// Reset the idle time slice if we got something done on the main thread.
// This allows us to wait on more very short jobs.
idleTimeSlice = TimeSlice.MillisFromNow(0.1f);
} else if (idleTimeSlice.expired) {
return false;
} else {
// Allow waiting for a short amount of time to allow very short running
// jobs in graph updates to complete without having to wait until the next frame.
// While waiting we release our thread's time slice to make sure other threads get priority.
if (!anyMainThreadProgress) {
MarkerSleep.Begin();
System.Threading.Thread.Yield();
MarkerSleep.End();
}
}
}
}
for (int i = 0; i < promises.Count; i++) {
var(promise, it) = promises[i];
Assert.IsNull(it);
if (promise != null) {
MarkerApply.Begin();
try {
promise.Apply(context);
} catch (System.Exception e) {
Debug.LogError(new System.Exception("Error while updating graphs.", e));
}
MarkerApply.End();
}
}
return true;
}
}
}
|