1 /** 2 Copyright: Copyright (c) 2013-2016 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module voxelman.storage.chunkmanager; 7 8 import std.experimental.logger; 9 import std.typecons : Nullable; 10 11 import voxelman.core.block; 12 import voxelman.core.config; 13 import voxelman.storage.chunk; 14 import voxelman.storage.coordinates : ChunkWorldPos; 15 import voxelman.storage.utils; 16 import voxelman.utils.hashset; 17 18 19 private enum ChunkState { 20 non_loaded, 21 added_loaded, 22 removed_loading, 23 added_loading, 24 removed_loaded_saving, 25 removed_loaded_used, 26 added_loaded_saving, 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 enum maxFreeItems = 200; 35 struct ChunkFreeList { 36 BlockType[][maxFreeItems] items; 37 size_t numItems; 38 39 BlockType[] allocate() { 40 import std.array : uninitializedArray; 41 if (numItems > 0) { 42 --numItems; 43 BlockType[] item = items[numItems]; 44 items[numItems] = null; 45 return item; 46 } else { 47 return uninitializedArray!(BlockType[])(CHUNK_SIZE_CUBE); 48 } 49 } 50 51 void deallocate(BlockType[] blocks) { 52 if (blocks is null) return; 53 if (numItems == maxFreeItems) { 54 delete(blocks); 55 return; 56 } 57 items[numItems] = blocks; 58 } 59 } 60 61 final class ChunkManager { 62 void delegate(ChunkWorldPos)[] onChunkAddedHandlers; 63 void delegate(ChunkWorldPos)[] onChunkRemovedHandlers; 64 void delegate(ChunkWorldPos, BlockDataSnapshot)[] onChunkLoadedHandlers; 65 void delegate(BlockChange[][ChunkWorldPos])[] chunkChangesHandlers; 66 void delegate(ChunkWorldPos cwp, BlockType[] outBuffer) loadChunkHandler; 67 void delegate(ChunkWorldPos cwp, BlockDataSnapshot snapshot) saveChunkHandler; 68 69 private ChunkFreeList freeList; 70 private BlockDataSnapshot[ChunkWorldPos] snapshots; 71 private BlockDataSnapshot[TimestampType][ChunkWorldPos] oldSnapshots; 72 private BlockType[][ChunkWorldPos] writeBuffers; 73 private BlockChange[][ChunkWorldPos] chunkChanges; 74 private ChunkState[ChunkWorldPos] chunkStates; 75 private HashSet!ChunkWorldPos modifiedChunks; 76 private size_t[ChunkWorldPos] numInternalChunkUsers; 77 private size_t[ChunkWorldPos] numExternalChunkUsers; 78 79 80 /// Performs save of all modified chunks. 81 /// Modified chunks are those that were committed. 82 /// Perform save right after commit. 83 void save() { 84 foreach(cwp; modifiedChunks.items) { 85 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 86 with(ChunkState) final switch(state) { 87 case non_loaded: 88 assert(false, "Save should not occur for not added chunks"); 89 case added_loaded: 90 chunkStates[cwp] = added_loaded_saving; 91 auto snap = cwp in snapshots; 92 ++snap.numUsers; 93 saveChunkHandler(cwp, *snap); 94 break; 95 case removed_loading: 96 assert(false, "Save should not occur for not loaded chunks"); 97 case added_loading: 98 assert(false, "Save should not occur for not loaded chunks"); 99 case removed_loaded_saving: 100 assert(false, "Save should not occur for not added chunks"); 101 case removed_loaded_used: 102 assert(false, "Save should not occur for not added chunks"); 103 case added_loaded_saving: 104 assert(false, "Save should not occur for not for saving chunk"); 105 } 106 mixin(traceStateStr); 107 } 108 modifiedChunks.clear(); 109 } 110 111 /// Sets number of users of chunk at cwp. 112 /// If total chunk users if greater than zero, then chunk is loaded, 113 /// if equal to zero, chunk will be unloaded. 114 void setExternalChunkUsers(ChunkWorldPos cwp, size_t numExternalUsers) { 115 numExternalChunkUsers[cwp] = numExternalUsers; 116 if (numExternalUsers == 0) 117 numExternalChunkUsers.remove(cwp); 118 setChunkTotalObservers(cwp, numInternalChunkUsers.get(cwp, 0) + numExternalUsers); 119 } 120 121 /// returned value isNull if chunk is not loaded/added 122 Nullable!BlockDataSnapshot getChunkSnapshot(ChunkWorldPos cwp) { 123 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 124 if (state == ChunkState.added_loaded || state == ChunkState.added_loaded_saving) 125 return Nullable!BlockDataSnapshot(snapshots[cwp]); 126 else { 127 return Nullable!BlockDataSnapshot.init; 128 } 129 } 130 131 /// Returns writeable copy of current chunk snapshot. 132 /// Any changes made to it must be reported trough onBlockChanges method. 133 /// This buffer is valid until commit. 134 /// After commit this buffer becomes next immutable snapshot. 135 /// Returns null if chunk is not added and/or not loaded. 136 BlockType[] getWriteBuffer(ChunkWorldPos cwp) { 137 auto newData = writeBuffers.get(cwp, null); 138 if (newData is null) { 139 newData = createWriteBuffer(cwp); 140 } 141 return newData; 142 } 143 144 import std.range : isInputRange, array; 145 /// Call this whenewer changes to write buffer are done. 146 /// Those changes will be passed to chunkChangesHandlers to be handled when sendChanges is called. 147 void onBlockChanges(R)(ChunkWorldPos cwp, R blockChanges) 148 if (isInputRange!(R)) 149 { 150 chunkChanges[cwp] = chunkChanges.get(cwp, null) ~ blockChanges.array; 151 } 152 153 /// Returns timestamp of current chunk snapshot. 154 /// Store this timestamp to use in removeSnapshotUser 155 TimestampType addCurrentSnapshotUser(ChunkWorldPos cwp) { 156 auto snap = cwp in snapshots; 157 assert(snap, "Cannot add chunk user. No such snapshot."); 158 159 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 160 assert(state == ChunkState.added_loaded || state == ChunkState.added_loaded_saving, 161 "To add user chunk must be both added and loaded"); 162 163 ++snap.numUsers; 164 return snap.timestamp; 165 } 166 167 /// Generic removal of snapshot user. Removes chunk if numUsers == 0. 168 /// Use this to remove added snapshot user. Use timestamp returned from addCurrentSnapshotUser. 169 void removeSnapshotUser(ChunkWorldPos cwp, TimestampType timestamp) { 170 auto snap = cwp in snapshots; 171 if (snap && snap.timestamp == timestamp) { 172 auto numUsersLeft = removeCurrentSnapshotUser(cwp); 173 if (numUsersLeft == 0) { 174 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 175 if (state == ChunkState.removed_loaded_used) { 176 chunkStates[cwp] = ChunkState.non_loaded; 177 clearChunkData(cwp); 178 } 179 } 180 } else { 181 auto snapshot = removeOldSnapshotUser(cwp, timestamp); 182 if (snapshot.numUsers == 0) 183 recycleSnapshotMemory(snapshot); 184 } 185 } 186 187 /// Internal. Called by code which loads chunks from storage. 188 void onSnapshotLoaded(ChunkWorldPos cwp, BlockDataSnapshot snap) { 189 snapshots[cwp] = BlockDataSnapshot(snap.blockData, snap.timestamp); 190 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 191 with(ChunkState) final switch(state) { 192 case non_loaded: 193 assert(false); 194 case added_loaded: 195 assert(false, "On loaded should not occur for already loaded chunk"); 196 case removed_loading: 197 chunkStates[cwp] = non_loaded; 198 clearChunkData(cwp); 199 break; 200 case added_loading: 201 chunkStates[cwp] = added_loaded; 202 // Create snapshot for loaded data 203 auto snapshot = cwp in snapshots; 204 if (snapshot.blockData.uniform) { 205 freeList.deallocate(snapshot.blockData.blocks); 206 snapshot.blockData.blocks = null; 207 } 208 notifyLoaded(cwp); 209 break; 210 case removed_loaded_saving: 211 assert(false, "On loaded should not occur for already loaded chunk"); 212 case removed_loaded_used: 213 assert(false, "On loaded should not occur for already loaded chunk"); 214 case added_loaded_saving: 215 assert(false, "On loaded should not occur for already loaded chunk"); 216 } 217 mixin(traceStateStr); 218 } 219 220 /// Internal. Called by code which saves chunks to storage. 221 void onSnapshotSaved(ChunkWorldPos cwp, TimestampType timestamp) { 222 auto snap = cwp in snapshots; 223 if (snap && snap.timestamp == timestamp) { 224 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 225 with(ChunkState) final switch(state) { 226 case non_loaded: 227 assert(false, "On saved should not occur for not added chunks"); 228 case added_loaded: 229 assert(false, "On saved should not occur for not saving chunks"); 230 case removed_loading: 231 assert(false, "On saved should not occur for not loaded chunks"); 232 case added_loading: 233 assert(false, "On saved should not occur for not loaded chunks"); 234 case removed_loaded_saving: 235 auto numUsersLeft = removeCurrentSnapshotUser(cwp); 236 if (numUsersLeft == 0) { 237 chunkStates[cwp] = non_loaded; 238 clearChunkData(cwp); 239 } else { 240 chunkStates[cwp] = removed_loaded_used; 241 } 242 break; 243 case removed_loaded_used: 244 assert(false, "On saved should not occur for not saving chunks"); 245 case added_loaded_saving: 246 chunkStates[cwp] = added_loaded; 247 removeCurrentSnapshotUser(cwp); 248 break; 249 } 250 mixin(traceStateStr); 251 } else { // old snapshot saved 252 auto snapshot = removeOldSnapshotUser(cwp, timestamp); 253 if (snapshot.numUsers == 0) 254 recycleSnapshotMemory(snapshot); 255 } 256 } 257 258 /// called at the end of tick 259 void commitSnapshots(TimestampType currentTime) { 260 auto writeBuffersCopy = writeBuffers; 261 // Clear it here because commit can unload chunk. 262 // And unload asserts that chunk is not in writeBuffers. 263 writeBuffers = null; 264 foreach(snapshot; writeBuffersCopy.byKeyValue) { 265 auto cwp = snapshot.key; 266 auto blockData = snapshot.value; 267 modifiedChunks.put(cwp); 268 commitChunkSnapshot(cwp, blockData, currentTime); 269 } 270 } 271 272 /// Send changes to clients 273 void sendChanges() { 274 foreach(handler; chunkChangesHandlers) 275 handler(chunkChanges); 276 chunkChanges = null; 277 } 278 279 // PPPPPP RRRRRR IIIII VV VV AAA TTTTTTT EEEEEEE 280 // PP PP RR RR III VV VV AAAAA TTT EE 281 // PPPPPP RRRRRR III VV VV AA AA TTT EEEEE 282 // PP RR RR III VV VV AAAAAAA TTT EE 283 // PP RR RR IIIII VVV AA AA TTT EEEEEEE 284 // 285 286 private void notifyAdded(ChunkWorldPos cwp) { 287 foreach(handler; onChunkAddedHandlers) 288 handler(cwp); 289 } 290 291 private void notifyRemoved(ChunkWorldPos cwp) { 292 foreach(handler; onChunkRemovedHandlers) 293 handler(cwp); 294 } 295 296 private void notifyLoaded(ChunkWorldPos cwp) { 297 auto snap = getChunkSnapshot(cwp); 298 assert(!snap.isNull); 299 foreach(handler; onChunkLoadedHandlers) 300 handler(cwp, snap); 301 } 302 303 // Puts chunk in added state requesting load if needed. 304 // Notifies on add. Notifies on load if loaded. 305 private void loadChunk(ChunkWorldPos cwp) { 306 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 307 with(ChunkState) final switch(state) { 308 case non_loaded: 309 chunkStates[cwp] = added_loading; 310 loadChunkHandler(cwp, freeList.allocate()); 311 notifyAdded(cwp); 312 break; 313 case added_loaded: 314 break; // ignore 315 case removed_loading: 316 chunkStates[cwp] = added_loading; 317 notifyAdded(cwp); 318 break; 319 case added_loading: 320 break; // ignore 321 case removed_loaded_saving: 322 chunkStates[cwp] = added_loaded_saving; 323 notifyAdded(cwp); 324 notifyLoaded(cwp); 325 break; 326 case removed_loaded_used: 327 chunkStates[cwp] = added_loaded; 328 notifyAdded(cwp); 329 notifyLoaded(cwp); 330 break; 331 case added_loaded_saving: 332 break; // ignore 333 } 334 mixin(traceStateStr); 335 } 336 337 // Puts chunk in removed state requesting save if needed. 338 // Notifies on remove. 339 private void unloadChunk(ChunkWorldPos cwp) { 340 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 341 with(ChunkState) final switch(state) { 342 case non_loaded: 343 assert(false, "Unload should not occur when chunk was not yet loaded"); 344 case added_loaded: 345 assert(cwp !in writeBuffers, "Chunk with write buffer should not be unloaded"); 346 notifyRemoved(cwp); 347 auto snap = cwp in snapshots; 348 if(cwp in modifiedChunks) { 349 chunkStates[cwp] = removed_loaded_saving; 350 saveChunkHandler(cwp, *snap); 351 ++snap.numUsers; 352 modifiedChunks.remove(cwp); 353 } else { // state 0 354 chunkStates[cwp] = non_loaded; 355 clearChunkData(cwp); 356 } 357 break; 358 case removed_loading: 359 assert(false, "Unload should not occur when chunk is already removed"); 360 case added_loading: 361 notifyRemoved(cwp); 362 chunkStates[cwp] = removed_loading; 363 break; 364 case removed_loaded_saving: 365 assert(false, "Unload should not occur when chunk is already removed"); 366 case removed_loaded_used: 367 assert(false, "Unload should not occur when chunk is already removed"); 368 case added_loaded_saving: 369 notifyRemoved(cwp); 370 chunkStates[cwp] = removed_loaded_saving; 371 break; 372 } 373 mixin(traceStateStr); 374 } 375 376 // Fully removes chunk 377 private void clearChunkData(ChunkWorldPos cwp) { 378 recycleSnapshotMemory(snapshots[cwp]); 379 snapshots.remove(cwp); 380 assert(cwp !in writeBuffers); 381 assert(cwp !in chunkChanges); 382 assert(cwp !in modifiedChunks); 383 chunkStates.remove(cwp); 384 } 385 386 // Creates write buffer for writing changes in it. 387 // Latest snapshot's data is copied in it. 388 // On commit stage this is moved into new snapshot and. 389 // Adds internal user that is removed on commit to prevent unloading with uncommitted changes. 390 private BlockType[] createWriteBuffer(ChunkWorldPos cwp) { 391 assert(writeBuffers.get(cwp, null) is null); 392 auto old = getChunkSnapshot(cwp); 393 if (old.isNull) { 394 return null; 395 } 396 auto buffer = freeList.allocate(); 397 old.blockData.copyToBuffer(buffer); 398 writeBuffers[cwp] = buffer; 399 addInternalUser(cwp); // prevent unload until commit 400 return buffer; 401 } 402 403 // Here comes sum of all internal and external chunk users which results in loading or unloading of specific chunk. 404 private void setChunkTotalObservers(ChunkWorldPos cwp, size_t totalObservers) { 405 if (totalObservers > 0) { 406 loadChunk(cwp); 407 } else { 408 unloadChunk(cwp); 409 } 410 } 411 412 // Used inside chunk manager to add chunk users, to prevent chunk unloading. 413 private void addInternalUser(ChunkWorldPos cwp) { 414 numInternalChunkUsers[cwp] = numInternalChunkUsers.get(cwp, 0) + 1; 415 auto totalUsers = numInternalChunkUsers[cwp] + numExternalChunkUsers.get(cwp, 0); 416 setChunkTotalObservers(cwp, totalUsers); 417 } 418 419 // Used inside chunk manager to remove chunk users. 420 private void removeInternalUser(ChunkWorldPos cwp) { 421 auto numUsers = numInternalChunkUsers.get(cwp, 0); 422 assert(numUsers > 0, "numInternalChunkUsers is zero when removing internal user"); 423 --numUsers; 424 if (numUsers == 0) 425 numInternalChunkUsers.remove(cwp); 426 else 427 numInternalChunkUsers[cwp] = numUsers; 428 auto totalUsers = numUsers + numExternalChunkUsers.get(cwp, 0); 429 setChunkTotalObservers(cwp, totalUsers); 430 } 431 432 // Returns number of current snapshot users left. 433 private uint removeCurrentSnapshotUser(ChunkWorldPos cwp) { 434 auto snap = cwp in snapshots; 435 assert(snap, "Cannot remove chunk user. No such snapshot."); 436 assert(snap.numUsers > 0, "cannot remove chunk user. Snapshot has 0 users."); 437 --snap.numUsers; 438 return snap.numUsers; 439 } 440 441 // Returns that snapshot with updated numUsers. 442 // Snapshot is removed from oldSnapshots if numUsers == 0. 443 private BlockDataSnapshot removeOldSnapshotUser(ChunkWorldPos cwp, TimestampType timestamp) { 444 BlockDataSnapshot[TimestampType]* chunkSnaps = cwp in oldSnapshots; 445 assert(chunkSnaps, "old snapshot should have waited for releasing user"); 446 BlockDataSnapshot* snapshot = timestamp in *chunkSnaps; 447 assert(snapshot, "cannot release snapshot user. No such snapshot"); 448 assert(snapshot.numUsers > 0, "snapshot with 0 users was not released"); 449 --snapshot.numUsers; 450 if (snapshot.numUsers == 0) { 451 (*chunkSnaps).remove(timestamp); 452 if ((*chunkSnaps).length == 0) { // all old snaps of one chunk released 453 oldSnapshots.remove(cwp); 454 } 455 } 456 return *snapshot; 457 } 458 459 // Commit for single chunk. 460 private void commitChunkSnapshot(ChunkWorldPos cwp, BlockType[] blocks, TimestampType currentTime) { 461 auto currentSnapshot = getChunkSnapshot(cwp); 462 assert(!currentSnapshot.isNull); 463 if (currentSnapshot.numUsers == 0) 464 recycleSnapshotMemory(currentSnapshot); 465 else { 466 BlockDataSnapshot[TimestampType] chunkSnaps = oldSnapshots.get(cwp, null); 467 assert(currentTime !in chunkSnaps); 468 chunkSnaps[currentTime] = currentSnapshot.get; 469 } 470 snapshots[cwp] = BlockDataSnapshot(BlockData(blocks, BlockType.init, false), currentTime); 471 472 auto state = chunkStates.get(cwp, ChunkState.non_loaded); 473 with(ChunkState) final switch(state) { 474 case non_loaded: 475 assert(false, "Commit is not possible for non-loaded chunk"); 476 case added_loaded: 477 break; // ignore 478 case removed_loading: 479 // Write buffer will be never returned when no snapshot is loaded. 480 assert(false, "Commit is not possible for removed chunk"); 481 case added_loading: 482 // Write buffer will be never returned when no snapshot is loaded. 483 assert(false, "Commit is not possible for non-loaded chunk"); 484 case removed_loaded_saving: 485 // This is guarded by internal user count. 486 assert(false, "Commit is not possible for removed chunk"); 487 case removed_loaded_used: 488 // This is guarded by internal user count. 489 assert(false, "Commit is not possible for removed chunk"); 490 case added_loaded_saving: 491 // This is now old snapshot with saving state. New one is not used by IO. 492 chunkStates[cwp] = added_loaded; 493 break; 494 } 495 removeInternalUser(cwp); // remove user added in getWriteBuffer 496 497 mixin(traceStateStr); 498 } 499 500 // Called when snapshot data can be recycled. 501 private void recycleSnapshotMemory(BlockDataSnapshot snap) { 502 freeList.deallocate(snap.blockData.blocks); 503 } 504 }