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