1 /** 2 Copyright: Copyright (c) 2015-2017 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module voxelman.world.mesh.chunkmeshman; 7 8 import voxelman.log; 9 import std.datetime : Duration; 10 import std.typecons : Nullable; 11 import anchovy.isharedcontext; 12 13 import voxelman.geometry.box; 14 import voxelman.math; 15 import voxelman.world.block; 16 import voxelman.world.blockentity; 17 import voxelman.world.mesh.chunkmesh; 18 import voxelman.core.config; 19 import voxelman.world.mesh.meshgen; 20 import voxelman.world.storage; 21 import voxelman.utils.worker; 22 import voxelman.container.hashset; 23 import voxelman.graphics; 24 import voxelman.geometry.cube; 25 26 27 alias MeshingPassDoneHandler = void delegate(size_t chunksRemeshed, Duration totalDuration); 28 struct MeshingPass 29 { 30 size_t chunksToMesh; 31 size_t meshGroupId; 32 MeshingPassDoneHandler onDone; 33 size_t chunksMeshed; 34 Duration totalDuration; 35 } 36 37 enum debug_wasted_meshes = true; 38 enum WAIT_FOR_EMPTY_QUEUES = false; 39 40 //version = DBG; 41 42 struct MeshGenResult 43 { 44 MeshGenTaskType type; 45 ChunkWorldPos cwp; 46 ChunkMesh[2] meshes; 47 } 48 49 struct UploadLimiter 50 { 51 bool limitPreloadSpeed = false; 52 size_t maxPreloadedMeshesPerFrame = 30; 53 size_t maxPreloadedVertexesPerFrame = 100_000; 54 55 size_t thisFramePreloadedMeshes; 56 size_t thisFramePreloadedVertexes; 57 58 bool frameUploadLimitExceeded() 59 { 60 //return thisFramePreloadedMeshes >= maxPreloadedMeshesPerFrame; 61 return limitPreloadSpeed && thisFramePreloadedVertexes >= maxPreloadedVertexesPerFrame; 62 } 63 64 void onMeshPreloaded(size_t numVertexes) 65 { 66 ++thisFramePreloadedMeshes; 67 thisFramePreloadedVertexes += numVertexes; 68 } 69 70 void resetUploadLimits() 71 { 72 thisFramePreloadedMeshes = 0; 73 thisFramePreloadedVertexes = 0; 74 } 75 } 76 77 /// 78 struct ChunkMeshMan 79 { 80 shared WorkerGroup meshWorkers; 81 82 ubyte[ChunkWorldPos] wastedMeshes; 83 84 ChunkMesh[ChunkWorldPos][2] chunkMeshes; 85 86 MeshGenResult[] newChunkMeshes; 87 MeshingPass[] meshingPasses; 88 size_t currentMeshGroupId; 89 90 size_t numMeshChunkTasks; 91 size_t totalMeshedChunks; 92 size_t totalMeshes; 93 long totalMeshDataBytes; 94 95 UploadLimiter uploadLimiter; 96 97 ChunkManager chunkManager; 98 BlockInfoTable blocks; 99 BlockEntityInfoTable beInfos; 100 Box delegate(DimensionId) getDimensionBorders; 101 102 void init(ChunkManager _chunkManager, BlockInfoTable _blocks, BlockEntityInfoTable _beInfos, uint numMeshWorkers) 103 { 104 chunkManager = _chunkManager; 105 blocks = _blocks; 106 beInfos = _beInfos; 107 108 meshWorkers.startWorkers(numMeshWorkers, &meshWorkerThread, SeparatedBlockInfoTable(blocks), beInfos); 109 } 110 111 void stop() 112 { 113 static if (WAIT_FOR_EMPTY_QUEUES) 114 { 115 while (!meshWorkers.queuesEmpty()) 116 { 117 update(); 118 } 119 } 120 meshWorkers.stop(); 121 } 122 123 // Returns number of chunks sent to be meshed 124 size_t remeshChangedChunks(HashSet!ChunkWorldPos modifiedChunks, 125 MeshingPassDoneHandler onDone = null) 126 { 127 if (modifiedChunks.length == 0) return 0; 128 129 size_t numMeshed; 130 foreach(cwp; modifiedChunks) 131 { 132 if (meshChunk(cwp)) 133 ++numMeshed; 134 } 135 136 if (numMeshed == 0) return 0; 137 138 meshingPasses ~= MeshingPass(numMeshed, currentMeshGroupId, onDone); 139 ++currentMeshGroupId; 140 141 return numMeshed; 142 } 143 144 void update() 145 { 146 if (meshingPasses.length == 0) return; 147 148 uploadLimiter.resetUploadLimits(); 149 150 foreach(ref w; meshWorkers.workers) 151 { 152 while(!w.resultQueue.empty) 153 { 154 bool breakLoop = receiveTaskResult(w); 155 if (breakLoop) break; 156 } 157 } 158 159 commitMeshes(); 160 } 161 162 bool receiveTaskResult(ref shared Worker w) 163 { 164 auto taskHeader = w.resultQueue.peekItem!MeshGenTaskHeader(); 165 166 // Process only current meshing pass. Leave next passes for later. 167 if (taskHeader.meshGroupId != meshingPasses[0].meshGroupId) 168 { 169 //infof("meshGroup %s != %s", taskHeader.meshGroupId, meshingPasses[0].meshGroupId); 170 return true; 171 } 172 173 ++meshingPasses[0].chunksMeshed; 174 --numMeshChunkTasks; 175 176 w.resultQueue.dropItem!MeshGenTaskHeader(); 177 178 if (taskHeader.type == MeshGenTaskType.genMesh) 179 { 180 MeshVertex[][2] meshes = w.resultQueue.popItem!(MeshVertex[][2])(); 181 uint[27] blockTimestamps = w.resultQueue.popItem!(uint[27])(); 182 uint[27] entityTimestamps = w.resultQueue.popItem!(uint[27])(); 183 uint[27] metadataTimestamps = w.resultQueue.popItem!(uint[27])(); 184 meshingPasses[0].totalDuration += w.resultQueue.popItem!Duration(); 185 186 // Remove users 187 auto positions = AdjChunkPositions27(taskHeader.cwp); 188 foreach(i, pos; positions.all) 189 { 190 chunkManager.removeSnapshotUser(pos, blockTimestamps[i], BLOCK_LAYER); 191 chunkManager.removeSnapshotUser(pos, entityTimestamps[i], ENTITY_LAYER); 192 chunkManager.removeSnapshotUser(pos, metadataTimestamps[i], METADATA_LAYER); 193 } 194 195 // save result for later. All new meshes are loaded at once to prevent holes in geometry. 196 auto result = MeshGenResult(taskHeader.type, taskHeader.cwp); 197 preloadMesh(result, meshes); 198 newChunkMeshes ~= result; 199 } 200 else // taskHeader.type == MeshGenTaskType.unloadMesh 201 { 202 // even mesh deletions are saved in a queue. 203 newChunkMeshes ~= MeshGenResult(taskHeader.type, taskHeader.cwp); 204 } 205 206 if (uploadLimiter.frameUploadLimitExceeded) 207 return true; 208 209 return false; 210 } 211 212 void commitMeshes() 213 { 214 import std.algorithm : remove, SwapStrategy; 215 if (meshingPasses[0].chunksMeshed != meshingPasses[0].chunksToMesh) return; 216 217 foreach(ref meshResult; newChunkMeshes) 218 { 219 if (meshResult.type == MeshGenTaskType.genMesh) 220 { 221 loadMeshData(meshResult); 222 } 223 else // taskHeader.type == MeshGenTaskType.unloadMesh 224 { 225 unloadChunkMesh(meshResult.cwp); 226 } 227 meshResult = MeshGenResult.init; 228 } 229 newChunkMeshes.length = 0; 230 newChunkMeshes.assumeSafeAppend(); 231 if (meshingPasses[0].onDone) 232 meshingPasses[0].onDone(meshingPasses[0].chunksToMesh, meshingPasses[0].totalDuration); 233 meshingPasses = remove!(SwapStrategy.stable)(meshingPasses, 0); 234 meshingPasses.assumeSafeAppend(); 235 } 236 237 void drawDebug(ref Batch debugBatch) 238 { 239 static if (debug_wasted_meshes) 240 foreach(cwp; wastedMeshes.byKey) 241 { 242 vec3 blockPos = cwp.vector * CHUNK_SIZE; 243 debugBatch.putCube(blockPos + CHUNK_SIZE/2-1, vec3(4,4,4), Colors.red, false); 244 } 245 } 246 247 void onChunkRemoved(ChunkWorldPos cwp) 248 { 249 unloadChunkMesh(cwp); 250 static if (debug_wasted_meshes) 251 { 252 wastedMeshes.remove(cwp); 253 } 254 } 255 256 bool producesMesh( 257 const ref Nullable!ChunkLayerSnap[6] adjacent, 258 const ref ChunkLayerSnap central) 259 { 260 import voxelman.world.block; 261 262 Solidity solidity; 263 bool singleSolidity = hasSingleSolidity(central.metadata, solidity); 264 265 if (singleSolidity) 266 { 267 // completely transparent chunks do not produce meshes. 268 if (solidity == Solidity.transparent) { 269 return false; 270 } else { 271 if (central.isUniform) 272 { 273 // uniform unknown chunks do not produce mesh. 274 if (central.getUniform!BlockId == 0) 275 return false; 276 } 277 } 278 279 foreach(CubeSide side, adj; adjacent) 280 { 281 if (!adj.isNull()) 282 { 283 Solidity adjSideSolidity = chunkSideSolidity(adj.metadata, oppSide[side]); 284 if (solidity.isMoreSolidThan(adjSideSolidity)) return true; 285 } 286 // otherwise it is unknown blocks, which are solid 287 } 288 289 // uniformly solid chunk is surrounded by blocks with the same of higher solidity. 290 // so mesh will not be produced. 291 return false; 292 } 293 else 294 { 295 // on borders between different solidities mesh is present. 296 return true; 297 } 298 } 299 300 // returns true if was sent to mesh 301 bool meshChunk(ChunkWorldPos cwp) 302 { 303 Box dimBorders = getDimensionBorders(cwp.w); 304 auto snapsPositions = AdjChunkPositions27(cwp); 305 306 foreach(pos; snapsPositions.all) 307 { 308 if (!chunkManager.isChunkLoaded(pos)) 309 { 310 if (dimBorders.contains(pos.ivector3)) 311 { 312 // chunk in dim borders is not loaded 313 return false; 314 } 315 } 316 } 317 318 assert(dimBorders.contains(snapsPositions.central.ivector3)); 319 320 AdjChunkLayers27 snapsBlocks; 321 322 // get compressed layers first to look at metadata. 323 snapsBlocks.central = chunkManager.getChunkSnapshot(snapsPositions.central, BLOCK_LAYER); 324 if (snapsBlocks.central.isNull()) 325 return false; 326 snapsBlocks.adjacent6 = chunkManager.getChunkSnapshots(snapsPositions.adjacent6, BLOCK_LAYER); 327 328 ++numMeshChunkTasks; 329 330 if (!producesMesh(snapsBlocks.adjacent6, snapsBlocks.central)) 331 { 332 version(DBG) tracef("meshChunk %s produces no mesh", cwp); 333 334 // send remove mesh task 335 with(meshWorkers.nextWorker) { 336 auto header = MeshGenTaskHeader(MeshGenTaskType.unloadMesh, currentMeshGroupId, cwp); 337 taskQueue.pushItem(header); 338 notify(); 339 } 340 341 return true; 342 } 343 344 version(DBG) tracef("meshChunk %s", cwp); 345 346 // get uncompressed blocks to use for meshing 347 snapsBlocks.all = chunkManager.getChunkSnapshots( 348 snapsPositions.all, BLOCK_LAYER, Yes.Uncompress); 349 350 AdjChunkLayers27 snapsEntities; 351 snapsEntities.all = chunkManager.getChunkSnapshots( 352 snapsPositions.all, ENTITY_LAYER, Yes.Uncompress); 353 354 AdjChunkLayers27 snapsMetadatas; 355 snapsMetadatas.all = chunkManager.getChunkSnapshots( 356 snapsPositions.all, METADATA_LAYER, Yes.Uncompress); 357 358 ChunkLayerItem[27] blockLayers; 359 ChunkLayerItem[27] entityLayers; 360 ChunkLayerItem[27] metadataLayers; 361 foreach(i; 0..27) 362 { 363 if (!dimBorders.contains(snapsPositions.all[i].ivector3)) // out-of-border chunk 364 { 365 blockLayers[i].metadata = solidity_metadatas[Solidity.solid]; 366 } 367 else 368 { 369 blockLayers[i] = ChunkLayerItem(snapsBlocks.all[i].get(), BLOCK_LAYER); 370 entityLayers[i] = ChunkLayerItem(snapsEntities.all[i].get(), ENTITY_LAYER); 371 metadataLayers[i] = ChunkLayerItem(snapsMetadatas.all[i].get(), METADATA_LAYER); 372 } 373 } 374 375 foreach(i, pos; snapsPositions.all) 376 { 377 blockLayers[i].timestamp = chunkManager.addCurrentSnapshotUser(pos, BLOCK_LAYER); 378 entityLayers[i].timestamp = chunkManager.addCurrentSnapshotUser(pos, ENTITY_LAYER); 379 metadataLayers[i].timestamp = chunkManager.addCurrentSnapshotUser(pos, METADATA_LAYER); 380 } 381 382 // debug 383 foreach (i, layer; blockLayers) { 384 import std..string : format; 385 assert(layer.type != StorageType.compressedArray, "[MESHING] Data needs to be uncompressed 1"); 386 if (!layer.isUniform) { 387 auto length = layer.getArray!ubyte.length; 388 if (length != BLOCKS_DATA_LENGTH) infof("Wrong length of %s: %s", snapsPositions.all[i], length); 389 assert(length == BLOCKS_DATA_LENGTH, format("Wrong length of %s: %s", snapsPositions.all[i], length)); 390 } 391 } 392 foreach (layer; entityLayers) 393 assert(layer.type != StorageType.compressedArray, "[MESHING] Data needs to be uncompressed 2"); 394 foreach (layer; metadataLayers) 395 assert(layer.type != StorageType.compressedArray, "[MESHING] Data needs to be uncompressed 3"); 396 397 // send mesh task 398 auto header = MeshGenTaskHeader(MeshGenTaskType.genMesh, currentMeshGroupId, cwp); 399 with(meshWorkers.nextWorker) { 400 taskQueue.startMessage(); 401 taskQueue.pushMessagePart(header); 402 taskQueue.pushMessagePart(blockLayers); 403 taskQueue.pushMessagePart(entityLayers); 404 taskQueue.pushMessagePart(metadataLayers); 405 taskQueue.endMessage(); 406 notify(); 407 } 408 409 return true; 410 } 411 412 void preloadMesh(ref MeshGenResult result, MeshVertex[][2] meshes) 413 { 414 ChunkWorldPos cwp = result.cwp; 415 if (!chunkManager.isChunkLoaded(cwp)) 416 return; 417 418 foreach(i, meshData; meshes) 419 { 420 if (meshData.length == 0) continue; 421 auto mesh = ChunkMesh(vec3(cwp.vector * CHUNK_SIZE)); 422 mesh.uploadMeshData(meshData); 423 uploadLimiter.onMeshPreloaded(meshData.length); 424 freeChunkMeshData(meshData); 425 result.meshes[i] = mesh; 426 } 427 } 428 429 void loadMeshData(MeshGenResult result) 430 { 431 ChunkWorldPos cwp = result.cwp; 432 if (!chunkManager.isChunkLoaded(cwp)) 433 { 434 version(DBG) tracef("loadMeshData %s chunk unloaded", cwp); 435 436 result.meshes[0].del(); 437 result.meshes[1].del(); 438 439 return; 440 } 441 442 // Attach mesh 443 bool hasMesh = false; 444 foreach(i, mesh; result.meshes) 445 { 446 unloadChunkSubmesh(cwp, i); 447 if (mesh.empty) { 448 mesh.del; 449 continue; 450 } 451 452 totalMeshDataBytes += mesh.uploadedBytes; 453 chunkMeshes[i][cwp] = mesh; 454 hasMesh = true; 455 } 456 457 ++totalMeshedChunks; 458 if (hasMesh) 459 { 460 ++totalMeshes; 461 } 462 else 463 { 464 version(DBG) tracef("loadMeshData %s no mesh", cwp); 465 466 static if (debug_wasted_meshes) 467 { 468 wastedMeshes[cwp] = 0; 469 } 470 } 471 } 472 473 void unloadChunkMesh(ChunkWorldPos cwp) 474 { 475 version(DBG) tracef("unloadChunkMesh %s", cwp); 476 foreach(i; 0..chunkMeshes.length) 477 { 478 unloadChunkSubmesh(cwp, i); 479 } 480 } 481 482 void unloadChunkSubmesh(ChunkWorldPos cwp, size_t index) 483 { 484 if (auto mesh = cwp in chunkMeshes[index]) 485 { 486 totalMeshDataBytes -= mesh.uploadedBytes; 487 mesh.del(); 488 chunkMeshes[index].remove(cwp); 489 } 490 } 491 }