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 }