1 /**
2 Copyright: Copyright (c) 2015-2016 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module voxelman.client.chunkmeshman;
7 
8 import std.experimental.logger;
9 import std.concurrency : Tid, thisTid, send, receiveTimeout;
10 import std.datetime : msecs;
11 
12 import voxelman.client.chunkman;
13 import voxelman.core.blockman;
14 import voxelman.core.chunkmesh;
15 import voxelman.core.config;
16 import voxelman.core.meshgen;
17 import voxelman.storage.chunk;
18 import voxelman.storage.coordinates;
19 import voxelman.utils.queue;
20 import voxelman.utils.workergroup;
21 import voxelman.utils.hashset;
22 
23 
24 ///
25 struct ChunkMeshMan
26 {
27 	WorkerGroup!(meshWorkerThread) meshWorkers;
28 
29 	ChunkChange[ChunkWorldPos] chunkChanges;
30 
31 	Queue!(Chunk*) changedChunks;
32 	Queue!(Chunk*) chunksToMesh;
33 	Queue!(Chunk*) dirtyChunks;
34 	HashSet!ChunkWorldPos visibleChunks;
35 
36 	size_t numMeshChunkTasks;
37 	size_t numDirtyChunksPending;
38 
39 	BlockMan* blockMan;
40 	ChunkMan* chunkMan;
41 
42 	void init(ChunkMan* _chunkMan, BlockMan* _blockMan, uint numWorkers)
43 	{
44 		chunkMan = _chunkMan;
45 		blockMan = _blockMan;
46 		meshWorkers.startWorkers(numWorkers, thisTid, blockMan.blocks);
47 	}
48 
49 	void stop()
50 	{
51 		meshWorkers.stopWorkers();
52 	}
53 
54 	void update()
55 	{
56 		bool message = true;
57 		while (message)
58 		{
59 			message = receiveTimeout(0.msecs,
60 				(immutable(MeshGenResult)* data){onMeshLoaded(cast(MeshGenResult*)data);}
61 				);
62 		}
63 
64 		startMeshUpdateCycle();
65 		applyChunkChanges();
66 		meshChunks();
67 		processDirtyChunks();
68 	}
69 
70 	void onChunkLoaded(Chunk* chunk, BlockData blockData)
71 	{
72 		// full chunk update
73 		if (chunk.isLoaded)
74 		{
75 			infof("full chunk change %s", chunk.position);
76 			// if there was previous changes they do not matter anymore
77 			chunkChanges[chunk.position] = ChunkChange(null, blockData);
78 			return;
79 		}
80 
81 		//infof("chunk loaded %s data %s", chunk.position, blockData.blocks);
82 
83 		chunk.isLoaded = true;
84 
85 		++chunkMan.totalLoadedChunks;
86 
87 		setChunkData(chunk, blockData);
88 
89 		if (chunk.isVisible)
90 			tryMeshChunk(chunk);
91 
92 		foreach(a; chunk.adjacent)
93 			if (a !is null) tryMeshChunk(a);
94 	}
95 
96 	void setChunkData(Chunk* chunk, ref BlockData blockData)
97 	{
98 		chunk.isVisible = true;
99 		if (blockData.uniform)
100 		{
101 			chunk.isVisible = blockMan.blocks[blockData.uniformType].isVisible;
102 		}
103 		chunk.snapshot.blockData = blockData;
104 	}
105 
106 	void onChunkChanged(Chunk* chunk, BlockChange[] changes)
107 	{
108 		//infof("partial chunk change %s", chunk.position);
109 		if (auto _changes = chunk.position in chunkChanges)
110 		{
111 			if (_changes.blockChanges is null)
112 			{
113 				// block changes applied on top of full chunk update
114 				_changes.newBlockData.applyChangesFast(changes);
115 			}
116 			else
117 			{
118 				// more changes added
119 				_changes.blockChanges ~= changes;
120 			}
121 		}
122 		else
123 		{
124 			// new changes arrived
125 			chunkChanges[chunk.position] = ChunkChange(changes);
126 		}
127 	}
128 
129 	void onChunkRemoved(Chunk* chunk)
130 	{
131 		visibleChunks.remove(chunk.position);
132 		chunkChanges.remove(chunk.position);
133 		changedChunks.remove(chunk);
134 		chunksToMesh.remove(chunk);
135 		dirtyChunks.remove(chunk);
136 	}
137 
138 	void tryMeshChunk(Chunk* chunk)
139 	{
140 		assert(chunk);
141 		if (chunk.needsMesh && chunk.canBeMeshed)
142 		{
143 			if (!surroundedBySolidChunks(chunk))
144 				meshChunk(chunk);
145 		}
146 	}
147 
148 	bool surroundedBySolidChunks(Chunk* chunk)
149 	{
150 		import voxelman.core.block;
151 		foreach(i, a; chunk.adjacent)
152 		if (a !is null) {
153 			bool solidSide = a.snapshot.blockData.uniform &&
154 			blockMan.blocks[a.snapshot.blockData.uniformType]
155 				.isSideTransparent(cast(Side)oppSide[i]);
156 			if (!solidSide) return false;
157 		}
158 		return true;
159 	}
160 
161 	void meshChunk(Chunk* chunk)
162 	{
163 		assert(chunk);
164 
165 		++chunk.numReaders;
166 		foreach(a; chunk.adjacent)
167 			if (a !is null) ++a.numReaders;
168 
169 		assert(chunk);
170 		assert(!chunk.hasWriter);
171 		foreach(a; chunk.adjacent)
172 		{
173 			assert(a !is null);
174 			assert(!a.hasWriter);
175 		}
176 
177 		chunk.isMeshing = true;
178 		++numMeshChunkTasks;
179 		meshWorkers.nextWorker.send(cast(shared(Chunk)*)chunk);
180 	}
181 
182 	void onMeshLoaded(MeshGenResult* data)
183 	{
184 		Chunk* chunk = chunkMan.getChunk(data.position);
185 		assert(chunk);
186 
187 		chunk.isMeshing = false;
188 
189 		// Allow chunk to be written or deleted.
190 		// TODO: that can break if chunks where added during meshing
191 		--chunk.numReaders;
192 		foreach(a; chunk.adjacent)
193 				if (a !is null) --a.numReaders;
194 		--numMeshChunkTasks;
195 
196 		// Chunk is already in delete queue
197 		if (chunk.isMarkedForDeletion)
198 		{
199 			delete data.meshData;
200 			delete data;
201 			return;
202 		}
203 
204 		//infof("mesh data loaded %s %s", data.position, data.meshData.length);
205 
206 		// chunk was remeshed after change.
207 		// Mesh will be uploaded for all changed chunks at once in processDirtyChunks.
208 		if (chunk.isDirty)
209 		{
210 			chunk.isDirty = false;
211 			chunk.newMeshData = data.meshData;
212 			--numDirtyChunksPending;
213 		}
214 		else
215 			loadMeshData(chunk, data.meshData);
216 	}
217 
218 	void loadMeshData(Chunk* chunk, ubyte[] meshData)
219 	{
220 		assert(chunk);
221 		// Attach mesh
222 		if (chunk.mesh is null)
223 			chunk.mesh = new ChunkMesh();
224 		chunk.mesh.data = meshData;
225 
226 		ChunkWorldPos position = chunk.position;
227 		chunk.mesh.position = position.vector * CHUNK_SIZE;
228 		chunk.mesh.isDataDirty = true;
229 		chunk.isVisible = chunk.mesh.data.length > 0;
230 		chunk.hasMesh = true;
231 
232 		if (chunk.isVisible)
233 			visibleChunks.put(chunk.position);
234 
235 		//infof("Chunk mesh loaded at %s, length %s", chunk.position, chunk.mesh.data.length);
236 	}
237 
238 	/// Checks if there is any chunks that have changes
239 	/// Starts new mesh update cycle if previous one was completed.
240 	/// Adds changed chunks to changedChunks queue on new cycle start
241 	void startMeshUpdateCycle()
242 	{
243 		auto queuesEmpty = changedChunks.empty &&
244 			chunksToMesh.empty && dirtyChunks.empty;
245 
246 		if (!queuesEmpty || chunkChanges.length == 0)
247 			return;
248 
249 		trace("startMeshUpdateCycle");
250 
251 		foreach(pair; chunkChanges.byKeyValue)
252 		{
253 			Chunk** chunkPtr = pair.key in chunkMan.chunks;
254 			if (chunkPtr is null || (**chunkPtr).isMarkedForDeletion || (*chunkPtr) is null)
255 			{
256 				chunkChanges.remove(pair.key);
257 				continue;
258 			}
259 
260 			Chunk* chunk = *chunkPtr;
261 			assert(chunk);
262 
263 			chunk.change = pair.value;
264 			chunk.hasUnappliedChanges = true;
265 			changedChunks.put(chunk);
266 			chunkChanges.remove(pair.key);
267 		}
268 
269 		chunkChanges = null;
270 	}
271 
272 	/// Applies changes to chunks
273 	/// Calculates affected chunks and adds them to chunksToMesh queue
274 	void applyChunkChanges()
275 	{
276 		foreach(queueItem; changedChunks)
277 		{
278 			Chunk* chunk = queueItem.value;
279 			if (chunk is null)
280 			{
281 				queueItem.remove();
282 				continue;
283 			}
284 			assert(chunk);
285 
286 			void addAdjacentChunks()
287 			{
288 				foreach(a; chunk.adjacent)
289 				{
290 					if (a && a.canBeMeshed)
291 						chunksToMesh.put(a);
292 				}
293 			}
294 
295 			if (!chunk.isUsed)
296 			{
297 				bool blocksChanged = false;
298 				// apply changes
299 				if (chunk.change.blockChanges is null)
300 				{
301 					// full chunk update
302 					setChunkData(chunk, chunk.change.newBlockData);
303 					// TODO remove mesh if not visible
304 					addAdjacentChunks();
305 					blocksChanged = true;
306 
307 					infof("applying full update to %s", chunk.position);
308 				}
309 				else
310 				{
311 					// partial update
312 					ushort[2] changedBlocksRange = chunk
313 						.snapshot
314 						.blockData
315 						.applyChanges(chunk.change.blockChanges);
316 
317 					// blocks was changed
318 					if (changedBlocksRange[0] != changedBlocksRange[1])
319 					{
320 						addAdjacentChunks();
321 						blocksChanged = true;
322 					}
323 					//infof("applying block changes to %s", chunk.position);
324 					ubyte bx, by, bz;
325 					foreach(change; chunk.change.blockChanges)
326 					{
327 						bx = change.index & CHUNK_SIZE_BITS;
328 						by = (change.index / CHUNK_SIZE_SQR) & CHUNK_SIZE_BITS;
329 						bz = (change.index / CHUNK_SIZE) & CHUNK_SIZE_BITS;
330 						tracef("i %s | x %s y %s z %s | wx %s wy %s wz %s | b %s; ",
331 							change.index,
332 							bx,
333 							by,
334 							bz,
335 							bx + chunk.position.x * CHUNK_SIZE,
336 							by + chunk.position.y * CHUNK_SIZE,
337 							bz + chunk.position.z * CHUNK_SIZE,
338 							change.blockType);
339 					}
340 				}
341 
342 				chunk.change = ChunkChange.init;
343 
344 				//infof("canBeMeshed %s, blocksChanged %s", chunk.canBeMeshed, blocksChanged);
345 				if (chunk.canBeMeshed && blocksChanged)
346 				{
347 					assert(chunk);
348 					chunksToMesh.put(chunk);
349 				}
350 
351 				chunk.hasUnappliedChanges = false;
352 
353 				queueItem.remove();
354 			}
355 		}
356 	}
357 
358 	/// Sends chunks from chunksToMesh queue to mesh worker and moves them
359 	/// to dirtyChunks queue
360 	void meshChunks()
361 	{
362 		foreach(queueItem; chunksToMesh)
363 		{
364 			Chunk* chunk = queueItem.value;
365 			if (chunk is null)
366 			{
367 				queueItem.remove();
368 				continue;
369 			}
370 			assert(chunk);
371 
372 			// chunks adjacent to the modified one may still be in use
373 			if (!chunk.isUsed && !chunk.adjacentHasUnappliedChanges)
374 			{
375 				meshChunk(chunk);
376 				++numDirtyChunksPending;
377 				queueItem.remove();
378 			}
379 		}
380 	}
381 
382 	///
383 	void processDirtyChunks()
384 	{
385 		auto queuesEmpty = changedChunks.empty && chunksToMesh.empty;
386 
387 		// swap meshes when all chunks are meshed
388 		if (queuesEmpty && numDirtyChunksPending == 0)
389 		{
390 			foreach(chunk; dirtyChunks.valueRange)
391 			{
392 				loadMeshData(chunk, chunk.newMeshData);
393 				chunk.newMeshData = null;
394 			}
395 		}
396 	}
397 }