OverviewFilesCommitsIssuesDocsReleasesPeers

terrain-lookdev commits / 4204207

lookdev: publish .hblk as tiles generate, so browsing IS the bake
The engine reads a different directory in a different format from the one the lookdev fills. Converting between them was a separate manual stitch_block --per-tile run, which meant an afternoon of browsing could look like it did nothing: 971 .npy in the cache, and whatever the last conversion happened to capture in 1234_hblk. TileStore now writes the .hblk twin in the same step as the .npy, and backfill_hblk() converts anything already cached at startup (841 tiles, and it correctly skips the 130 t_legacy_256 that cached_tiles has always ignored). The .npy cache stays authoritative; .hblk is a derived view, still regenerable by stitch_block --per-tile. tile_origin() is now the ONE definition of a tile's world corner, called by both writers. The two axes are not symmetric -- tx indexes z, ty indexes x -- so a second derivation next to the second writer is exactly how they would drift apart. VERIFIED byte-identical: 60 tiles sampled at random from the live cache, rewritten through the new path and sha256'd against the files the old converter produced. 60 identical, 0 mismatch. Backfill against the live directory writes 0, and into an empty one writes 841 = the live count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4204207f9e7021544a4747ea144fb0aa6b66e0b1
by plankatron <fleeky@gmail.com> · 2026-09-03T21:40:29.000Z

Changed files (3) +75 -5

server.py +12 -1
……@@ -47,6 +47,12 @@ HERE = os.path.dirname(os.path.abspath(__file__))
4747TILE_CACHE = os.environ.get("LOOKDEV_TILE_CACHE",
4848 os.path.expanduser(
4949 "~/.local/share/godot/app_userdata/HyperTPS/tile_cache/1234"))
50# Where the ENGINE reads. map_dc_diffusion.gd's TILE_DIR is
51# user://tile_cache/1234_hblk, and user:// for this project is
52# ~/.local/share/godot/app_userdata/HyperTPS -- so the default is the .npy
53# cache path with _hblk appended, and the two stay together if either moves.
54HBLK_CACHE = os.environ.get("LOOKDEV_HBLK_CACHE", TILE_CACHE + "_hblk")
55
5056DC_TERRAIN = os.path.abspath(os.path.join(HERE, "..", "dc-terrain"))
5157INC = os.path.abspath(os.path.join(HERE, "..", "proc-nodes", "src", "shared",
5258 "erosion_filter.inc"))
……@@ -130,9 +136,14 @@ def tiles():
130136 def _fetch(i1, j1, n):
131137 return fetch_diffusion(app.config["TD_URL"], i1, j1, n)[:2]
132138 _tiles = TileStore(_fetch, TILE_CACHE,
133 tile_px=int(app.config.get("TILE_PX", 256)))
139 tile_px=int(app.config.get("TILE_PX", 256)),
140 hblk_dir=HBLK_CACHE)
134141 print(f"tile cache: {TILE_CACHE} tile {_tiles.tile_px} px "
135142 f"= {_tiles.tile_px*30/1000:.2f} km")
143 print(f"hblk out: {HBLK_CACHE}")
144 n = _tiles.backfill_hblk()
145 if n:
146 print(f"backfilled {n} .hblk from the existing cache")
136147 return _tiles
137148
138149# Every knob the renderer will accept, with its type and a sane range. The
stitch_block.py +12 -2
……@@ -105,11 +105,21 @@ def stitch(tx0, ty0, n, cache=CACHE, tile_px=TILE_PX):
105105 # the only way to get it at all.
106106 r0, c0 = i * tile_px, j * tile_px
107107 out[r0:r0 + tile_px + 2, c0:c0 + tile_px + 2] = t
108 origin_z = (tx0 * tile_px - APRON) * MPP
109 origin_x = (ty0 * tile_px - APRON) * MPP
108 origin_x, origin_z = tile_origin(tx0, ty0, tile_px)
110109 return out, origin_x, origin_z
111110
112111
112def tile_origin(tx, ty, tile_px=TILE_PX, mpp=MPP, apron=APRON):
113 """World-space origin (x, z) in metres of a tile's APRON corner.
114
115 ONE definition, because it is the only thing tying a .hblk to a place and
116 the two axes are NOT symmetric: tx indexes z, ty indexes x. Deriving it a
117 second time next to the second writer is how the two would drift.
118 """
119 return ((ty * tile_px - apron) * mpp, # origin_x
120 (tx * tile_px - apron) * mpp) # origin_z
121
122
113123def write_hblk(path, arr, origin_x, origin_z, mpp=MPP):
114124 side = arr.shape[0]
115125 hdr = struct.pack("<4sIIfffII", MAGIC, VERSION, side, mpp,
tiles.py +51 -2
……@@ -36,19 +36,28 @@ DESIGN, and the reasons are measured:
3636 the wrong model for a queue whose ordering depends on a moving camera.
3737 The worker now picks the nearest to the CURRENT centre each iteration.
3838"""
39import os, queue, threading, time
39import os, queue, re, threading, time
4040import numpy as np
4141
4242TILE_M = 30.0 # model native metres per pixel
4343
4444
4545class TileStore:
46 def __init__(self, fetch_fn, cache_dir, tile_px=256, log=print):
46 def __init__(self, fetch_fn, cache_dir, tile_px=256, log=print,
47 hblk_dir=None):
4748 self.fetch = fetch_fn # (i1, j1, n) -> (elev, climate)
4849 self.dir = cache_dir
4950 self.tile_px = tile_px
5051 self.log = log
52 # PUBLISH AS WE GENERATE, because the engine reads a DIFFERENT
53 # directory in a DIFFERENT format, and a manual conversion step between
54 # the two is a step that gets forgotten. Browsing then looks like it did
55 # nothing. The .npy cache stays authoritative -- .hblk is a derived
56 # view, regenerable from it at any time by stitch_block --per-tile.
57 self.hblk_dir = hblk_dir
5158 os.makedirs(self.dir, exist_ok=True)
59 if self.hblk_dir:
60 os.makedirs(self.hblk_dir, exist_ok=True)
5261 self._mem = {} # (tx,ty) -> np.int16 array
5362 self._state = {} # (tx,ty) -> 'cached'|'queued'|'busy'|'error'
5463 self._want = {} # (tx,ty) -> True, the pending set
……@@ -129,6 +138,45 @@ class TileStore:
129138 return k
130139
131140 # ---- the single worker -------------------------------------------------
141 def hblk_path(self, tx, ty):
142 return os.path.join(self.hblk_dir, f"tile_{tx}_{ty}.hblk")
143
144 def _publish_hblk(self, tx, ty, arr):
145 """Write the engine-format twin. Never fatal: the .npy is the cache."""
146 if not self.hblk_dir:
147 return
148 try:
149 from stitch_block import write_hblk, tile_origin
150 ox, oz = tile_origin(tx, ty, self.tile_px, TILE_M, self.APRON)
151 write_hblk(self.hblk_path(tx, ty), arr, ox, oz, TILE_M)
152 except Exception as ex:
153 self.log(f" tile ({tx},{ty}) hblk FAILED: {ex}")
154
155 def backfill_hblk(self):
156 """Publish every cached .npy that has no .hblk yet.
157
158 The cache predates this path by 841 tiles, and a legacy prefix sits
159 alongside the current one -- so walk the SAME pattern the converter
160 does rather than every file in the directory.
161 """
162 if not self.hblk_dir:
163 return 0
164 pat = re.compile(rf"^t2_{self.tile_px}_(-?\d+)_(-?\d+)\.npy$")
165 n = 0
166 for f in os.listdir(self.dir):
167 m = pat.match(f)
168 if not m:
169 continue
170 tx, ty = int(m.group(1)), int(m.group(2))
171 if os.path.exists(self.hblk_path(tx, ty)):
172 continue
173 try:
174 self._publish_hblk(tx, ty, np.load(os.path.join(self.dir, f)))
175 n += 1
176 except Exception as ex:
177 self.log(f" backfill ({tx},{ty}) FAILED: {ex}")
178 return n
179
132180 def _run(self):
133181 while True:
134182 k = self._take_nearest()
……@@ -146,6 +194,7 @@ class TileStore:
146194 e, _c = self.fetch(tx * n - a_, ty * n - a_, n + 2 * a_)
147195 a = np.clip(e, -32768, 32767).astype(np.int16)
148196 np.save(self.path(tx, ty), a)
197 self._publish_hblk(tx, ty, a)
149198 self._mem[(tx, ty)] = a
150199 dt = time.time() - t0
151200 self.stats["generated"] += 1