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