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.world.storage.chunkmanager; 7 8 import std.experimental.logger; 9 import std.typecons : Nullable; 10 import std.string : format; 11 public import std.typecons : Flag, Yes, No; 12 13 import voxelman.block.utils; 14 import voxelman.core.config; 15 import voxelman.world.storage.chunk; 16 import voxelman.world.storage.coordinates : ChunkWorldPos, adjacentPositions; 17 import voxelman.world.storage.utils; 18 import voxelman.container.hashset; 19 import voxelman.world.storage.chunkprovider; 20 21 22 private enum ChunkState { 23 non_loaded, 24 added_loaded, 25 removed_loading, 26 added_loading, 27 removed_loaded_used 28 } 29 30 private enum traceStateStr = q{ 31 //infof("state @%s %s => %s", cwp, state, 32 // chunkStates.get(cwp, ChunkState.non_loaded)); 33 }; 34 35 struct ChunkSnapWithAdjacent 36 { 37 union 38 { 39 ChunkWorldPos[7] positions; 40 struct 41 { 42 ChunkWorldPos[6] adjacentPositions; 43 ChunkWorldPos centralPosition; 44 } 45 } 46 union 47 { 48 Nullable!ChunkLayerSnap[7] snapshots; 49 struct 50 { 51 Nullable!ChunkLayerSnap[6] adjacentSnapshots; 52 Nullable!ChunkLayerSnap centralSnapshot; 53 } 54 } 55 bool allLoaded = true; 56 } 57 58 ChunkSnapWithAdjacent getSnapWithAdjacent(ChunkManager cm, ChunkWorldPos cwp, ubyte layer) 59 { 60 ChunkSnapWithAdjacent result; 61 62 result.centralSnapshot = cm.getChunkSnapshot(cwp, layer); 63 result.centralPosition = cwp; 64 65 result.adjacentPositions = adjacentPositions(cwp); 66 67 result.allLoaded = !result.centralSnapshot.isNull(); 68 foreach(i, pos; result.adjacentPositions) 69 { 70 result.adjacentSnapshots[i] = cm.getChunkSnapshot(pos, layer); 71 result.allLoaded = result.allLoaded && !result.adjacentSnapshots[i].isNull(); 72 } 73 74 return result; 75 } 76 77 //version = TRACE_SNAP_USERS; 78 //version = DBG_OUT; 79 //version = DBG_COMPR; 80 81 enum WriteBufferPolicy 82 { 83 createUniform, 84 copySnapshotArray, 85 } 86 87 final class ChunkManager { 88 void delegate(ChunkWorldPos)[] onChunkAddedHandlers; 89 void delegate(ChunkWorldPos)[] onChunkRemovedHandlers; 90 void delegate(ChunkWorldPos) onChunkLoadedHandler; 91 92 void delegate(ChunkWorldPos) loadChunkHandler; 93 94 // Used on server only 95 size_t delegate() startChunkSave; 96 void delegate(ChunkLayerItem layer) pushLayer; 97 void delegate(size_t headerPos, ChunkHeaderItem header) endChunkSave; 98 99 bool isLoadCancelingEnabled = false; /// Set to true on client to cancel load on unload 100 bool isChunkSavingEnabled = true; 101 long totalLayerDataBytes; // debug 102 103 private ChunkLayerSnap[ChunkWorldPos][] snapshots; 104 private ChunkLayerSnap[TimestampType][ChunkWorldPos][] oldSnapshots; 105 private WriteBuffer[ChunkWorldPos][] writeBuffers; 106 private ChunkState[ChunkWorldPos] chunkStates; 107 private HashSet!ChunkWorldPos modifiedChunks; 108 private size_t[ChunkWorldPos] numInternalChunkObservers; 109 private size_t[ChunkWorldPos] numExternalChunkObservers; 110 // total number of snapshot users of all 'snapshots' 111 // used to change state from added_loaded to removed_loaded_used 112 private size_t[ChunkWorldPos] totalSnapshotUsers; 113 ubyte numLayers; 114 115 void setup(ubyte _numLayers) { 116 numLayers = _numLayers; 117 snapshots.length = numLayers; 118 oldSnapshots.length = numLayers; 119 writeBuffers.length = numLayers; 120 } 121 122 /// Performs save of all modified chunks. 123 /// Modified chunks are those that were committed. 124 /// Perform save right after commit. 125 void save() { 126 foreach(cwp; modifiedChunks.items) { 127 saveChunk(cwp); 128 } 129 clearModifiedChunks(); 130 } 131 132 /// Used on client to clear modified chunks instead of saving them. 133 void clearModifiedChunks() 134 { 135 modifiedChunks.clear(); 136 } 137 138 HashSet!ChunkWorldPos getModifiedChunks() 139 { 140 return modifiedChunks; 141 } 142 143 bool isChunkLoaded(ChunkWorldPos cwp) 144 { 145 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 146 return state == ChunkState.added_loaded; 147 } 148 149 bool isChunkAdded(ChunkWorldPos cwp) 150 { 151 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 152 with(ChunkState) { 153 return state == added_loaded || state == added_loading; 154 } 155 } 156 157 /// Sets number of users of chunk at cwp. 158 /// If total chunk users if greater than zero, then chunk is loaded, 159 /// if equal to zero, chunk will be unloaded. 160 void setExternalChunkObservers(ChunkWorldPos cwp, size_t numExternalObservers) { 161 numExternalChunkObservers[cwp] = numExternalObservers; 162 if (numExternalObservers == 0) 163 numExternalChunkObservers.remove(cwp); 164 setChunkTotalObservers(cwp, numInternalChunkObservers.get(cwp, 0) + numExternalObservers); 165 } 166 167 /// returned value isNull if chunk is not loaded/added 168 /// If uncompress is Yes then tries to convert snapshot to uncompressed. 169 /// If has users, then compressed snapshot is returned. 170 Nullable!ChunkLayerSnap getChunkSnapshot(ChunkWorldPos cwp, ubyte layer, Flag!"Uncompress" uncompress = Flag!"Uncompress".no) { 171 if (isChunkLoaded(cwp)) 172 { 173 auto snap = cwp in snapshots[layer]; 174 if (snap) 175 { 176 if (snap.type == StorageType.compressedArray && uncompress) 177 { 178 ubyte[] decompressedData = decompressLayerData((*snap).getArray!ubyte); 179 if (snap.numUsers == 0) { 180 recycleSnapshotMemory(*snap); 181 snap.dataPtr = decompressedData.ptr; 182 snap.dataLength = cast(LayerDataLenType)decompressedData.length; 183 snap.type = StorageType.fullArray; 184 } 185 else 186 { 187 ChunkLayerSnap res = *snap; 188 res.dataPtr = decompressedData.ptr; 189 res.dataLength = cast(LayerDataLenType)decompressedData.length; 190 res.type = StorageType.fullArray; 191 return Nullable!ChunkLayerSnap(res); 192 } 193 } 194 auto res = Nullable!ChunkLayerSnap(*snap); 195 return res; 196 } 197 else 198 { 199 return Nullable!ChunkLayerSnap(ChunkLayerSnap.init); 200 } 201 } 202 203 auto res = Nullable!ChunkLayerSnap.init; 204 return res; 205 } 206 207 /// Returns writeable copy of current chunk snapshot. 208 /// This buffer is valid until commit. 209 /// After commit this buffer becomes next immutable snapshot. 210 /// Returns null if chunk is not added and/or not loaded. 211 /// If write buffer was not yet created then it is created based on policy. 212 /// BUG: returned pointer points inside hash table. 213 /// If new write buffer is added hash table can reallocate. 214 /// Do not use more than one write buffer at a time. 215 /// Reallocation can prevent changes to buffers obtained earlier than reallocation to be invisible. 216 WriteBuffer* getOrCreateWriteBuffer(ChunkWorldPos cwp, ubyte layer, 217 WriteBufferPolicy policy = WriteBufferPolicy.createUniform) 218 { 219 if (!isChunkLoaded(cwp)) return null; 220 auto writeBuffer = cwp in writeBuffers[layer]; 221 if (writeBuffer is null) { 222 writeBuffer = createWriteBuffer(cwp, layer); 223 if (writeBuffer && policy == WriteBufferPolicy.copySnapshotArray) { 224 auto old = getChunkSnapshot(cwp, layer); 225 if (!old.isNull) { 226 applyLayer(old, writeBuffer.layer); 227 } 228 } 229 } 230 return writeBuffer; 231 } 232 233 /// Returns timestamp of current chunk snapshot. 234 /// Store this timestamp to use in removeSnapshotUser 235 TimestampType addCurrentSnapshotUser(ChunkWorldPos cwp, ubyte layer) { 236 auto snap = cwp in snapshots[layer]; 237 assert(snap, "Cannot add chunk user. No such snapshot."); 238 239 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 240 assert(state == ChunkState.added_loaded, 241 format("To add user chunk must be both added and loaded, not %s", state)); 242 243 totalSnapshotUsers[cwp] = totalSnapshotUsers.get(cwp, 0) + 1; 244 245 ++snap.numUsers; 246 version(TRACE_SNAP_USERS) tracef("#%s:%s (add cur:+1) %s/%s @%s", cwp, layer, snap.numUsers, totalSnapshotUsers[cwp], snap.timestamp); 247 return snap.timestamp; 248 } 249 250 /// Generic removal of snapshot user. Removes chunk if numUsers == 0. 251 /// Use this to remove added snapshot user. Use timestamp returned from addCurrentSnapshotUser. 252 void removeSnapshotUser(ChunkWorldPos cwp, TimestampType timestamp, ubyte layer) { 253 auto snap = cwp in snapshots[layer]; 254 if (snap && snap.timestamp == timestamp) 255 { 256 auto totalUsersLeft = removeCurrentSnapshotUser(cwp, layer); 257 if (totalUsersLeft == 0) 258 { 259 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 260 assert(state == ChunkState.added_loaded || state == ChunkState.removed_loaded_used); 261 if (state == ChunkState.removed_loaded_used) 262 { 263 chunkStates[cwp] = ChunkState.non_loaded; 264 clearChunkData(cwp); 265 } 266 } 267 } 268 else 269 { 270 removeOldSnapshotUser(cwp, timestamp, layer); 271 } 272 } 273 274 /// Internal. Called by code which loads chunks from storage. 275 /// LoadedChunk is a type that has following memeber: 276 /// ChunkHeaderItem getHeader() 277 /// ChunkLayerItem getLayer() 278 void onSnapshotLoaded(LoadedChunk)(LoadedChunk chunk, bool needsSave) { 279 ChunkHeaderItem header = chunk.getHeader(); 280 281 version(DBG_OUT)infof("res loaded %s", header.cwp); 282 283 foreach(_; 0..header.numLayers) 284 { 285 ChunkLayerItem layer = chunk.getLayer(); 286 totalLayerDataBytes += getLayerDataBytes(layer); 287 288 snapshots[layer.layerId][header.cwp] = ChunkLayerSnap(layer); 289 version(DBG_COMPR)if (layer.type == StorageType.compressedArray) 290 infof("CM Loaded %s %s %s\n(%(%02x%))", header.cwp, layer.dataPtr, layer.dataLength, layer.getArray!ubyte); 291 } 292 293 auto cwp = header.cwp; 294 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 295 296 with(ChunkState) final switch(state) 297 { 298 case non_loaded: 299 if (isLoadCancelingEnabled) { 300 clearChunkData(cwp); 301 } else { 302 assert(false, "On loaded should not occur for non-loading chunk"); 303 } 304 break; 305 case added_loaded: 306 assert(false, "On loaded should not occur for already loaded chunk"); 307 case removed_loading: 308 if (!needsSave || !isChunkSavingEnabled) { 309 chunkStates[cwp] = non_loaded; 310 clearChunkData(cwp); 311 } else { 312 assert(!isLoadCancelingEnabled, "Should happen only when isLoadCancelingEnabled is false"); 313 chunkStates[cwp] = added_loaded; 314 saveChunk(cwp); 315 chunkStates[cwp] = removed_loaded_used; 316 } 317 break; 318 case added_loading: 319 chunkStates[cwp] = added_loaded; 320 if (needsSave) modifiedChunks.put(cwp); 321 notifyLoaded(cwp); 322 break; 323 case removed_loaded_used: 324 assert(false, "On loaded should not occur for already loaded chunk"); 325 } 326 mixin(traceStateStr); 327 } 328 329 /// Internal. Called by code which saves chunks to storage. 330 /// SavedChunk is a type that has following memeber: 331 /// ChunkHeaderItem getHeader() 332 /// ChunkLayerTimestampItem getLayerTimestamp() 333 void onSnapshotSaved(SavedChunk)(SavedChunk chunk) { 334 ChunkHeaderItem header = chunk.getHeader(); 335 version(DBG_OUT)infof("res saved %s", header.cwp); 336 337 auto cwp = header.cwp; 338 auto state = chunkStates.get(header.cwp, ChunkState.non_loaded); 339 foreach(i; 0..header.numLayers) 340 { 341 ChunkLayerTimestampItem layer = chunk.getLayerTimestamp(); 342 // will delete current chunk when totalUsersLeft becomes 0 and is removed 343 removeSnapshotUser(header.cwp, layer.timestamp, layer.layerId); 344 } 345 mixin(traceStateStr); 346 } 347 348 /// called at the end of tick 349 void commitSnapshots(TimestampType currentTime) { 350 foreach(ubyte layer; 0..numLayers) 351 { 352 auto writeBuffersCopy = writeBuffers[layer]; 353 // Clear it here because commit can unload chunk. 354 // And unload asserts that chunk is not in writeBuffers. 355 writeBuffers[layer] = null; 356 foreach(snapshot; writeBuffersCopy.byKeyValue) 357 { 358 auto cwp = snapshot.key; 359 WriteBuffer writeBuffer = snapshot.value; 360 if (writeBuffer.isModified) 361 { 362 modifiedChunks.put(cwp); 363 commitLayerSnapshot(cwp, writeBuffer, currentTime, layer); 364 } 365 else 366 { 367 if (!writeBuffer.isUniform) { 368 freeLayerArray(writeBuffer.layer); 369 } 370 } 371 removeInternalObserver(cwp); // remove user added in createWriteBuffer 372 } 373 } 374 } 375 376 // PPPPPP RRRRRR IIIII VV VV AAA TTTTTTT EEEEEEE 377 // PP PP RR RR III VV VV AAAAA TTT EE 378 // PPPPPP RRRRRR III VV VV AA AA TTT EEEEE 379 // PP RR RR III VV VV AAAAAAA TTT EE 380 // PP RR RR IIIII VVV AA AA TTT EEEEEEE 381 // 382 383 private void notifyAdded(ChunkWorldPos cwp) { 384 foreach(handler; onChunkAddedHandlers) 385 handler(cwp); 386 } 387 388 private void notifyRemoved(ChunkWorldPos cwp) { 389 foreach(handler; onChunkRemovedHandlers) 390 handler(cwp); 391 } 392 393 private void notifyLoaded(ChunkWorldPos cwp) { 394 if (onChunkLoadedHandler) onChunkLoadedHandler(cwp); 395 } 396 397 private void saveChunk(ChunkWorldPos cwp) 398 { 399 assert(startChunkSave, "startChunkSave is null"); 400 assert(pushLayer, "pushLayer is null"); 401 assert(endChunkSave, "endChunkSave is null"); 402 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 403 assert(state == ChunkState.added_loaded, "Save should only occur for added_loaded chunks"); 404 405 size_t headerPos = startChunkSave(); 406 // code lower does work of addCurrentSnapshotUsers too 407 ubyte numChunkLayers; 408 foreach(ubyte layerId; 0..numLayers) 409 { 410 if (auto snap = cwp in snapshots[layerId]) 411 { 412 ++numChunkLayers; 413 ++snap.numUsers; // in case new snapshot replaces current one, we need to keep it while it is saved 414 pushLayer(ChunkLayerItem(*snap, layerId)); 415 } 416 } 417 totalSnapshotUsers[cwp] = totalSnapshotUsers.get(cwp, 0) + numChunkLayers; 418 419 endChunkSave(headerPos, ChunkHeaderItem(cwp, numChunkLayers, 0)); 420 } 421 422 // Puts chunk in added state requesting load if needed. 423 // Notifies on add. Notifies on load if loaded. 424 private void loadChunk(ChunkWorldPos cwp) { 425 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 426 with(ChunkState) final switch(state) { 427 case non_loaded: 428 chunkStates[cwp] = added_loading; 429 loadChunkHandler(cwp); 430 notifyAdded(cwp); 431 break; 432 case added_loaded: 433 break; // ignore 434 case removed_loading: 435 chunkStates[cwp] = added_loading; 436 notifyAdded(cwp); 437 break; 438 case added_loading: 439 break; // ignore 440 case removed_loaded_used: 441 chunkStates[cwp] = added_loaded; 442 notifyAdded(cwp); 443 notifyLoaded(cwp); 444 break; 445 } 446 mixin(traceStateStr); 447 } 448 449 // Puts chunk in removed state requesting save if needed. 450 // Notifies on remove. 451 private void unloadChunk(ChunkWorldPos cwp) { 452 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 453 notifyRemoved(cwp); 454 with(ChunkState) final switch(state) { 455 case non_loaded: 456 assert(false, "Unload should not occur when chunk was not yet loaded"); 457 case added_loaded: 458 if(cwp in modifiedChunks) 459 { 460 modifiedChunks.remove(cwp); 461 if (isChunkSavingEnabled) 462 { 463 saveChunk(cwp); 464 chunkStates[cwp] = removed_loaded_used; 465 } 466 } 467 else 468 { // state 0 469 auto totalUsersLeft = totalSnapshotUsers.get(cwp, 0); 470 if (totalUsersLeft == 0) 471 { 472 chunkStates[cwp] = non_loaded; 473 modifiedChunks.remove(cwp); 474 clearChunkData(cwp); 475 } 476 else 477 { 478 chunkStates[cwp] = removed_loaded_used; 479 } 480 } 481 break; 482 case removed_loading: 483 assert(false, "Unload should not occur when chunk is already removed"); 484 case added_loading: 485 if (isLoadCancelingEnabled) 486 { 487 chunkStates[cwp] = non_loaded; 488 clearChunkData(cwp); 489 } 490 else 491 { 492 chunkStates[cwp] = removed_loading; 493 } 494 break; 495 case removed_loaded_used: 496 assert(false, "Unload should not occur when chunk is already removed"); 497 } 498 mixin(traceStateStr); 499 } 500 501 // Fully removes chunk 502 private void clearChunkData(ChunkWorldPos cwp) { 503 foreach(layer; 0..numLayers) 504 { 505 if (auto snap = cwp in snapshots[layer]) 506 { 507 recycleSnapshotMemory(*snap); 508 snapshots[layer].remove(cwp); 509 assert(cwp !in writeBuffers[layer]); 510 } 511 assert(cwp !in totalSnapshotUsers); 512 } 513 assert(cwp !in modifiedChunks); 514 chunkStates.remove(cwp); 515 } 516 517 // Creates write buffer for writing changes in it. 518 // Latest snapshot's data is not copied in it. 519 // Write buffer is then avaliable through getWriteBuffer/getOrCreateWriteBuffer. 520 // On commit stage WB is moved into new snapshot if write buffer was modified. 521 // Adds internal user that is removed on commit to prevent chunk from unloading with uncommitted changes. 522 // Returns pointer to created write buffer. 523 private WriteBuffer* createWriteBuffer(ChunkWorldPos cwp, ubyte layer) { 524 assert(cwp !in writeBuffers[layer]); 525 auto wb = WriteBuffer.init; 526 wb.layer.layerId = layer; 527 writeBuffers[layer][cwp] = wb; 528 addInternalObserver(cwp); // prevent unload until commit 529 return cwp in writeBuffers[layer]; 530 } 531 532 // Here comes sum of all internal and external chunk users which results in loading or unloading of specific chunk. 533 private void setChunkTotalObservers(ChunkWorldPos cwp, size_t totalObservers) { 534 if (totalObservers > 0) { 535 loadChunk(cwp); 536 } else { 537 unloadChunk(cwp); 538 } 539 } 540 541 // Used inside chunk manager to add chunk users, to prevent chunk unloading. 542 private void addInternalObserver(ChunkWorldPos cwp) { 543 numInternalChunkObservers[cwp] = numInternalChunkObservers.get(cwp, 0) + 1; 544 auto totalObservers = numInternalChunkObservers[cwp] + numExternalChunkObservers.get(cwp, 0); 545 setChunkTotalObservers(cwp, totalObservers); 546 } 547 548 // Used inside chunk manager to remove chunk users. 549 private void removeInternalObserver(ChunkWorldPos cwp) { 550 auto numObservers = numInternalChunkObservers.get(cwp, 0); 551 assert(numObservers > 0, "numInternalChunkObservers is zero when removing internal user"); 552 --numObservers; 553 if (numObservers == 0) 554 numInternalChunkObservers.remove(cwp); 555 else 556 numInternalChunkObservers[cwp] = numObservers; 557 auto totalObservers = numObservers + numExternalChunkObservers.get(cwp, 0); 558 setChunkTotalObservers(cwp, totalObservers); 559 } 560 561 // Returns number of current snapshot users left. 562 private size_t removeCurrentSnapshotUser(ChunkWorldPos cwp, ubyte layer) { 563 auto snap = cwp in snapshots[layer]; 564 assert(snap && snap.numUsers > 0, "cannot remove chunk user. Snapshot has 0 users"); 565 566 auto totalUsers = cwp in totalSnapshotUsers; 567 assert(totalUsers && (*totalUsers) > 0, "cannot remove chunk user. Snapshot has 0 users"); 568 569 --snap.numUsers; 570 --(*totalUsers); 571 version(TRACE_SNAP_USERS) tracef("#%s:%s (rem cur:-1) %s/%s @%s", cwp, layer, snap.numUsers, totalSnapshotUsers.get(cwp, 0), snap.timestamp); 572 573 if ((*totalUsers) == 0) { 574 totalSnapshotUsers.remove(cwp); 575 return 0; 576 } 577 578 return (*totalUsers); 579 } 580 581 // Snapshot is removed from oldSnapshots if numUsers == 0. 582 private void removeOldSnapshotUser(ChunkWorldPos cwp, TimestampType timestamp, ubyte layer) { 583 ChunkLayerSnap[TimestampType]* chunkSnaps = cwp in oldSnapshots[layer]; 584 version(TRACE_SNAP_USERS) tracef("#%s:%s (rem old) x/%s @%s", cwp, layer, totalSnapshotUsers.get(cwp, 0), timestamp); 585 assert(chunkSnaps, "old snapshot should have waited for releasing user"); 586 ChunkLayerSnap* snapshot = timestamp in *chunkSnaps; 587 assert(snapshot, "cannot release snapshot user. No such snapshot"); 588 assert(snapshot.numUsers > 0, "cannot remove chunk user. Snapshot has 0 users"); 589 --snapshot.numUsers; 590 version(TRACE_SNAP_USERS) tracef("#%s:%s (rem old:-1) %s/%s @%s", cwp, layer, snapshot.numUsers, totalSnapshotUsers.get(cwp, 0), timestamp); 591 if (snapshot.numUsers == 0) { 592 (*chunkSnaps).remove(timestamp); 593 if ((*chunkSnaps).length == 0) { // all old snaps of one chunk released 594 oldSnapshots[layer].remove(cwp); 595 } 596 recycleSnapshotMemory(*snapshot); 597 } 598 } 599 600 // Commit for single chunk. 601 private void commitLayerSnapshot(ChunkWorldPos cwp, WriteBuffer writeBuffer, TimestampType currentTime, ubyte layer) { 602 auto currentSnapshot = getChunkSnapshot(cwp, layer); 603 if (!currentSnapshot.isNull) handleCurrentSnapCommit(cwp, layer, currentSnapshot.get()); 604 605 assert(writeBuffer.isModified); 606 if (writeBuffer.isUniform) { 607 if (writeBuffer.layer == ChunkLayerItem.init) { 608 snapshots[layer].remove(cwp); 609 } else { 610 snapshots[layer][cwp] = ChunkLayerSnap(StorageType.uniform, writeBuffer.layer.dataLength, 611 currentTime, writeBuffer.layer.uniformData, writeBuffer.layer.metadata); 612 } 613 } else { 614 assert(writeBuffer.layer.type == StorageType.fullArray); 615 snapshots[layer][cwp] = ChunkLayerSnap(StorageType.fullArray, currentTime, 616 writeBuffer.getArray!ubyte, writeBuffer.layer.metadata); 617 totalLayerDataBytes += getLayerDataBytes(writeBuffer.layer); 618 } 619 620 assert(isChunkLoaded(cwp), "Commit is only possible for loaded chunk"); 621 } 622 623 void handleCurrentSnapCommit(ChunkWorldPos cwp, ubyte layer, ChunkLayerSnap currentSnapshot) 624 { 625 if (currentSnapshot.numUsers == 0) { 626 version(TRACE_SNAP_USERS) tracef("#%s:%s (commit:%s) %s/%s @%s", cwp, layer, currentSnapshot.numUsers, 0, totalSnapshotUsers.get(cwp, 0), currentTime); 627 recycleSnapshotMemory(currentSnapshot); 628 } else { 629 // transfer users from current layer snapshot into old snapshot 630 auto totalUsers = cwp in totalSnapshotUsers; 631 assert(totalUsers && (*totalUsers) >= currentSnapshot.numUsers, "layer has not enough users"); 632 (*totalUsers) -= currentSnapshot.numUsers; 633 if ((*totalUsers) == 0) { 634 totalSnapshotUsers.remove(cwp); 635 } 636 637 if (auto layerSnaps = cwp in oldSnapshots[layer]) { 638 version(TRACE_SNAP_USERS) tracef("#%s:%s (commit add:%s) %s/%s @%s", cwp, layer, 639 currentSnapshot.numUsers, 0, totalSnapshotUsers.get(cwp, 0), currentTime); 640 assert(currentSnapshot.timestamp !in *layerSnaps); 641 (*layerSnaps)[currentSnapshot.timestamp] = currentSnapshot; 642 } else { 643 version(TRACE_SNAP_USERS) tracef("#%s:%s (commit new:%s) %s/%s @%s", cwp, layer, 644 currentSnapshot.numUsers, 0, totalSnapshotUsers.get(cwp, 0), currentTime); 645 oldSnapshots[layer][cwp] = [currentSnapshot.timestamp : currentSnapshot]; 646 version(TRACE_SNAP_USERS) tracef("oldSnapshots[%s][%s] == %s", layer, cwp, oldSnapshots[layer][cwp]); 647 } 648 } 649 } 650 651 // Called when snapshot data can be recycled. 652 private void recycleSnapshotMemory(ref ChunkLayerSnap snap) { 653 totalLayerDataBytes -= getLayerDataBytes(snap); 654 if (snap.type != StorageType.uniform) { 655 freeLayerArray(snap); 656 } 657 } 658 } 659 660 661 // TTTTTTT EEEEEEE SSSSS TTTTTTT SSSSS 662 // TTT EE SS TTT SS 663 // TTT EEEEE SSSSS TTT SSSSS 664 // TTT EE SS TTT SS 665 // TTT EEEEEEE SSSSS TTT SSSSS 666 // 667 668 version(unittest) { 669 enum ZERO_CWP = ChunkWorldPos(0, 0, 0, 0); 670 671 private struct Handlers { 672 void setup(ChunkManager cm) { 673 cm.onChunkAddedHandlers ~= &onChunkAddedHandler; 674 cm.onChunkRemovedHandlers ~= &onChunkRemovedHandler; 675 cm.onChunkLoadedHandler = &onChunkLoadedHandler; 676 cm.loadChunkHandler = &loadChunkHandler; 677 678 cm.startChunkSave = &startChunkSave; 679 cm.pushLayer = &pushLayer; 680 cm.endChunkSave = &endChunkSave; 681 } 682 void onChunkAddedHandler(ChunkWorldPos) { 683 onChunkAddedHandlerCalled = true; 684 } 685 void onChunkRemovedHandler(ChunkWorldPos) { 686 onChunkRemovedHandlerCalled = true; 687 } 688 void onChunkLoadedHandler(ChunkWorldPos) { 689 onChunkLoadedHandlerCalled = true; 690 } 691 void loadChunkHandler(ChunkWorldPos cwp) { 692 loadChunkHandlerCalled = true; 693 } 694 size_t startChunkSave() { 695 saveChunkHandlerCalled = true; 696 return 0; 697 } 698 void pushLayer(ChunkLayerItem layer) {} 699 void endChunkSave(size_t headerPos, ChunkHeaderItem header) {} 700 void assertCalled(size_t flags) { 701 assert(!(((flags & 0b0000_0001) > 0) ^ onChunkAddedHandlerCalled)); 702 assert(!(((flags & 0b0000_0010) > 0) ^ onChunkRemovedHandlerCalled)); 703 assert(!(((flags & 0b0000_0100) > 0) ^ onChunkLoadedHandlerCalled)); 704 assert(!(((flags & 0b0001_0000) > 0) ^ loadChunkHandlerCalled)); 705 assert(!(((flags & 0b0010_0000) > 0) ^ saveChunkHandlerCalled)); 706 } 707 708 bool onChunkAddedHandlerCalled; 709 bool onChunkRemovedHandlerCalled; 710 bool onChunkLoadedHandlerCalled; 711 bool loadChunkHandlerCalled; 712 bool saveChunkHandlerCalled; 713 } 714 715 private struct TestLoadedChunkData 716 { 717 ChunkHeaderItem getHeader() { return ChunkHeaderItem(ZERO_CWP, 1, 0); } 718 ChunkLayerItem getLayer() { return ChunkLayerItem(); } 719 } 720 721 private struct TestSavedChunkData 722 { 723 TimestampType timestamp; 724 ChunkHeaderItem getHeader() { 725 return ChunkHeaderItem(ChunkWorldPos(0, 0, 0, 0), 1); 726 } 727 ChunkLayerTimestampItem getLayerTimestamp() { 728 return ChunkLayerTimestampItem(timestamp, 0); 729 } 730 } 731 732 private struct FSMTester { 733 auto ZERO_CWP = ChunkWorldPos(0, 0, 0, 0); 734 auto currentState(ChunkManager cm) { 735 return cm.chunkStates.get(ZERO_CWP, ChunkState.non_loaded); 736 } 737 void resetChunk(ChunkManager cm) { 738 foreach(layer; 0..cm.numLayers) { 739 cm.snapshots[layer].remove(ZERO_CWP); 740 cm.oldSnapshots[layer].remove(ZERO_CWP); 741 cm.writeBuffers[layer].remove(ZERO_CWP); 742 } 743 cm.chunkStates.remove(ZERO_CWP); 744 cm.modifiedChunks.remove(ZERO_CWP); 745 cm.numInternalChunkObservers.remove(ZERO_CWP); 746 cm.numExternalChunkObservers.remove(ZERO_CWP); 747 cm.totalSnapshotUsers.remove(ZERO_CWP); 748 } 749 void gotoState(ChunkManager cm, ChunkState state) { 750 resetChunk(cm); 751 with(ChunkState) final switch(state) { 752 case non_loaded: 753 break; 754 case added_loaded: 755 cm.setExternalChunkObservers(ZERO_CWP, 1); 756 cm.onSnapshotLoaded(TestLoadedChunkData(), false); 757 break; 758 case removed_loading: 759 cm.setExternalChunkObservers(ZERO_CWP, 1); 760 cm.setExternalChunkObservers(ZERO_CWP, 0); 761 break; 762 case added_loading: 763 cm.setExternalChunkObservers(ZERO_CWP, 1); 764 break; 765 //case removed_loaded_saving: 766 // gotoState(cm, ChunkState.added_loaded_saving); 767 // cm.setExternalChunkObservers(ZERO_CWP, 0); 768 // break; 769 case removed_loaded_used: 770 gotoState(cm, ChunkState.added_loaded); 771 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 772 cm.commitSnapshots(1); 773 TimestampType timestamp = cm.addCurrentSnapshotUser(ZERO_CWP, FIRST_LAYER); 774 cm.save(); 775 cm.setExternalChunkObservers(ZERO_CWP, 0); 776 cm.onSnapshotSaved(TestSavedChunkData(timestamp)); 777 break; 778 //case added_loaded_saving: 779 // gotoState(cm, ChunkState.added_loaded); 780 // cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER); 781 // cm.commitSnapshots(1); 782 // cm.save(); 783 // break; 784 } 785 import std.string : format; 786 assert(currentState(cm) == state, 787 format("Failed to set state %s, got %s", state, currentState(cm))); 788 } 789 790 void gotoStateSaving(ChunkManager cm, ChunkState state) 791 { 792 if (state == ChunkState.added_loaded) 793 { 794 gotoState(cm, ChunkState.added_loaded); 795 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 796 cm.commitSnapshots(1); 797 cm.save(); 798 } 799 else if (state == ChunkState.removed_loaded_used) 800 { 801 gotoStateSaving(cm, ChunkState.added_loaded); 802 cm.setExternalChunkObservers(ZERO_CWP, 0); 803 } 804 assert(currentState(cm) == state, 805 format("Failed to set state %s, got %s", state, currentState(cm))); 806 } 807 } 808 } 809 810 811 unittest { 812 import voxelman.utils.log : setupLogger; 813 setupLogger("test.log"); 814 815 Handlers h; 816 ChunkManager cm; 817 FSMTester fsmTester; 818 819 void assertState(ChunkState state) { 820 import std.string : format; 821 auto actualState = fsmTester.currentState(cm); 822 assert(actualState == state, 823 format("Got state '%s', while needed '%s'", actualState, state)); 824 } 825 826 void assertHasOldSnapshot(TimestampType timestamp) { 827 assert(timestamp in cm.oldSnapshots[FIRST_LAYER][ZERO_CWP]); 828 } 829 830 void assertNoOldSnapshots() { 831 assert(ZERO_CWP !in cm.oldSnapshots[FIRST_LAYER]); 832 } 833 834 void assertHasSnapshot() { 835 assert(!cm.getChunkSnapshot(ZERO_CWP, FIRST_LAYER).isNull); 836 } 837 838 void assertHasNoSnapshot() { 839 assert( cm.getChunkSnapshot(ZERO_CWP, FIRST_LAYER).isNull); 840 } 841 842 void resetHandlersState() { 843 h = Handlers.init; 844 } 845 void resetChunkManager() { 846 cm = new ChunkManager; 847 ubyte numLayers = 1; 848 cm.setup(numLayers); 849 h.setup(cm); 850 } 851 void reset() { 852 resetHandlersState(); 853 resetChunkManager(); 854 } 855 856 void setupState(ChunkState state) { 857 fsmTester.gotoState(cm, state); 858 resetHandlersState(); 859 assertState(state); 860 } 861 862 void setupStateSaving(ChunkState state) { 863 fsmTester.gotoStateSaving(cm, state); 864 resetHandlersState(); 865 assertState(state); 866 } 867 868 reset(); 869 870 //-------------------------------------------------------------------------- 871 // non_loaded -> added_loading 872 cm.setExternalChunkObservers(ZERO_CWP, 1); 873 assertState(ChunkState.added_loading); 874 assertHasNoSnapshot(); 875 h.assertCalled(0b0001_0001); //onChunkAddedHandlerCalled, loadChunkHandlerCalled 876 877 878 //-------------------------------------------------------------------------- 879 setupState(ChunkState.added_loading); 880 // added_loading -> removed_loading 881 cm.setExternalChunkObservers(ZERO_CWP, 0); 882 assertState(ChunkState.removed_loading); 883 assertHasNoSnapshot(); 884 h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled 885 886 887 //-------------------------------------------------------------------------- 888 setupState(ChunkState.removed_loading); 889 // removed_loading -> added_loading 890 cm.setExternalChunkObservers(ZERO_CWP, 1); 891 assertState(ChunkState.added_loading); 892 assertHasNoSnapshot(); 893 h.assertCalled(0b0000_0001); //onChunkAddedHandlerCalled 894 895 896 //-------------------------------------------------------------------------- 897 setupState(ChunkState.removed_loading); 898 // removed_loading -> non_loaded 899 cm.onSnapshotLoaded(TestLoadedChunkData(), false); 900 assertState(ChunkState.non_loaded); 901 assertHasNoSnapshot(); 902 h.assertCalled(0b0000_0000); 903 904 //-------------------------------------------------------------------------- 905 setupState(ChunkState.added_loading); 906 // added_loading -> added_loaded + modified 907 cm.onSnapshotLoaded(TestLoadedChunkData(), true); 908 assertState(ChunkState.added_loaded); 909 assertHasSnapshot(); 910 h.assertCalled(0b0000_0100); //onChunkLoadedHandlerCalled 911 assert(ZERO_CWP in cm.modifiedChunks); 912 913 //-------------------------------------------------------------------------- 914 setupState(ChunkState.added_loading); 915 // added_loading -> added_loaded 916 cm.onSnapshotLoaded(TestLoadedChunkData(), false); 917 assertState(ChunkState.added_loaded); 918 assertHasSnapshot(); 919 h.assertCalled(0b0000_0100); //onChunkLoadedHandlerCalled 920 assert(ZERO_CWP !in cm.modifiedChunks); 921 922 //-------------------------------------------------------------------------- 923 setupState(ChunkState.added_loaded); 924 // added_loaded -> non_loaded 925 cm.setExternalChunkObservers(ZERO_CWP, 0); 926 assertState(ChunkState.non_loaded); 927 h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled 928 929 //-------------------------------------------------------------------------- 930 setupState(ChunkState.added_loaded); 931 // added_loaded -> removed_loaded_used 932 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 933 cm.commitSnapshots(TimestampType(1)); 934 cm.setExternalChunkObservers(ZERO_CWP, 0); 935 assertState(ChunkState.removed_loaded_used); 936 h.assertCalled(0b0010_0010); //onChunkRemovedHandlerCalled, loadChunkHandlerCalled 937 938 //-------------------------------------------------------------------------- 939 setupState(ChunkState.added_loaded); 940 // added_loaded -> added_loaded 941 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 942 cm.commitSnapshots(TimestampType(1)); 943 cm.save(); 944 assertState(ChunkState.added_loaded); 945 h.assertCalled(0b0010_0000); //loadChunkHandlerCalled 946 947 //-------------------------------------------------------------------------- 948 setupState(ChunkState.added_loaded); 949 // added_loaded with user -> added_loaded no user after commit 950 cm.addCurrentSnapshotUser(ZERO_CWP, FIRST_LAYER); 951 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 952 cm.commitSnapshots(TimestampType(1)); 953 assertState(ChunkState.added_loaded); 954 h.assertCalled(0b0000_0000); 955 assertHasOldSnapshot(TimestampType(0)); 956 957 //-------------------------------------------------------------------------- 958 setupStateSaving(ChunkState.added_loaded); 959 // added_loaded saving -> added_loaded after on_saved 960 cm.onSnapshotSaved(TestSavedChunkData(TimestampType(1))); 961 assertState(ChunkState.added_loaded); 962 h.assertCalled(0b0000_0000); 963 964 //-------------------------------------------------------------------------- 965 setupStateSaving(ChunkState.added_loaded); 966 // added_loaded saving -> removed_loaded saving 967 cm.setExternalChunkObservers(ZERO_CWP, 0); 968 assertState(ChunkState.removed_loaded_used); 969 h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled 970 971 972 //-------------------------------------------------------------------------- 973 setupStateSaving(ChunkState.removed_loaded_used); 974 // removed_loaded_used saving -> non_loaded 975 cm.onSnapshotSaved(TestSavedChunkData(TimestampType(1))); 976 assertState(ChunkState.non_loaded); 977 h.assertCalled(0b0000_0000); 978 979 //-------------------------------------------------------------------------- 980 setupStateSaving(ChunkState.added_loaded); 981 // removed_loaded_used saving -> removed_loaded_used 982 cm.addCurrentSnapshotUser(ZERO_CWP, FIRST_LAYER); 983 cm.setExternalChunkObservers(ZERO_CWP, 0); 984 assertState(ChunkState.removed_loaded_used); // & saving 985 cm.onSnapshotSaved(TestSavedChunkData(TimestampType(1))); 986 assertState(ChunkState.removed_loaded_used); 987 h.assertCalled(0b0000_0010); //onChunkRemovedHandlerCalled 988 989 //-------------------------------------------------------------------------- 990 setupStateSaving(ChunkState.removed_loaded_used); 991 // removed_loaded_used saving -> added_loaded saving 992 cm.setExternalChunkObservers(ZERO_CWP, 1); 993 assertState(ChunkState.added_loaded); 994 h.assertCalled(0b0000_0101); //onChunkAddedHandlerCalled, onChunkLoadedHandlerCalled 995 996 //-------------------------------------------------------------------------- 997 setupState(ChunkState.removed_loaded_used); 998 // removed_loaded_used -> non_loaded 999 cm.removeSnapshotUser(ZERO_CWP, TimestampType(1), FIRST_LAYER); 1000 assertState(ChunkState.non_loaded); 1001 h.assertCalled(0b0000_0000); 1002 1003 1004 //-------------------------------------------------------------------------- 1005 setupState(ChunkState.removed_loaded_used); 1006 // removed_loaded_used -> added_loaded 1007 cm.setExternalChunkObservers(ZERO_CWP, 1); 1008 assertState(ChunkState.added_loaded); 1009 h.assertCalled(0b0000_0101); //onChunkAddedHandlerCalled, onChunkLoadedHandlerCalled 1010 1011 1012 //-------------------------------------------------------------------------- 1013 // test unload of old chunk when it has users. No prev snapshots for given pos. 1014 setupState(ChunkState.added_loaded); 1015 TimestampType timestamp = cm.addCurrentSnapshotUser(ZERO_CWP, FIRST_LAYER); 1016 assert(timestamp == TimestampType(0)); 1017 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 1018 cm.commitSnapshots(1); 1019 assert(timestamp in cm.oldSnapshots[FIRST_LAYER][ZERO_CWP]); 1020 cm.removeSnapshotUser(ZERO_CWP, timestamp, FIRST_LAYER); 1021 assertNoOldSnapshots(); 1022 1023 //-------------------------------------------------------------------------- 1024 // test unload of old chunk when it has users. Already has snapshot for earlier timestamp. 1025 setupState(ChunkState.added_loaded); 1026 1027 TimestampType timestamp0 = cm.addCurrentSnapshotUser(ZERO_CWP, FIRST_LAYER); 1028 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 1029 cm.commitSnapshots(1); // commit adds timestamp 0 to oldSnapshots 1030 assert(timestamp0 in cm.oldSnapshots[FIRST_LAYER][ZERO_CWP]); 1031 1032 TimestampType timestamp1 = cm.addCurrentSnapshotUser(ZERO_CWP, FIRST_LAYER); 1033 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 1034 cm.commitSnapshots(2); // commit adds timestamp 1 to oldSnapshots 1035 assert(timestamp1 in cm.oldSnapshots[FIRST_LAYER][ZERO_CWP]); 1036 1037 cm.removeSnapshotUser(ZERO_CWP, timestamp0, FIRST_LAYER); 1038 cm.removeSnapshotUser(ZERO_CWP, timestamp1, FIRST_LAYER); 1039 assertNoOldSnapshots(); 1040 1041 //-------------------------------------------------------------------------- 1042 // test case where old snapshot was saved and current snapshot is added_loaded 1043 setupState(ChunkState.added_loaded); 1044 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 1045 cm.commitSnapshots(1); 1046 cm.save(); 1047 cm.getOrCreateWriteBuffer(ZERO_CWP, FIRST_LAYER, WriteBufferPolicy.copySnapshotArray); 1048 cm.commitSnapshots(2); // now, snap that is saved is old. 1049 cm.onSnapshotSaved(TestSavedChunkData(TimestampType(1))); 1050 assertNoOldSnapshots(); 1051 }