docs(audit C2): the within-strate stale-room suspicion is CONFIRMED by reading -- and it is the default config
No code changed. AUDIT-2026-07.md only: §C2's "SUSPECTED, NOT PROVEN" sub-item becomes "CONFIRMED BY READING", with the four links of the chain and where each one is in the source. THE CHAIN, checked instead of believed 1. GetGenerationParams' Gradient arms compute Alpha = 1 - DistFromBottom / EffectiveBlend, where DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ. Alpha is therefore a function of chunk Z WITHIN the slot -- so two chunk Zs in the same strate really do get different params. The top boundary mirrors it. 2. They differ in exactly the fields that place rooms: Lerp expands VF_STRATE_PARAM_FIELDS, which lists RoomSpacing, RoomDensity, MinRoomRadius, MaxRoomRadius, RoomHeightRatio, RoomShapeVariety, SDFBlendRadius, CaveWarpStrength and the tunnel fields -- every input BuildChunkCache reads. 3. It is the DEFAULT: TransitionType = Gradient, TransitionBlendChunks = 2. Two chunk-Z layers with distinct params at each end of every strate, out of the box. 4. StrateIndex cannot save it: the memo resolves the SLOT index from the band centre, identical for every chunk Z in the slot, and the cache key's XY box does not change as a worker walks down a column. No rebuild happens. So a worker that builds (X,Y,Z1) then (X,Y,Z2) in one strate evaluates the second chunk against the room list baked from the first chunk's params. WHY IT MATTERS MORE THAN A SEAM The result depends on which chunk that worker happened to build first. That is a window-invariance break (ARCHITECTURE 8.4), not a cosmetic one, and in multiplayer two peers can generate different geometry for the same chunk from the same seed -- a direct 2.6.1 violation on the ORIGINAL path. WHY NOTHING CAUGHT IT The test fixture sets TransitionType = Hard on every strate deliberately, so that "which archetype owns this chunk" stays unambiguous. That switches the blend off entirely. The one configuration the tests never build is the default one. NOT FIXED HERE, deliberately: this is the switch path, and the fix (fold a params CRC + LayoutVersion into the SDF cache key, exactly as FRoomGraphSource already does) is a behaviour change to live generation that Jahni should land with a build in front of him. The operator stack does not inherit the bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+41
-14
@@ -182,24 +182,51 @@ Seed) **are** seed-guarded, and the strate-index memo **is** version-guarded (`S
|
|||||||
`ChangeSeed` mostly survives; **live-edit is where this bites**. Symptom: "I tweaked the strate asset,
|
`ChangeSeed` mostly survives; **live-edit is where this bites**. Symptom: "I tweaked the strate asset,
|
||||||
regenerated, and one patch kept the old shape."
|
regenerated, and one patch kept the old shape."
|
||||||
|
|
||||||
#### ⚠️ SUSPECTED, NOT PROVEN, 2026-07-28 — the SDF cache may serve stale rooms *within* a strate
|
#### ✅ CONFIRMED BY READING, 2026-07-28 — the SDF cache DOES serve stale rooms *within* a strate
|
||||||
|
|
||||||
Found while porting TunnelNetwork, and stated as a suspicion on purpose: I have reasoned it, not
|
Raised as a suspicion while porting TunnelNetwork, then checked rather than believed. **It holds**,
|
||||||
measured it.
|
on default settings, with no live edit and no unusual asset configuration.
|
||||||
|
|
||||||
`GetDensityWithParams`' SDF cache key is `(XY search box, StrateIndex, Seed)`. It contains **no
|
The chain, each link read in the code rather than inferred:
|
||||||
params and no chunk Z**. Meanwhile `GetGenerationParams` *blends* params between neighbouring strates
|
|
||||||
across a Gradient transition — so two chunks at different Z **inside the same strate** can carry
|
|
||||||
different `RoomSpacing`/`RoomDensity`/… while sharing an XY box, a strate index and a seed.
|
|
||||||
|
|
||||||
If that is right, a worker descending a transition band gets **no rebuild** and evaluates the lower
|
1. **`GetGenerationParams` really does vary within one strate.** Its Gradient branch computes
|
||||||
chunk against the upper chunk's room layout. Same family as `§C2` above and as the overhang
|
`Alpha = 1 − DistFromBottom / EffectiveBlend` where `DistFromBottom = ChunkCoord.Z −
|
||||||
regression of 2026-07-27; the difference is that this one needs no live edit to trigger.
|
Slot.BottomChunkZ`, then returns `FStrateGenerationParams::Lerp(BaseParams, BelowParams, Alpha)`.
|
||||||
|
`Alpha` is a function of **chunk Z inside the slot**, so two chunk Zs in the *same* slot get
|
||||||
|
genuinely different params. The top boundary mirrors it.
|
||||||
|
*(`VoxelStrateManager.cpp`, `GetGenerationParams`, the `case EVoxelStrateTransition::Gradient:`
|
||||||
|
arms — both of them.)*
|
||||||
|
2. **It varies in exactly the fields that place rooms.** `Lerp` expands the
|
||||||
|
`VF_STRATE_PARAM_FIELDS` X-macro, which lists `RoomSpacing`, `RoomDensity`, `MinRoomRadius`,
|
||||||
|
`MaxRoomRadius`, `RoomHeightRatio`, `RoomShapeVariety`, `SDFBlendRadius`, `CaveWarpStrength`,
|
||||||
|
the tunnel fields — every input `BuildChunkCache` reads.
|
||||||
|
3. **And it is the DEFAULT.** `UVoxelStrateDefinition::TransitionType = Gradient` and
|
||||||
|
`TransitionBlendChunks = 2`. So at each end of every strate there are two chunk-Z layers with
|
||||||
|
distinct params (`Alpha` 1.0 and 0.5), out of the box.
|
||||||
|
4. **`StrateIndex` cannot save it.** The memo resolves the *slot* index from the band centre, so it
|
||||||
|
is identical for every chunk Z within the slot. The cache key is `(XY box, StrateIndex, Seed)`
|
||||||
|
and the XY box does not change when the worker moves down a column. **No rebuild happens.**
|
||||||
|
|
||||||
**What would confirm it:** call `GetGenerationParams` for two adjacent chunk Zs inside a
|
⇒ A worker that generates `(X, Y, Z1)` and then `(X, Y, Z2)` in the same strate and the same XY box
|
||||||
Gradient-transitioned strate and compare the room-placement fields. If they differ, the cache is
|
evaluates the second chunk against **the room list baked from the first chunk's params**.
|
||||||
being reused across a real param change. **Do that before acting** — the whole thing rests on
|
|
||||||
"Gradient blending actually varies within a strate", which I have not verified.
|
**Two consequences, and the second is the serious one:**
|
||||||
|
- a visible seam / wrong room sizes in the transition band, and
|
||||||
|
- **the result depends on which chunk that worker happened to build first.** That is a window-
|
||||||
|
invariance break (`ARCHITECTURE §8.4`) rather than a cosmetic bug, and in multiplayer it means two
|
||||||
|
peers can generate different geometry for the same chunk from the same seed — a direct
|
||||||
|
`OPSTACK-PLAN §2.6.1` violation on the *original* path.
|
||||||
|
|
||||||
|
**Why nothing caught it:** the test fixture sets `TransitionType = Hard` on every strate on purpose
|
||||||
|
(so "which archetype owns this chunk" stays unambiguous), which switches the blend off entirely.
|
||||||
|
The one configuration the tests never build is the default one.
|
||||||
|
|
||||||
|
**Fix (unimplemented, deliberately — this is the `switch` path, not the port):** fold the params into
|
||||||
|
the SDF cache key exactly as `FRoomGraphSource` already does — `FCrc::MemCrc32` over the params
|
||||||
|
struct, plus `LayoutVersion`. `FStrateGenerationParams` is pure POD, so a memory CRC cannot produce a
|
||||||
|
false *match*; at worst padding causes a needless rebuild. Err on CPU, never on a wrong room.
|
||||||
|
Alternatively add chunk Z to the key, which is coarser (it rebuilds on every Z step even outside a
|
||||||
|
blend band) but needs no CRC.
|
||||||
|
|
||||||
The operator-stack port does **not** inherit this: `FRoomGraphSource` folds a `FCrc::MemCrc32`
|
The operator-stack port does **not** inherit this: `FRoomGraphSource` folds a `FCrc::MemCrc32`
|
||||||
fingerprint of the params (plus `LayoutVersion`) into its key, so differing params force a rebuild.
|
fingerprint of the params (plus `LayoutVersion`) into its key, so differing params force a rebuild.
|
||||||
|
|||||||
Reference in New Issue
Block a user