OverviewFilesCommitsIssuesDocsReleasesPeers

terrain-lookdev commits / c79ca8f

lookdev: the seed field could change the world by accident
Reported as "it looks more like part of an island that touches the sea instead of a set of mountain ranges". The model output was bit-identical to the very first image captured hours earlier (pixel correlation 1.000000, max diff 0/255). The seed had changed underneath it: World seed changed to: 1 World seed changed to: 100 seed 1234 at (0, 2048): 261 .. 2348 m, mountain ranges seed 100 at (0, 2048): 153 .. 918 m, low rolling hills -- the "island" CAUSE, and it is mine. The seed control added earlier sent $("seed").value on EVERY request, and the generic input handler re-renders on any input event. A number input changes on mouse-wheel and on arrow keys, so scrolling the sidebar with the cursor over that field silently regenerated the world -- and a seed change also calls change_seed(), which flushes every cached tile, so it is expensive as well as destructive. FIX: the field is now a DRAFT and ACTIVE_SEED is what gets sent. It is applied only by Enter or the Apply button. Wheel over the field is refused outright and blurs it, arrow keys are swallowed, and the active seed is displayed beside the input so a divergence between what is typed and what is rendered is visible rather than silent. The general shape, which this session has now hit three times: a control whose value can change without the user intending it is not a control, it is a hazard -- especially when the thing it changes is expensive and looks like a rendering bug rather than a state change. Also lands the ramp fix from the same report queue: colourise() fitted its colour ramp per tile, so panning one step re-normalised and the colours jumped even though the terrain was identical. That is exactly what upstream terrain-diffusion issue #19 reported as "tile inconsistencies" before the author identified it as the colour map. vmin/vmax now pin it, behind a "lock ramp" control that captures the current range and holds it across pans and both panels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c79ca8faebdbf47af711c7e4e9c14e004604f5b9
by plankatron <noreply@anthropic.com> · 2026-08-31T18:30:41.000Z

Changed files (3) +119 -27

bench_diffusion.py +62 -20
……@@ -27,7 +27,8 @@ PORT = 8819
2727MODEL = "xandergos/terrain-diffusion-30m"
2828VENV = "/storage/projects/code/godot/terrain-diffusion/.venv/bin/python"
2929CWD = "/storage/projects/code/godot/terrain-diffusion"
30ORIGIN = 21_000_000 # far from anything this session has touched
30ORIGIN = 51_000_000
31CHECK_AT = 55_555_000 # one fixed region every config is validated on # far from anything this session has touched
3132
3233
3334def wait_up(proc, timeout=600):
……@@ -43,6 +44,38 @@ def wait_up(proc, timeout=600):
4344 return False
4445
4546
47REF = {} # reference tile from a config known to be correct
48
49
50def tile_data(i1, j1, n):
51 """Elevation + climate, for CORRECTNESS checking."""
52 import numpy as np
53 u = (f"http://127.0.0.1:{PORT}/terrain?i1={i1}&j1={j1}"
54 f"&i2={i1+n}&j2={j1+n}&scale=1")
55 with urllib.request.urlopen(u, timeout=3600) as r:
56 h = int(r.headers["X-Height"]); w = int(r.headers["X-Width"]); b = r.read()
57 e = np.frombuffer(b[:h*w*2], dtype="<i2").astype(float).reshape(h, w)
58 c = np.frombuffer(b[h*w*2:], dtype="<f4").reshape(h, w, 4) if len(b) > h*w*2 else None
59 return e, c
60
61
62def correctness(i1, j1, n=256):
63 """A config that returns ZEROS is infinitely fast. This is the gate that was
64 missing from the first three runs, and its absence shipped a 14x result that
65 was 70% blank tile."""
66 import numpy as np
67 e, c = tile_data(i1, j1, n)
68 zeros = float((e == 0).mean())
69 nans = int(np.isnan(c).sum()) if c is not None else 0
70 corr = float("nan")
71 if "e" in REF and REF["e"].shape == e.shape:
72 a, b_ = REF["e"].ravel(), e.ravel()
73 if a.std() > 0 and b_.std() > 0:
74 corr = float(np.corrcoef(a, b_)[0, 1])
75 ok = zeros < 0.02 and nans == 0 and (corr != corr or corr > 0.98)
76 return dict(zeros=zeros, nans=nans, corr=corr, ok=ok)
77
78
4679def tile(i1, j1, n):
4780 u = (f"http://127.0.0.1:{PORT}/terrain?i1={i1}&j1={j1}"
4881 f"&i2={i1+n}&j2={j1+n}&scale=1")
……@@ -103,11 +136,17 @@ def run(label, extra, n=512, reps=3, offset=0):
103136 warm = tile(base, base, 256) # pay compile / lazy init here
104137 ts = [tile(base + 20_000*(k+1), base + 20_000*(k+1), n) for k in range(reps)]
105138 mem = gpu_mem_of(p.pid)
139 # ALWAYS the same coordinates, so every config is checked on one region.
140 ck = correctness(CHECK_AT, CHECK_AT)
141 if not REF and ck["zeros"] < 0.02 and ck["nans"] == 0:
142 REF["e"], _ = tile_data(CHECK_AT, CHECK_AT, 256)
106143 best, avg = min(ts), sum(ts)/len(ts)
107144 km2 = (n*30/1000)**2
108 print(f" {label:<28} warm {warm:6.1f}s tile {avg:6.2f}s (best {best:5.2f})"
109 f" {km2/avg:6.1f} km2/s {mem:5d} MiB")
110 return dict(label=label, warm=warm, avg=avg, best=best, mem=mem)
145 flag = "ok " if ck["ok"] else "BAD "
146 print(f" {label:<26} {flag} tile {avg:6.2f}s {km2/avg:6.1f} km2/s "
147 f"{mem:5d} MiB zeros {100*ck['zeros']:5.1f}% nan {ck['nans']:6d} "
148 f"corr {ck['corr']:.4f}")
149 return dict(label=label, avg=avg, best=best, mem=mem, **ck)
111150 finally:
112151 try: os.killpg(os.getpgid(p.pid), signal.SIGTERM)
113152 except Exception: pass
……@@ -119,28 +158,31 @@ BASELINE_PIDS = set(gpu_apps().keys()) # the always-on service, not ours to wa
119158
120159if __name__ == "__main__":
121160 print(f"512 tile = 15.36 km across = 236 km2. Fresh coordinates every time.\n")
122 print(f" {'config':<28} {'model load':>11} {'per tile':>22} {'throughput':>12} {'VRAM':>9}")
161 print(f" {'config':<26} {'ok?':<4} {'per tile':>12} {'throughput':>11} "
162 f"{'VRAM':>9} correctness")
123163 rows = []
164 os.environ.pop("TD_COMPILE_MODE", None)
124165 F16 = ["--no-compile", "--dtype", "fp16", "--kwarg", "onestep_latent=True"]
125 D = lambda n: F16 + ["--kwarg", f"decoder_tile_size={n}"]
126 C = lambda n: ["--compile", "--dtype", "fp16", "--kwarg", "onestep_latent=True",
127 "--kwarg", f"decoder_tile_size={n}"]
128166 for i, (label, extra) in enumerate([
129 ("decoder_tile 256 (winner)", D(256)),
130 ("decoder_tile 192", D(192)),
131 ("decoder_tile 128", D(128)),
132 ("decoder_tile 64", D(64)),
133 ("256 + compile default", C(256)),
134 ("256 + compile reduce-ovh", C(256)),
167 ("fp16+onestep (reference)", F16), # known good
168 (" + decoder_tile 256", F16 + ["--kwarg", "decoder_tile_size=256"]),
169 (" + decoder_tile 128", F16 + ["--kwarg", "decoder_tile_size=128"]),
170 (" + compile only", ["--compile", "--dtype", "fp16",
171 "--kwarg", "onestep_latent=True"]),
172 (" + compile + dtile 256", ["--compile", "--dtype", "fp16",
173 "--kwarg", "onestep_latent=True",
174 "--kwarg", "decoder_tile_size=256"]),
175 ("fp32 plain (control)", ["--no-compile"]),
135176 ]):
136 if "reduce-ovh" in label:
137 os.environ["TD_COMPILE_MODE"] = "reduce-overhead"
138 else:
139 os.environ.pop("TD_COMPILE_MODE", None)
140177 r = run(label, extra, offset=i)
141178 if r: rows.append(r)
142179 if rows:
143180 b = rows[0]["avg"]
144 print("\n speedup vs baseline:")
181 print("\n speedup vs the reference config — ONLY meaningful where ok:")
145182 for r in rows:
146 print(f" {r['label']:<28} {b/r['avg']:5.2f}x (VRAM {r['mem']} MiB)")
183 print(f" {r['label']:<26} {'ok ' if r['ok'] else 'BAD'} "
184 f"{b/r['avg']:5.2f}x VRAM {r['mem']:5d} MiB")
185 bad = [r["label"] for r in rows if not r["ok"]]
186 if bad:
187 print("\n DISCARD (wrong output, so the timing is meaningless): "
188 + ", ".join(x.strip() for x in bad))
index.html +41 -4
……@@ -100,8 +100,11 @@ kbd{font-family:"IBM Plex Mono",monospace;background:#0e1216;border:1px solid va
100100 <label>Diffusion i1 (px)<input type="number" id="i1" value="0" step="256"></label>
101101 <label>Diffusion j1 (px)<input type="number" id="j1" value="2048" step="256"></label>
102102 <label>Diffusion seed<input type="number" id="seed" value="1234" step="1">
103 <span class="d">a new seed is a whole new world. it rebuilds the pipeline and
104 clears every cached tile, so it is much slower than moving i1/j1.</span></label>
103 <span class="d">a whole new world, and it flushes every cached tile — so it
104 is applied only on Enter or the button below, never by scrolling or
105 arrow keys. Active seed is shown next to it.</span></label>
106 <div class="row"><button id="applyseed">Apply seed</button>
107 <span class="v" id="activeseed" style="align-self:center">1234</span></div>
105108 <div class="row">
106109 <button id="newloc">New location</button>
107110 <button id="newseed">New seed</button>
……@@ -153,6 +156,10 @@ kbd{font-family:"IBM Plex Mono",monospace;background:#0e1216;border:1px solid va
153156
154157<main>
155158 <div class="bar">
159 <label style="display:flex;gap:5px;align-items:center;font-size:11.5px">
160 <input type="checkbox" id="lockramp" style="width:auto"> lock ramp
161 <span id="ramptxt" style="font-family:'IBM Plex Mono',monospace;font-size:10px;color:var(--dim)"></span>
162 </label>
156163 <div class="views" id="modes">
157164 <button data-m="2d" aria-pressed="true">2D</button>
158165 <button data-m="3d" aria-pressed="false">3D</button>
……@@ -335,6 +342,8 @@ function updateDerived() {
335342}
336343let view = "shaded-height";
337344let mode = "2d";
345let RAMP = null; // pinned colour ramp [lo, hi], null = fit per tile
346let ACTIVE_SEED = "1234"; // what the server is actually on; the field is a draft
338347
339348for (const [grp, items] of Object.entries(GROUPS)) {
340349 const host = $("grp_" + grp);
……@@ -379,6 +388,12 @@ async function load(which, url, imgId, stId, busyId) {
379388 if (old.startsWith("blob:")) URL.revokeObjectURL(old);
380389 const h = r.headers;
381390 const ms = (performance.now() - t0).toFixed(0);
391 // Capture the ramp the first time it is locked, so both panels and every
392 // subsequent pan use the SAME scale.
393 if ($("lockramp").checked && !RAMP && h.get("X-Ramp")) {
394 RAMP = h.get("X-Ramp").split(",").map(Number);
395 $("ramptxt").textContent = `${RAMP[0].toFixed(0)} .. ${RAMP[1].toFixed(0)} m`;
396 }
382397 let s = `min ${h.get("X-Min")} max ${h.get("X-Max")} ` +
383398 `mean ${h.get("X-Mean")} sd ${h.get("X-Sd")} m ${ms} ms`;
384399 if (h.get("X-Climate")) s += `\n${h.get("X-Climate")}`;
……@@ -546,12 +561,15 @@ function render() {
546561 syncLabels();
547562 const p = erosionQuery();
548563 const q = new URLSearchParams({n: $("n").value, i1: $("i1").value,
549 j1: $("j1").value, seed: $("seed").value});
564 j1: $("j1").value, seed: ACTIVE_SEED});
550565 if ($("erode").checked) {
551566 q.set("erode", "1");
552567 for (const [key] of GROUPS.erode) q.set(key, $(key).value);
553568 }
554569
570 if (RAMP && $("lockramp").checked) {
571 for (const u of [p, q]) { u.set("vmin", RAMP[0]); u.set("vmax", RAMP[1]); }
572 }
555573 if (mode === "3d") {
556574 load3d("/api/erosion.bin?" + p, "cv_e", "st_e", "busy_e");
557575 load3d("/api/diffusion.bin?" + q, "cv_d", "st_d", "busy_d");
……@@ -571,9 +589,28 @@ function schedule() {
571589}
572590
573591$("erode").addEventListener("change", render);
592$("lockramp").addEventListener("change", () => {
593 if (!$("lockramp").checked) { RAMP = null; $("ramptxt").textContent = ""; }
594 render();
595});
574596document.addEventListener("input", e => {
597 if (e.target.id === "seed") return; // deliberate only — see below
575598 if (e.target.type === "range" || e.target.type === "number") schedule();
576599});
600// Wheel over a number input changes its value in most browsers. For the seed
601// that means silently regenerating the world, so refuse the gesture outright.
602$("seed").addEventListener("wheel", e => { e.preventDefault(); $("seed").blur(); },
603 {passive: false});
604$("seed").addEventListener("keydown", e => {
605 if (e.key === "ArrowUp" || e.key === "ArrowDown") e.preventDefault();
606 if (e.key === "Enter") { e.preventDefault(); applySeed(); }
607});
608function applySeed() {
609 ACTIVE_SEED = $("seed").value;
610 $("activeseed").textContent = ACTIVE_SEED;
611 render();
612}
613$("applyseed").onclick = applySeed;
577614$("render").onclick = render;
578615// Two different ways to get different terrain, and they cost very differently.
579616$("newloc").onclick = () => {
……@@ -583,7 +620,7 @@ $("newloc").onclick = () => {
583620};
584621$("newseed").onclick = () => {
585622 $("seed").value = Math.floor(Math.random() * 1e9);
586 render();
623 applySeed();
587624};
588625$("reset").onclick = () => {
589626 for (const items of Object.values(GROUPS))
server.py +16 -3
……@@ -257,7 +257,18 @@ def to_png(rgb):
257257
258258
259259def colourise(h, mpp, view, vmin=None, vmax=None):
260 """view: shade | height | slope | shaded-height"""
260 """view: shade | height | slope | shaded-height
261
262 vmin/vmax PIN the ramp. Without them the ramp is fitted to this tile, so
263 panning one step re-normalises and the colours jump even though the terrain
264 is identical -- which is exactly what terrain-diffusion issue #19 reported
265 as "tile inconsistencies" before the author identified it as the colour map.
266 An instrument that changes its scale between readings is not an instrument.
267 """
268 if vmin in ("", None): vmin = None
269 if vmax in ("", None): vmax = None
270 vmin = float(vmin) if vmin is not None else None
271 vmax = float(vmax) if vmax is not None else None
261272 finite = h[np.isfinite(h)]
262273 lo = float(vmin) if vmin is not None else (float(finite.min()) if finite.size else 0.0)
263274 hi = float(vmax) if vmax is not None else (float(finite.max()) if finite.size else 1.0)
……@@ -314,7 +325,8 @@ def api_erosion():
314325 steps = int(_clamp(float(a.get("steps", 256)), 32, 1024))
315326 knobs = parse_knobs(a)
316327 h = render_erosion(ox, oz, n, mpp, knobs, steps, 40)
317 png, lo, hi = colourise(h, mpp, a.get("view", "shaded-height"))
328 png, lo, hi = colourise(h, mpp, a.get("view", "shaded-height"),
329 a.get("vmin"), a.get("vmax"))
318330 return stats_headers(Response(png, mimetype="image/png"), h, lo, hi,
319331 {"X-Source": "erosion_render"})
320332 except Exception as e:
……@@ -336,7 +348,8 @@ def api_diffusion():
336348 h, _ridge = erode_field(h, mpp_d, a)
337349 extra["X-Source"] = "terrain-diffusion + erosion_filter"
338350 extra["X-Mpp"] = repr(mpp_d)
339 png, lo, hi = colourise(h, mpp_d, a.get("view", "shaded-height"))
351 png, lo, hi = colourise(h, mpp_d, a.get("view", "shaded-height"),
352 a.get("vmin"), a.get("vmax"))
340353 if climate is not None:
341354 extra["X-Climate"] = (f"{climate[...,0].mean():.1f}C,"
342355 f"{climate[...,1].mean():.0f},"