/
QuadNode.cs
292 lines (246 loc) · 11.9 KB
/
QuadNode.cs
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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Diagnostics;
namespace GenesisEngine
{
public class QuadNode : IQuadNode, IDisposable
{
DoubleVector3 _locationRelativeToPlanet;
double _planetRadius;
DoubleVector3 _uVector;
DoubleVector3 _vVector;
DoubleVector3 _planeNormalVector;
protected QuadNodeExtents _extents;
bool _hasSubnodes;
protected bool _splitInProgress;
protected bool _mergeInProgress;
protected Task _splitCompletionTask;
protected Task _backgroundMergeTask;
protected CancellationTokenSource _cancellationTokenSource;
protected List<IQuadNode> _subnodes = new List<IQuadNode>();
readonly IQuadMesh _mesh;
readonly IQuadNodeFactory _quadNodeFactory;
readonly ISplitMergeStrategy _splitMergeStrategy;
readonly ITaskSchedulerFactory _taskSchedulerFactory;
readonly IQuadNodeRenderer _renderer;
readonly Statistics _statistics;
public QuadNode(IQuadMesh mesh, IQuadNodeFactory quadNodeFactory, ISplitMergeStrategy splitMergeStrategy, ITaskSchedulerFactory taskSchedulerFactory, IQuadNodeRenderer renderer, Statistics statistics)
{
_mesh = mesh;
_quadNodeFactory = quadNodeFactory;
_splitMergeStrategy = splitMergeStrategy;
_taskSchedulerFactory = taskSchedulerFactory;
_renderer = renderer;
_statistics = statistics;
}
public int Level { get; private set; }
// TODO: push this data in through the constructor, probably in a QuadNodeDefintion class, and make
// this method private. Except that would do real work in construction. Hmmm.
public void Initialize(double planetRadius, DoubleVector3 planeNormalVector, DoubleVector3 uVector, DoubleVector3 vVector, QuadNodeExtents extents, int level)
{
_planetRadius = planetRadius;
_planeNormalVector = planeNormalVector;
_uVector = uVector;
_vVector = vVector;
_extents = extents;
Level = level;
_locationRelativeToPlanet = (_planeNormalVector) + (_uVector * (_extents.North + (_extents.Width / 2.0))) + (_vVector * (_extents.West + (_extents.Width / 2.0)));
_locationRelativeToPlanet = _locationRelativeToPlanet.ProjectUnitPlaneToUnitSphere() * _planetRadius;
_mesh.Initialize(planetRadius, planeNormalVector, uVector, vVector, extents, level);
Interlocked.Increment(ref _statistics.NumberOfQuadNodes);
Interlocked.Increment(ref _statistics.NumberOfQuadNodesAtLevel[Level]);
}
public void Update(DoubleVector3 cameraLocation, DoubleVector3 planetLocation)
{
_mesh.Update(cameraLocation, planetLocation);
if (ShouldSplit())
{
Split(cameraLocation, planetLocation);
}
else if (ShouldMerge())
{
Merge();
}
else if (ShouldCancelSplit())
{
CancelSplit();
}
UpdateSubnodes(cameraLocation, planetLocation);
}
bool ShouldSplit()
{
return IsSplittable() && _splitMergeStrategy.ShouldSplit(_mesh, Level);
}
bool IsSplittable()
{
return !_hasSubnodes && !(_splitInProgress || _mergeInProgress);
}
bool ShouldMerge()
{
return IsMergable() && _splitMergeStrategy.ShouldMerge(_mesh);
}
bool IsMergable()
{
return _hasSubnodes && !(_splitInProgress || _mergeInProgress);
}
bool ShouldCancelSplit()
{
return _splitInProgress && !_cancellationTokenSource.IsCancellationRequested && !_splitMergeStrategy.ShouldSplit(_mesh, Level);
}
void CancelSplit()
{
Interlocked.Increment(ref _statistics.NumberOfSplitsCanceledPerInterval);
_cancellationTokenSource.Cancel();
}
void UpdateSubnodes(DoubleVector3 cameraLocation, DoubleVector3 planetLocation)
{
if (_hasSubnodes)
{
foreach (var subnode in _subnodes)
{
subnode.Update(cameraLocation, planetLocation);
}
}
}
private void Split(DoubleVector3 cameraLocation, DoubleVector3 planetLocation)
{
// TODO: should we bother to write specs for the threading behavior?
Interlocked.Increment(ref _statistics.NumberOfSplitsScheduledPerInterval);
Interlocked.Increment(ref _statistics.NumberOfPendingSplits);
CreateCancellationTokenSource();
var splitTasks = CreateBackgroundSplitTasks(cameraLocation, planetLocation);
CreateSplitCompletionTask(splitTasks);
}
void CreateCancellationTokenSource()
{
_cancellationTokenSource = new CancellationTokenSource();
}
List<Task<IQuadNode>> CreateBackgroundSplitTasks(DoubleVector3 cameraLocation, DoubleVector3 planetLocation)
{
// TODO: there's a problem with this algorithm. If we need to split very deeply because for example
// the camera is teleported to the surface, we only split one level at a time and wait for that level
// to finish and for the next update sweep to occur before queuing splits for the next level. There
// may be wasted time in there (not clear yet). We also spend time generating meshes for quads that
// we know we don't need. Ideally we'd jump straight to rendering meshes for the new leaves and worry
// about rendering meshes for the intermediate nodes later when we need them. This means we need a
// general way to delay completing a merge (continuing to render its children in the meantime) until
// a mesh is rendered for that node. Once we have that behavior
// we can maybe throw away the vertex buffers for all non-leaf meshes to save memory and regenerate them
// as needed. That would take more CPU overall but it's not time sensitive because we'd just keep
// rendering the child nodes until we got around to it. That would be a problem only if we end up
// overloading the GPU but in many cases that wouldn't happen because the merging nodes would be behind
// the camera as it travels and thus not rendered.
// Potential problem: we don't know for sure if a node needs to be split until after we generate its mesh.
_splitInProgress = true;
var subextents = _extents.Split();
return subextents.Select(extent => Task<IQuadNode>.Factory.StartNew(() =>
{
var node = _quadNodeFactory.Create();
node.Initialize(_planetRadius, _planeNormalVector, _uVector, _vVector, extent, Level + 1);
node.Update(cameraLocation, planetLocation);
return node;
}, _cancellationTokenSource.Token, TaskCreationOptions.None, _taskSchedulerFactory.CreateForLevel(Level))).ToList();
}
void CreateSplitCompletionTask(List<Task<IQuadNode>> tasks)
{
_splitCompletionTask = Task.Factory.ContinueWhenAll(tasks.ToArray(), finishedTasks =>
{
if (!_cancellationTokenSource.IsCancellationRequested)
{
StoreCompletedSubnodes(finishedTasks);
}
else
{
// TODO: this class is coupled too closely with the test-unfriendly TPL so we can't
// easily write tests for this kind of required behavior. How to fix?
DisposeCompletedSubnodes(finishedTasks);
}
_splitInProgress = false;
Interlocked.Decrement(ref _statistics.NumberOfPendingSplits);
}, CancellationToken.None, TaskContinuationOptions.None, _taskSchedulerFactory.CreateForLevel(Level));
}
void StoreCompletedSubnodes(IEnumerable<Task<IQuadNode>> finishedTasks)
{
foreach (var task in finishedTasks)
{
_subnodes.Add(task.Result);
}
_hasSubnodes = true;
}
void DisposeCompletedSubnodes(IEnumerable<Task<IQuadNode>> finishedTasks)
{
foreach (var task in finishedTasks.Where(task => task.Status == TaskStatus.RanToCompletion))
{
((IDisposable)task.Result).Dispose();
}
}
private void Merge()
{
Interlocked.Increment(ref _statistics.NumberOfPendingMerges);
// TODO: if a split is pending, cancel it
_mergeInProgress = true;
_hasSubnodes = false;
CreateCancellationTokenSource();
CreateBackgroundMergeTask();
}
void CreateBackgroundMergeTask()
{
_backgroundMergeTask = Task.Factory.StartNew(() =>
{
DisposeSubNodes();
_subnodes.Clear();
_mergeInProgress = false;
Interlocked.Decrement(ref _statistics.NumberOfPendingMerges);
}, _cancellationTokenSource.Token, TaskCreationOptions.None, _taskSchedulerFactory.Create());
}
void DisposeSubNodes()
{
// TODO: we have a race condition here where the parent node might
// decide to dispose this node at the same time that this node is
// being split. We get an exception here because the _subNodes
// collection is modified while we're iterating it.
foreach (var node in _subnodes)
{
((IDisposable)node).Dispose();
}
}
public void Draw(DoubleVector3 cameraLocation, BoundingFrustum originBasedViewFrustum, Matrix originBasedViewMatrix, Matrix projectionMatrix)
{
if (_hasSubnodes)
{
// TODO: we'd like to stop traversing into subnodes if this node's mesh isn't visibile, but our
// horizon culling algorithm isn't that great right now and the primary faces are so large that
// sometimes all of their sample points are below the horizon even though we're above that face
// and would want to draw its children. For now, we'll scan all subnodes regardless. The child
// node's meshes will do visibility culling on an individual basis.
foreach (var subnode in _subnodes)
{
subnode.Draw(cameraLocation, originBasedViewFrustum, originBasedViewMatrix, projectionMatrix);
}
}
else
{
_renderer.Draw(_locationRelativeToPlanet, cameraLocation, originBasedViewMatrix, projectionMatrix);
_mesh.Draw(cameraLocation, originBasedViewFrustum, originBasedViewMatrix, projectionMatrix);
}
}
public void Dispose()
{
((IDisposable)_renderer).Dispose();
((IDisposable)_mesh).Dispose();
DisposeSubNodes();
if (_cancellationTokenSource != null)
{
_cancellationTokenSource.Cancel();
}
Interlocked.Decrement(ref _statistics.NumberOfQuadNodes);
Interlocked.Decrement(ref _statistics.NumberOfQuadNodesAtLevel[Level]);
}
}
}