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 }