perf: the column memo threw itself away every chunk

Jahni measured what I had only flagged: generation is slower on the op-stack path.
Two compounding causes.

The memo was keyed on InstanceId, which changes on every stack rebuild — every chunk.
GSurfColCache, the cache this path replaced, is keyed on (XY box, StrateKey, Seed,
LayoutVersion) with no ChunkZ, deliberately shared down the whole vertical strate
stack. So a 4-chunk strate recomputed every column four times, including the cliff's
four extra structural samples per column.

And the table held 256 entries where a chunk is CHUNK_SIZE^2 = 1024 columns, so it
thrashed against itself within a single tile before any cross-chunk question arose.

PrepareChunk now derives a shared ColumnKey from (StrateBottomWorldZ, LayoutVersion,
Seed) — the same identity GSurfColCache uses — and the table is 4096 entries
(~150 KB/worker, in line with GSurfColCache's 6 x 59 KB). The memo is thread_local so
it already survived rebuilds; only the key was discarding the contents.

Sharing across chunk Z is sound because heights are XY-pure by type and the biome
field is documented Z-independent — the same justification GSurfColCache rests on.

ColumnKey starts at InstanceId rather than 0: slots initialise to Key = 0, so a zero
key would falsely hit the pristine slot at (0,0). Without PrepareChunk you get
per-instance caching, which is less sharing but still correct.

This may not close the gap entirely and I am not claiming it does. Virtual dispatch
and the hashed lookup vs a direct-indexed box both remain; they are smaller than a 4x
column recompute, but "smaller" is a guess until measured.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:59:37 +02:00
parent c277931a08
commit f3faa3b5c2
2 changed files with 107 additions and 12 deletions
+45
View File
@@ -1631,3 +1631,48 @@ the case that was guarded off until now.
`FSdfRoughnessMod` and `FSdfCarve` unchanged from Maze, so it should be the cheapest of the eight.
---
## 2026-07-27 — SurfaceWorld verified visually. And Jahni measured the thing I had only flagged.
All 10 green, biome checks now visible and correct (blend bit-exact across the whole weight sweep;
ceiling selects rather than blends at weight 1.0). Jahni: *"the opstack looks similar if not
identical to the old terrain"* — the biome case included, which is what step 2c added.
**And: *"it took a bit more time generating with the opstack."*** That is a real regression, it was
predictable, and I had flagged it as "pending" rather than fixed. Diagnosed, two compounding causes:
**1. The memo key invalidated on every chunk.** It was keyed on `InstanceId`, which changes on every
stack rebuild — i.e. every chunk. `GSurfColCache`, the path it replaced, is keyed on
`(XY box, StrateKey, Seed, LayoutVersion)` with **no ChunkZ**, deliberately *"shared down the whole
vertical strate stack"*. So a 4-chunk-tall strate recomputed **every column four times**, cliff
resamples included — and the cliff costs four extra structural samples per column.
**2. The table could not hold one chunk.** A chunk is `CHUNK_SIZE²` = 1024 columns; the table had
**256** entries. It thrashed against itself *within a single tile*, before any cross-chunk question.
**Fixed:** `PrepareChunk` now derives a **shared** `ColumnKey` from `(StrateBottomWorldZ,
LayoutVersion, Seed)` — the same identity `GSurfColCache` uses — and the table is 4096 entries
(~150 KB/worker, in line with `GSurfColCache`'s 6 × 59 KB). The memo is `thread_local`, so it
already survived stack rebuilds; **only the key was throwing the contents away.**
**Why sharing across chunk Z is sound:** heights are XY-pure *by type* (the whole point of
`VoxelHeightOp.h` — there is no Z in the signature), and the biome field is documented
Z-independent (*"ZERO Z dependence: the climate/Voronoi fields are pure-XY"*). That is precisely the
justification `GSurfColCache` already rests on.
**Un-prepared safety:** `ColumnKey` starts at `InstanceId` rather than 0, because slots initialise to
`Key = 0` and a zero key would falsely hit the pristine slot at (0,0). Without `PrepareChunk` you
get per-instance caching — less sharing, still correct. Degrade, never lie.
**⚠️ This may not close the gap entirely, and I am not claiming it does.** Virtual dispatch (5 ops
per voxel) and the hashed lookup versus the original's direct-indexed box both remain. Those are
smaller than a 4× column recompute, but "smaller" is a guess until measured. **The next generation
timing is the measurement** — if it is still slower, the remaining suspects in order are: the box
cache's direct indexing vs. my hash, then per-voxel virtual calls.
**UNVERIFIED:** not compiled.
**Next single action:** build, regenerate, and compare generation time against the switch path with
the flag off. Then `VerticalShafts` (§6).
---