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 }