source: binary-improvements2/MapRendering/src/MapRenderer.cs@ 410

Last change on this file since 410 was 402, checked in by alloc, 22 months ago
  • Major refactoring
  • Using Utf8Json for (de)serialization
  • Moving APIs to REST
  • Removing dependencies from WebServer and MapRenderer to ServerFixes
File size: 12.5 KB
Line 
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.IO;
5using System.Text;
6using System.Threading;
7using UnityEngine;
8using UnityEngine.Profiling;
9using Utf8Json;
10using Webserver.FileCache;
11using Object = UnityEngine.Object;
12
13namespace MapRendering {
14 public class MapRenderer {
15 private static MapRenderer instance;
16
17 private static readonly object lockObject = new object ();
18 public static bool renderingEnabled = true;
19 private readonly MapTileCache cache = new MapTileCache (Constants.MapBlockSize);
20 private readonly Dictionary<Vector2i, Color32[]> dirtyChunks = new Dictionary<Vector2i, Color32[]> ();
21 private readonly MicroStopwatch msw = new MicroStopwatch ();
22 private readonly MapRenderBlockBuffer[] zoomLevelBuffers;
23 private Coroutine renderCoroutineRef;
24 private bool renderingFullMap;
25 private float renderTimeout = float.MaxValue;
26 private bool shutdown;
27
28 private MapRenderer () {
29 Constants.MapDirectory = $"{GameIO.GetSaveGameDir ()}/map";
30
31 if (!LoadMapInfo ()) {
32 WriteMapInfo ();
33 }
34
35 cache.SetZoomCount (Constants.Zoomlevels);
36
37 zoomLevelBuffers = new MapRenderBlockBuffer[Constants.Zoomlevels];
38 for (int i = 0; i < Constants.Zoomlevels; i++) {
39 zoomLevelBuffers [i] = new MapRenderBlockBuffer (i, cache);
40 }
41
42 renderCoroutineRef = ThreadManager.StartCoroutine (renderCoroutine ());
43 }
44
45 public static MapRenderer Instance => instance ??= new MapRenderer ();
46
47 public static AbstractCache GetTileCache () {
48 return Instance.cache;
49 }
50
51 public static void Shutdown () {
52 if (Instance == null) {
53 return;
54 }
55
56 Instance.shutdown = true;
57
58 if (Instance.renderCoroutineRef != null) {
59 ThreadManager.StopCoroutine (Instance.renderCoroutineRef);
60 Instance.renderCoroutineRef = null;
61 }
62 }
63
64 public static void RenderSingleChunk (Chunk _chunk) {
65 if (renderingEnabled && Instance != null) {
66 // TODO: Replace with regular thread and a blocking queue / set
67 ThreadPool.UnsafeQueueUserWorkItem (_o => {
68 try {
69 if (Instance.renderingFullMap) {
70 return;
71 }
72
73 lock (lockObject) {
74 Chunk c = (Chunk) _o;
75 Vector3i cPos = c.GetWorldPos ();
76 Vector2i cPos2 = new Vector2i (cPos.x / Constants.MapChunkSize,
77 cPos.z / Constants.MapChunkSize);
78
79 ushort[] mapColors = c.GetMapColors ();
80 if (mapColors == null) {
81 return;
82 }
83
84 Color32[] realColors =
85 new Color32[Constants.MapChunkSize * Constants.MapChunkSize];
86 for (int iColors = 0; iColors < mapColors.Length; iColors++) {
87 realColors [iColors] = shortColorToColor32 (mapColors [iColors]);
88 }
89
90 Instance.dirtyChunks [cPos2] = realColors;
91
92 //Log.Out ("Add Dirty: " + cPos2);
93 }
94 } catch (Exception e) {
95 Log.Out ($"Exception in MapRendering.RenderSingleChunk(): {e}");
96 }
97 }, _chunk);
98 }
99 }
100
101 public void RenderFullMap () {
102 MicroStopwatch microStopwatch = new MicroStopwatch ();
103
104 string regionSaveDir = GameIO.GetSaveGameRegionDir ();
105 RegionFileManager rfm = new RegionFileManager (regionSaveDir, regionSaveDir, 0, false);
106 Texture2D fullMapTexture = null;
107
108 getWorldExtent (rfm, out Vector2i minChunk, out Vector2i maxChunk, out Vector2i minPos, out Vector2i maxPos, out int widthChunks, out int heightChunks,
109 out int widthPix, out int heightPix);
110
111 Log.Out (
112 $"RenderMap: min: {minChunk.ToString ()}, max: {maxChunk.ToString ()}, minPos: {minPos.ToString ()}, maxPos: {maxPos.ToString ()}, w/h: {widthChunks}/{heightChunks}, wP/hP: {widthPix}/{heightPix}"
113 );
114
115 lock (lockObject) {
116 for (int i = 0; i < Constants.Zoomlevels; i++) {
117 zoomLevelBuffers [i].ResetBlock ();
118 }
119
120 if (Directory.Exists (Constants.MapDirectory)) {
121 Directory.Delete (Constants.MapDirectory, true);
122 }
123
124 WriteMapInfo ();
125
126 renderingFullMap = true;
127
128 if (widthPix <= 8192 && heightPix <= 8192) {
129 fullMapTexture = new Texture2D (widthPix, heightPix);
130 }
131
132 Vector2i curFullMapPos = default;
133 Vector2i curChunkPos = default;
134 for (curFullMapPos.x = 0; curFullMapPos.x < widthPix; curFullMapPos.x += Constants.MapChunkSize) {
135 for (curFullMapPos.y = 0;
136 curFullMapPos.y < heightPix;
137 curFullMapPos.y += Constants.MapChunkSize) {
138 curChunkPos.x = curFullMapPos.x / Constants.MapChunkSize + minChunk.x;
139 curChunkPos.y = curFullMapPos.y / Constants.MapChunkSize + minChunk.y;
140
141 try {
142 long chunkKey = WorldChunkCache.MakeChunkKey (curChunkPos.x, curChunkPos.y);
143 if (rfm.ContainsChunkSync (chunkKey)) {
144 Chunk c = rfm.GetChunkSync (chunkKey);
145 ushort[] mapColors = c.GetMapColors ();
146 if (mapColors != null) {
147 Color32[] realColors =
148 new Color32[Constants.MapChunkSize * Constants.MapChunkSize];
149 for (int iColors = 0; iColors < mapColors.Length; iColors++) {
150 realColors [iColors] = shortColorToColor32 (mapColors [iColors]);
151 }
152
153 dirtyChunks [curChunkPos] = realColors;
154 if (fullMapTexture != null) {
155 fullMapTexture.SetPixels32 (curFullMapPos.x, curFullMapPos.y,
156 Constants.MapChunkSize, Constants.MapChunkSize, realColors);
157 }
158 }
159 }
160 } catch (Exception e) {
161 Log.Out ($"Exception: {e}");
162 }
163 }
164
165 while (dirtyChunks.Count > 0) {
166 RenderDirtyChunks ();
167 }
168
169 Log.Out ($"RenderMap: {curFullMapPos.x}/{widthPix} ({(int)((float)curFullMapPos.x / widthPix * 100)}%)");
170 }
171 }
172
173 rfm.Cleanup ();
174
175 if (fullMapTexture != null) {
176 byte[] array = fullMapTexture.EncodeToPNG ();
177 File.WriteAllBytes ($"{Constants.MapDirectory}/map.png", array);
178 Object.Destroy (fullMapTexture);
179 }
180
181 renderingFullMap = false;
182
183 Log.Out ($"Generating map took: {microStopwatch.ElapsedMilliseconds} ms");
184 Log.Out ($"World extent: {minPos} - {maxPos}");
185 }
186
187 private void SaveAllBlockMaps () {
188 for (int i = 0; i < Constants.Zoomlevels; i++) {
189 zoomLevelBuffers [i].SaveBlock ();
190 }
191 }
192
193 private readonly WaitForSeconds coroutineDelay = new WaitForSeconds (0.2f);
194
195 private IEnumerator renderCoroutine () {
196 while (!shutdown) {
197 lock (lockObject) {
198 if (dirtyChunks.Count > 0 && renderTimeout >= float.MaxValue / 2) {
199 renderTimeout = Time.time + 0.5f;
200 }
201
202 if (Time.time > renderTimeout || dirtyChunks.Count > 200) {
203 Profiler.BeginSample ("RenderDirtyChunks");
204 RenderDirtyChunks ();
205 Profiler.EndSample ();
206 }
207 }
208
209 yield return coroutineDelay;
210 }
211 }
212
213 private readonly List<Vector2i> chunksToRender = new List<Vector2i> ();
214 private readonly List<Vector2i> chunksRendered = new List<Vector2i> ();
215
216 private void RenderDirtyChunks () {
217 msw.ResetAndRestart ();
218
219 if (dirtyChunks.Count <= 0) {
220 return;
221 }
222
223 Profiler.BeginSample ("RenderDirtyChunks.Prepare");
224 chunksToRender.Clear ();
225 chunksRendered.Clear ();
226
227 dirtyChunks.CopyKeysTo (chunksToRender);
228
229 Vector2i chunkPos = chunksToRender [0];
230 chunksRendered.Add (chunkPos);
231
232 //Log.Out ("Start Dirty: " + chunkPos);
233
234 getBlockNumber (chunkPos, out Vector2i block, out _, Constants.MAP_BLOCK_TO_CHUNK_DIV, Constants.MapChunkSize);
235
236 zoomLevelBuffers [Constants.Zoomlevels - 1].LoadBlock (block);
237 Profiler.EndSample ();
238
239 Profiler.BeginSample ("RenderDirtyChunks.Work");
240 // Write all chunks that are in the same image tile of the highest zoom level
241 foreach (Vector2i v in chunksToRender) {
242 getBlockNumber (v, out Vector2i vBlock, out Vector2i vBlockOffset, Constants.MAP_BLOCK_TO_CHUNK_DIV,
243 Constants.MapChunkSize);
244 if (!vBlock.Equals (block)) {
245 continue;
246 }
247
248 //Log.Out ("Dirty: " + v + " render: true");
249 chunksRendered.Add (v);
250 if (dirtyChunks [v].Length != Constants.MapChunkSize * Constants.MapChunkSize) {
251 Log.Error (
252 $"Rendering chunk has incorrect data size of {dirtyChunks [v].Length} instead of {Constants.MapChunkSize * Constants.MapChunkSize}");
253 }
254
255 zoomLevelBuffers [Constants.Zoomlevels - 1]
256 .SetPart (vBlockOffset, Constants.MapChunkSize, dirtyChunks [v]);
257 }
258 Profiler.EndSample ();
259
260 foreach (Vector2i v in chunksRendered) {
261 dirtyChunks.Remove (v);
262 }
263
264 // Update lower zoom levels affected by the change of the highest one
265 RenderZoomLevel (block);
266
267 Profiler.BeginSample ("RenderDirtyChunks.SaveAll");
268 SaveAllBlockMaps ();
269 Profiler.EndSample ();
270 }
271
272 private void RenderZoomLevel (Vector2i _innerBlock) {
273 Profiler.BeginSample ("RenderZoomLevel");
274 int level = Constants.Zoomlevels - 1;
275 while (level > 0) {
276 getBlockNumber (_innerBlock, out Vector2i block, out Vector2i blockOffset, 2, Constants.MapBlockSize / 2);
277
278 zoomLevelBuffers [level - 1].LoadBlock (block);
279
280 Profiler.BeginSample ("RenderZoomLevel.Transfer");
281 if ((zoomLevelBuffers [level].FormatSelf == TextureFormat.ARGB32 ||
282 zoomLevelBuffers [level].FormatSelf == TextureFormat.RGBA32) &&
283 zoomLevelBuffers [level].FormatSelf == zoomLevelBuffers [level - 1].FormatSelf) {
284 zoomLevelBuffers [level - 1].SetPartNative (blockOffset, Constants.MapBlockSize / 2, zoomLevelBuffers [level].GetHalfScaledNative ());
285 } else {
286 zoomLevelBuffers [level - 1].SetPart (blockOffset, Constants.MapBlockSize / 2, zoomLevelBuffers [level].GetHalfScaled ());
287 }
288 Profiler.EndSample ();
289
290 level--;
291 _innerBlock = block;
292 }
293 Profiler.EndSample ();
294 }
295
296 private void getBlockNumber (Vector2i _innerPos, out Vector2i _block, out Vector2i _blockOffset, int _scaleFactor,
297 int _offsetSize) {
298 _block = default;
299 _blockOffset = default;
300 _block.x = (_innerPos.x + 16777216) / _scaleFactor - 16777216 / _scaleFactor;
301 _block.y = (_innerPos.y + 16777216) / _scaleFactor - 16777216 / _scaleFactor;
302 _blockOffset.x = (_innerPos.x + 16777216) % _scaleFactor * _offsetSize;
303 _blockOffset.y = (_innerPos.y + 16777216) % _scaleFactor * _offsetSize;
304 }
305
306 private void WriteMapInfo () {
307 JsonWriter writer = new JsonWriter ();
308 writer.WriteBeginObject ();
309
310 writer.WriteString ("blockSize");
311 writer.WriteNameSeparator ();
312 writer.WriteInt32 (Constants.MapBlockSize);
313
314 writer.WriteValueSeparator ();
315 writer.WriteString ("maxZoom");
316 writer.WriteNameSeparator ();
317 writer.WriteInt32 (Constants.Zoomlevels - 1);
318
319 writer.WriteEndObject ();
320
321 Directory.CreateDirectory (Constants.MapDirectory);
322 File.WriteAllBytes ($"{Constants.MapDirectory}/mapinfo.json", writer.ToUtf8ByteArray ());
323 }
324
325 private bool LoadMapInfo () {
326 if (!File.Exists ($"{Constants.MapDirectory}/mapinfo.json")) {
327 return false;
328 }
329
330 string json = File.ReadAllText ($"{Constants.MapDirectory}/mapinfo.json", Encoding.UTF8);
331 try {
332 IDictionary<string,object> inputJson = JsonSerializer.Deserialize<IDictionary<string, object>> (json);
333
334 if (inputJson.TryGetValue ("blockSize", out object fieldNode) && fieldNode is double value) {
335 Constants.MapBlockSize = (int)value;
336 }
337
338 if (inputJson.TryGetValue ("maxZoom", out fieldNode) && fieldNode is double value2) {
339 Constants.Zoomlevels = (int)value2 + 1;
340 }
341
342 return true;
343 } catch (Exception e) {
344 Log.Out ($"Exception in LoadMapInfo: {e}");
345 }
346
347 return false;
348 }
349
350 private void getWorldExtent (RegionFileManager _rfm, out Vector2i _minChunk, out Vector2i _maxChunk,
351 out Vector2i _minPos, out Vector2i _maxPos,
352 out int _widthChunks, out int _heightChunks,
353 out int _widthPix, out int _heightPix) {
354 _minChunk = default;
355 _maxChunk = default;
356 _minPos = default;
357 _maxPos = default;
358
359 long[] keys = _rfm.GetAllChunkKeys ();
360 int minX = int.MaxValue;
361 int minY = int.MaxValue;
362 int maxX = int.MinValue;
363 int maxY = int.MinValue;
364 foreach (long key in keys) {
365 int x = WorldChunkCache.extractX (key);
366 int y = WorldChunkCache.extractZ (key);
367
368 if (x < minX) {
369 minX = x;
370 }
371
372 if (x > maxX) {
373 maxX = x;
374 }
375
376 if (y < minY) {
377 minY = y;
378 }
379
380 if (y > maxY) {
381 maxY = y;
382 }
383 }
384
385 _minChunk.x = minX;
386 _minChunk.y = minY;
387
388 _maxChunk.x = maxX;
389 _maxChunk.y = maxY;
390
391 _minPos.x = minX * Constants.MapChunkSize;
392 _minPos.y = minY * Constants.MapChunkSize;
393
394 _maxPos.x = maxX * Constants.MapChunkSize;
395 _maxPos.y = maxY * Constants.MapChunkSize;
396
397 _widthChunks = maxX - minX + 1;
398 _heightChunks = maxY - minY + 1;
399
400 _widthPix = _widthChunks * Constants.MapChunkSize;
401 _heightPix = _heightChunks * Constants.MapChunkSize;
402 }
403
404 private static Color32 shortColorToColor32 (ushort _col) {
405 byte r = (byte) (256 * ((_col >> 10) & 31) / 32);
406 byte g = (byte) (256 * ((_col >> 5) & 31) / 32);
407 byte b = (byte) (256 * (_col & 31) / 32);
408 const byte a = 255;
409 return new Color32 (r, g, b, a);
410 }
411 }
412}
Note: See TracBrowser for help on using the repository browser.