1 /**
2 Copyright: Copyright (c) 2015-2018 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.chunk.manager;
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.container.multihashset;
14 import voxelman.container.hash.set;
15 import voxelman.world.block;
16 import voxelman.core.config;
17 import voxelman.world.storage.chunk;
18 import voxelman.world.storage.coordinates : ChunkWorldPos, adjacentPositions;
19 import voxelman.world.storage.utils;
20 import voxelman.world.storage.chunk.chunkprovider;
21 
22 
23 private enum ChunkState {
24 	non_loaded,
25 	added_loaded,
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 
36 struct AdjChunkPositions27 {
37 	this(ChunkWorldPos cwp) {
38 		central = cwp;
39 		adjacentPositions!26(cwp, adjacent26);
40 	}
41 	union {
42 		ChunkWorldPos[27] all;
43 		ChunkWorldPos[26] adjacent26;
44 		struct {
45 			ChunkWorldPos[6] adjacent6;
46 			ChunkWorldPos[20] adjacent20;
47 			ChunkWorldPos central;
48 		}
49 	}
50 }
51 
52 struct AdjChunkLayers27 {
53 	union {
54 		Nullable!ChunkLayerSnap[27] all;
55 		Nullable!ChunkLayerSnap[26] adjacent26;
56 		struct {
57 			Nullable!ChunkLayerSnap[6] adjacent6;
58 			Nullable!ChunkLayerSnap[20] adjacent20;
59 			Nullable!ChunkLayerSnap central;
60 		}
61 	}
62 }
63 
64 Nullable!ChunkLayerSnap[len] getChunkSnapshots
65 	(size_t len)
66 	(ChunkManager cm,
67 	ChunkWorldPos[len] positions,
68 	ubyte layer,
69 	Flag!"Uncompress" uncompress = No.Uncompress)
70 {
71 	typeof(return) result;
72 
73 	foreach(i, cwp; positions)
74 	{
75 		result[i] = cm.getChunkSnapshot(cwp, layer, uncompress);
76 	}
77 
78 	return result;
79 }
80 
81 //version = TRACE_SNAP_USERS;
82 //version = DBG_OUT;
83 //version = DBG_COMPR;
84 
85 enum WriteBufferPolicy
86 {
87 	createUniform,
88 	copySnapshotArray,
89 }
90 
91 struct ChunkLayerInfo
92 {
93 	/// Defines the size of
94 	LayerDataLenType uniformExpansionType;
95 }
96 
97 struct ModifiedChunksRange
98 {
99 	private HashSet!ChunkWorldPos modifiedChunks;
100 	int opApply(scope int delegate(in ChunkWorldPos) del) {
101 		return modifiedChunks.opApply(del);
102 	}
103 }
104 
105 enum MAX_CHUNK_LAYERS = 8;
106 
107 final class ChunkManager {
108 	void delegate(ChunkWorldPos) onChunkRemovedHandler;
109 	void delegate(ChunkWorldPos) onChunkLoadedHandler;
110 
111 	void delegate(ChunkWorldPos) loadChunkHandler;
112 	void delegate(ChunkWorldPos) saveChunkHandler;
113 	void delegate(ChunkWorldPos) cancelLoadChunkHandler;
114 
115 	// debug
116 	long totalLayerDataBytes;
117 	long numLoadedChunks;
118 	size_t numTrackedChunks() { return chunkStates.length; }
119 
120 	private ChunkLayerSnap[ChunkWorldPos][] snapshots;
121 	private ChunkLayerSnap[TimestampType][ChunkWorldPos][] oldSnapshots;
122 	private ChunkState[ChunkWorldPos] chunkStates;
123 	private HashSet!ChunkWorldPos modifiedChunks;
124 	// used to change state from added_loaded to removed_loaded_used
125 	private MultiHashSet!ChunkWorldPos totalSnapshotUsers;
126 
127 
128 	ubyte numLayers;
129 	package ChunkLayerInfo[] layerInfos;
130 
131 	this(ubyte _numLayers) {
132 		numLayers = _numLayers;
133 		snapshots.length = numLayers;
134 		oldSnapshots.length = numLayers;
135 		layerInfos.length = numLayers;
136 	}
137 
138 	void setLayerInfo(ubyte layer, ChunkLayerInfo info) {
139 		layerInfos[layer] = info;
140 	}
141 
142 	bool areChunksLoaded(ChunkWorldPos[] positions) {
143 		foreach(pos; positions)
144 			if (!isChunkLoaded(pos))
145 				return false;
146 		return true;
147 	}
148 
149 	bool isChunkLoaded(ChunkWorldPos cwp) {
150 		return getChunkState(cwp) == ChunkState.added_loaded;
151 	}
152 
153 	bool isChunkAdded(ChunkWorldPos cwp) {
154 		auto state = getChunkState(cwp);
155 		with(ChunkState) {
156 			return state == added_loaded || state == added_loading;
157 		}
158 	}
159 
160 	bool hasSnapshot(ChunkWorldPos cwp, ubyte layer) {
161 		return (cwp in snapshots[layer]) !is null;
162 	}
163 
164 	ModifiedChunksRange getModifiedChunks() {
165 		return ModifiedChunksRange(modifiedChunks);
166 	}
167 
168 	/// Used on client to clear modified chunks instead of saving them.
169 	void clearModifiedChunks() {
170 		modifiedChunks.clear();
171 	}
172 
173 	/// returned value isNull if chunk is not loaded/added
174 	/// If uncompress is Yes then tries to convert snapshot to uncompressed.
175 	/// If has users, then uncompressed snapshot copy is returned. Original will not be uncompressed.
176 	Nullable!ChunkLayerSnap getChunkSnapshot(ChunkWorldPos cwp, ubyte layer, Flag!"Uncompress" uncompress = No.Uncompress) {
177 		if (isChunkLoaded(cwp))
178 		{
179 			auto snap = cwp in snapshots[layer];
180 			if (snap)
181 			{
182 				if (snap.type == StorageType.compressedArray && uncompress)
183 				{
184 					ubyte[] decompressedData = decompressLayerData((*snap).getArray!ubyte);
185 					if (snap.numUsers == 0) {
186 						recycleSnapshotMemory(*snap);
187 						snap.dataPtr = decompressedData.ptr;
188 						snap.dataLength = cast(LayerDataLenType)decompressedData.length;
189 						totalLayerDataBytes += snap.dataLength;
190 						snap.type = StorageType.fullArray;
191 					}
192 					else
193 					{
194 						// BUG: memory leak. Memory is allocated and returned, but not assigned to the snapshot
195 						// This way, when snapshot data is deleted, uncompressed data is not deleted.
196 						// a) store owner info inside ChunkLayerSnap, so that user can free resources when done.
197 						// b) store both compressed and uncompressed data here in snapshots.
198 						ChunkLayerSnap res = *snap;
199 						res.dataPtr = decompressedData.ptr;
200 						res.dataLength = cast(LayerDataLenType)decompressedData.length;
201 						res.type = StorageType.fullArray;
202 						return Nullable!ChunkLayerSnap(res);
203 					}
204 				}
205 				auto res = Nullable!ChunkLayerSnap(*snap);
206 				return res;
207 			}
208 			else
209 			{
210 				auto res = ChunkLayerSnap.init;
211 				res.dataLength = layerInfos[layer].uniformExpansionType;
212 				return Nullable!ChunkLayerSnap(res);
213 			}
214 		}
215 
216 		auto res = Nullable!ChunkLayerSnap.init;
217 		return res;
218 	}
219 
220 	/// Returns timestamp of current chunk snapshot.
221 	/// Store this timestamp to use in removeSnapshotUser
222 	/// Adding a user to non-existent snapshot of loaded chunk is allowed,
223 	/// since a ChunkLayerSnap.init is returned for such layer.
224 	/// Special TimestampType.max is returned.
225 	TimestampType addCurrentSnapshotUser(ChunkWorldPos cwp, ubyte layer) {
226 		auto snap = cwp in snapshots[layer];
227 		if (!snap)
228 		{
229 			return TimestampType.max;
230 		}
231 		version(TRACE_SNAP_USERS) tracef("#%s:%s (before add) %s/%s", cwp, layer, snap.numUsers, totalSnapshotUsers[cwp]);
232 
233 		auto state = getChunkState(cwp);
234 		assert(state == ChunkState.added_loaded,
235 			format("To add user chunk must be both added and loaded, not %s", state));
236 
237 		totalSnapshotUsers.add(cwp);
238 
239 		++snap.numUsers;
240 		version(TRACE_SNAP_USERS) tracef("#%s:%s (add cur:+1) %s/%s @%s", cwp, layer, snap.numUsers, totalSnapshotUsers[cwp], snap.timestamp);
241 		return snap.timestamp;
242 	}
243 
244 	/// Generic removal of snapshot user. Removes chunk if numUsers == 0.
245 	/// Use this to remove added snapshot user. Use timestamp returned from addCurrentSnapshotUser.
246 	/// Removing TimestampType.max is no-op.
247 	void removeSnapshotUser(ChunkWorldPos cwp, TimestampType timestamp, ubyte layer) {
248 		if (timestamp == TimestampType.max) return;
249 		auto snap = cwp in snapshots[layer];
250 		if (snap && snap.timestamp == timestamp)
251 		{
252 			auto totalUsersLeft = removeCurrentSnapshotUser(cwp, layer);
253 			if (totalUsersLeft == 0)
254 			{
255 				auto state = getChunkState(cwp);
256 				assert(state == ChunkState.added_loaded || state == ChunkState.removed_loaded_used);
257 				if (state == ChunkState.removed_loaded_used)
258 				{
259 					clearChunkData(cwp);
260 					--numLoadedChunks;
261 				}
262 			}
263 		}
264 		else
265 		{
266 			removeOldSnapshotUser(cwp, timestamp, layer);
267 		}
268 	}
269 
270 	private struct SnapshotIterator
271 	{
272 		private ChunkLayerSnap[ChunkWorldPos][] snapshots;
273 		private ubyte numLayers;
274 		private ChunkWorldPos cwp;
275 		private MultiHashSet!ChunkWorldPos* totalSnapshotUsers;
276 		int opApply(scope int delegate(ChunkLayerItem) del) {
277 			ubyte numChunkLayers;
278 			foreach(ubyte layerId; 0..numLayers)
279 			{
280 				if (auto snap = cwp in snapshots[layerId])
281 				{
282 					++snap.numUsers; // in case new snapshot replaces current one, we need to keep it while it is saved
283 					totalSnapshotUsers.add(cwp);
284 					if (auto ret = del(ChunkLayerItem(*snap, layerId)))
285 						return ret;
286 				}
287 			}
288 
289 			return 0;
290 		}
291 	}
292 
293 	SnapshotIterator iterateChunkSnapshotsAddUsers(ChunkWorldPos cwp) {
294 		return SnapshotIterator(snapshots, numLayers, cwp, &totalSnapshotUsers);
295 	}
296 
297 	/// Internal. Called by code which loads chunks from storage.
298 	void onSnapshotLoaded(ChunkWorldPos cwp, ChunkLayerItem[] layers, bool markAsModified) {
299 		version(DBG_OUT)infof("res loaded %s", cwp);
300 
301 		foreach(layer; layers)
302 		{
303 			totalLayerDataBytes += getLayerDataBytes(layer);
304 
305 			snapshots[layer.layerId][cwp] = ChunkLayerSnap(layer);
306 			version(DBG_COMPR)if (layer.type == StorageType.compressedArray)
307 				infof("CM Loaded %s %s %s\n(%(%02x%))", cwp, layer.dataPtr, layer.dataLength, layer.getArray!ubyte);
308 		}
309 
310 		auto state = getChunkState(cwp);
311 
312 		with(ChunkState) final switch(state)
313 		{
314 			case non_loaded:
315 				clearChunkData(cwp);
316 				break;
317 			case added_loaded:
318 				assert(false, "On loaded should not occur for already loaded chunk");
319 			case added_loading:
320 				chunkStates[cwp] = added_loaded;
321 				++numLoadedChunks;
322 				if (markAsModified) modifiedChunks.put(cwp);
323 				notifyLoaded(cwp);
324 				break;
325 			case removed_loaded_used:
326 				assert(false, "On loaded should not occur for already loaded chunk");
327 		}
328 		mixin(traceStateStr);
329 	}
330 
331 	// Puts chunk in added state requesting load if needed.
332 	// Notifies on add. Notifies on load if loaded.
333 	void loadChunk(ChunkWorldPos cwp) {
334 		auto state = getChunkState(cwp);
335 		with(ChunkState) final switch(state) {
336 			case non_loaded:
337 				chunkStates[cwp] = added_loading;
338 				if (loadChunkHandler) loadChunkHandler(cwp);
339 				break;
340 			case added_loaded:
341 				break; // ignore
342 			case added_loading:
343 				break; // ignore
344 			case removed_loaded_used:
345 				chunkStates[cwp] = added_loaded;
346 				notifyLoaded(cwp);
347 				break;
348 		}
349 		mixin(traceStateStr);
350 	}
351 
352 	// Puts chunk in removed state requesting save if needed.
353 	// Notifies on remove.
354 	void unloadChunk(ChunkWorldPos cwp) {
355 		auto state = getChunkState(cwp);
356 		notifyRemoved(cwp);
357 		with(ChunkState) final switch(state) {
358 			case non_loaded:
359 				assert(false, "Unload should not occur when chunk was not yet loaded");
360 			case added_loaded:
361 				if(cwp in modifiedChunks)
362 				{
363 					modifiedChunks.remove(cwp);
364 					if (saveChunkHandler) saveChunkHandler(cwp);
365 				}
366 				else
367 				{
368 					auto totalUsersLeft = totalSnapshotUsers[cwp];
369 					if (totalUsersLeft == 0)
370 					{
371 						clearChunkData(cwp);
372 						--numLoadedChunks;
373 					}
374 					else
375 					{
376 						chunkStates[cwp] = removed_loaded_used;
377 					}
378 				}
379 				break;
380 			case added_loading:
381 				if (cancelLoadChunkHandler) cancelLoadChunkHandler(cwp);
382 				clearChunkData(cwp);
383 				break;
384 			case removed_loaded_used:
385 				assert(false, "Unload should not occur when chunk is already removed");
386 		}
387 		mixin(traceStateStr);
388 	}
389 
390 	// Commit for single chunk.
391 	void commitLayerSnapshot(ChunkWorldPos cwp, WriteBuffer writeBuffer, TimestampType currentTime, ubyte layer) {
392 		modifiedChunks.put(cwp);
393 
394 		auto currentSnapshot = getChunkSnapshot(cwp, layer);
395 		if (!currentSnapshot.isNull) handleCurrentSnapCommit(cwp, layer, currentSnapshot.get());
396 
397 		assert(writeBuffer.isModified);
398 
399 		if (writeBuffer.removeSnapshot)
400 		{
401 			freeLayerArray(writeBuffer.layer);
402 			snapshots[layer].remove(cwp);
403 			return;
404 		}
405 
406 		writeBuffer.layer.timestamp = currentTime;
407 		snapshots[layer][cwp] = ChunkLayerSnap(writeBuffer.layer);
408 		totalLayerDataBytes += getLayerDataBytes(writeBuffer.layer);
409 
410 		if (!isChunkLoaded(cwp)) {
411 			chunkStates[cwp] = ChunkState.added_loaded;
412 			// BUG/TODO: this will be called on first write buffer, while needs to be called on last write buffer
413 			notifyLoaded(cwp);
414 		}
415 	}
416 
417 	//	PPPPPP  RRRRRR  IIIII VV     VV   AAA   TTTTTTT EEEEEEE
418 	//	PP   PP RR   RR  III  VV     VV  AAAAA    TTT   EE
419 	//	PPPPPP  RRRRRR   III   VV   VV  AA   AA   TTT   EEEEE
420 	//	PP      RR  RR   III    VV VV   AAAAAAA   TTT   EE
421 	//	PP      RR   RR IIIII    VVV    AA   AA   TTT   EEEEEEE
422 	//
423 
424 	private void notifyRemoved(ChunkWorldPos cwp) {
425 		if (onChunkRemovedHandler) onChunkRemovedHandler(cwp);
426 	}
427 
428 	private void notifyLoaded(ChunkWorldPos cwp) {
429 		if (onChunkLoadedHandler) onChunkLoadedHandler(cwp);
430 	}
431 
432 	private ChunkState getChunkState(ChunkWorldPos cwp) {
433 		return chunkStates.get(cwp, ChunkState.non_loaded);
434 	}
435 
436 	// Fully removes chunk
437 	private void clearChunkData(ChunkWorldPos cwp) {
438 		foreach(layer; 0..numLayers)
439 		{
440 			if (auto snap = cwp in snapshots[layer])
441 			{
442 				recycleSnapshotMemory(*snap);
443 				snapshots[layer].remove(cwp);
444 			}
445 			assert(totalSnapshotUsers[cwp] == 0);
446 		}
447 		assert(cwp !in modifiedChunks);
448 		chunkStates.remove(cwp);
449 	}
450 
451 	// Returns number of current snapshot users left.
452 	private size_t removeCurrentSnapshotUser(ChunkWorldPos cwp, ubyte layer) {
453 		auto snap = cwp in snapshots[layer];
454 		assert(snap && snap.numUsers > 0, "cannot remove chunk user. Snapshot has 0 users");
455 
456 		version(TRACE_SNAP_USERS) tracef("#%s:%s (rem before) %s/%s", cwp, layer, snap.numUsers, totalSnapshotUsers[cwp]);
457 
458 		--snap.numUsers;
459 		assert(totalSnapshotUsers[cwp] > 0, "cannot remove chunk user. Snapshot has 0 users");
460 		totalSnapshotUsers.remove(cwp);
461 
462 		version(TRACE_SNAP_USERS) tracef("#%s:%s (rem cur:-1) %s/%s @%s", cwp, layer, snap.numUsers, totalSnapshotUsers[cwp], snap.timestamp);
463 
464 		return totalSnapshotUsers[cwp];
465 	}
466 
467 	/// Snapshot is removed from oldSnapshots if numUsers == 0.
468 	private void removeOldSnapshotUser(ChunkWorldPos cwp, TimestampType timestamp, ubyte layer) {
469 		ChunkLayerSnap[TimestampType]* chunkSnaps = cwp in oldSnapshots[layer];
470 		version(TRACE_SNAP_USERS) tracef("#%s:%s (rem old) x/%s @%s", cwp, layer, totalSnapshotUsers[cwp], timestamp);
471 		assert(chunkSnaps, "old snapshot should have waited for releasing user");
472 		ChunkLayerSnap* snapshot = timestamp in *chunkSnaps;
473 		assert(snapshot, "cannot release snapshot user. No such snapshot");
474 		assert(snapshot.numUsers > 0, "cannot remove chunk user. Snapshot has 0 users");
475 		--snapshot.numUsers;
476 		version(TRACE_SNAP_USERS) tracef("#%s:%s (rem old:-1) %s/%s @%s", cwp, layer, snapshot.numUsers, totalSnapshotUsers[cwp], timestamp);
477 		if (snapshot.numUsers == 0) {
478 			(*chunkSnaps).remove(timestamp);
479 			if ((*chunkSnaps).length == 0) { // all old snaps of one chunk released
480 				oldSnapshots[layer].remove(cwp);
481 			}
482 			recycleSnapshotMemory(*snapshot);
483 		}
484 	}
485 
486 	private void handleCurrentSnapCommit(ChunkWorldPos cwp, ubyte layer, ChunkLayerSnap currentSnapshot)
487 	{
488 		if (currentSnapshot.numUsers == 0) {
489 			version(TRACE_SNAP_USERS) tracef("#%s:%s (commit:%s) %s/%s", cwp, layer, currentSnapshot.numUsers, 0, totalSnapshotUsers[cwp]);
490 			recycleSnapshotMemory(currentSnapshot);
491 		} else {
492 			// transfer users from current layer snapshot into old snapshot
493 			totalSnapshotUsers.remove(cwp, currentSnapshot.numUsers);
494 
495 			if (auto layerSnaps = cwp in oldSnapshots[layer]) {
496 				version(TRACE_SNAP_USERS) tracef("#%s:%s (commit add:%s) %s/%s", cwp, layer,
497 					currentSnapshot.numUsers, 0, totalSnapshotUsers[cwp]);
498 				assert(currentSnapshot.timestamp !in *layerSnaps);
499 				(*layerSnaps)[currentSnapshot.timestamp] = currentSnapshot;
500 			} else {
501 				version(TRACE_SNAP_USERS) tracef("#%s:%s (commit new:%s) %s/%s", cwp, layer,
502 					currentSnapshot.numUsers, 0, totalSnapshotUsers[cwp]);
503 				oldSnapshots[layer][cwp] = [currentSnapshot.timestamp : currentSnapshot];
504 				version(TRACE_SNAP_USERS) tracef("oldSnapshots[%s][%s] == %s", layer, cwp, oldSnapshots[layer][cwp]);
505 			}
506 		}
507 	}
508 
509 	// Called when snapshot data can be recycled.
510 	private void recycleSnapshotMemory(ref ChunkLayerSnap snap) {
511 		totalLayerDataBytes -= getLayerDataBytes(snap);
512 		freeLayerArray(snap);
513 	}
514 }
515