fix: call PrepareChunk and guard the degenerate strate on the wired path
Read the step-3 wiring against the equivalence test before spending a build. The symbols all line up; the two PATHS did not. - GetDensityAt never called FVoxelOpStack::PrepareChunk, though the test does. All seven concrete bodies are empty today so behaviour is unchanged — which is the reason to fix it now: the first op to hoist real per-chunk work would have been green in test and silently wrong in game. Builds an FVoxelOpContext in the same refetch block (chunk, seed, layout version, strate Z bounds). Step stays 1; GetDensityAt does not know the mesher's sampling step (T2.b). - GetMazeDensity early-outs to air on a degenerate strate (height <= 0) and the stack has no such early-out by design. Unguarded that is air on one path and spine/seal-of-a-zero-height-band on the other, so the wired path now falls back to the switch there — the reference behaviour is the behaviour. Docs: VoxelDensityOpStack.h's banner still claimed nothing here feeds the game, and CODEMAP 3.2d repeated it. Both now state what is wired (GetDensityAt) and what is not (ClassifyTile, hand-written guards, Phase 2), with C10's never-compare rule at the point of use. CODEMAP gains UsesOperatorStackForChunk and bUseOperatorStack rows, and BuildMazeStack's degenerate-strate precondition. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+12
-4
@@ -118,8 +118,13 @@ stack share ONE copy. `VoxelGenerator.cpp` keeps same-named `static FORCEINLINE`
|
|||||||
**Convention: INTERNAL (positive = solid).**
|
**Convention: INTERNAL (positive = solid).**
|
||||||
|
|
||||||
### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp`
|
### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp`
|
||||||
⚠️ **Feeds nothing yet.** `GetDensityAt`/`ClassifyTile` are untouched; the archetype `switch` is
|
⚠️ **Feeds the game, behind a per-strate opt-in** (Phase 1 step 3). `GetDensityAt` builds the stack
|
||||||
still the only production path. Exercised solely by `VoxelForge.OpStack.MazeEquivalence`.
|
in its per-chunk refetch block and evaluates it *instead of* the `switch` only when
|
||||||
|
`UVoxelStrateManager::UsesOperatorStackForChunk` says so — strate ticked `bUseOperatorStack` **and**
|
||||||
|
archetype in the ported list (**Maze only**). Everything else still takes the `switch`, unchanged.
|
||||||
|
**`ClassifyTile` is NOT wired** — still hand-written guards, not `ClassifyBox`. That is Phase 2.
|
||||||
|
⛔ Never run both paths in one world, and never compare them for equality: the ~1 ULP residue is
|
||||||
|
inherent (AUDIT §C10). The acceptance bar is visual (OPSTACK-PLAN §2.6).
|
||||||
|
|
||||||
| Symbol | Role | Notes |
|
| Symbol | Role | Notes |
|
||||||
|--------|------|-------|
|
|--------|------|-------|
|
||||||
@@ -129,7 +134,7 @@ still the only production path. Exercised solely by `VoxelForge.OpStack.MazeEqui
|
|||||||
| `VoxelDensityOps::MakeLatticeCorridorSource` | 1 | Maze corridors, SDF channel. Edge identity = `hash(lower node, axis)` ⇒ adjacent chunks cannot disagree (AUDIT §6.4's preferred pattern). Its `EffectOverBox` answers for the source+carve **pair** (Phase 1 simplification) so it must be told the downstream `ExtraReach`. |
|
| `VoxelDensityOps::MakeLatticeCorridorSource` | 1 | Maze corridors, SDF channel. Edge identity = `hash(lower node, axis)` ⇒ adjacent chunks cannot disagree (AUDIT §6.4's preferred pattern). Its `EffectOverBox` answers for the source+carve **pair** (Phase 1 simplification) so it must be told the downstream `ExtraReach`. |
|
||||||
| `VoxelDensityOps::MakeSdfRoughnessMod` | 3 | Wall roughness in **SDF** space (Maze/Shafts/Islands variant). TunnelNetwork's density-space roughness is a **different op** — see OPSTACK-DECOMPOSITION §1. |
|
| `VoxelDensityOps::MakeSdfRoughnessMod` | 3 | Wall roughness in **SDF** space (Maze/Shafts/Islands variant). TunnelNetwork's density-space roughness is a **different op** — see OPSTACK-DECOMPOSITION §1. |
|
||||||
| `VoxelDensityOps::MakeSdfCarve` | 2 | SDF → density carve. The same six lines currently copied in three archetypes. |
|
| `VoxelDensityOps::MakeSdfCarve` | 2 | SDF → density carve. The same six lines currently copied in three archetypes. |
|
||||||
| `VoxelDensityOps::BuildMazeStack` | — | The 7-op Maze stack. If this ever becomes one op, the refactor failed its own test (§2.5). |
|
| `VoxelDensityOps::BuildMazeStack` | — | The 7-op Maze stack. If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): `GetMazeDensity` early-outs to air there and the stack has no such early-out by design — `GetDensityAt` falls back to the `switch`. |
|
||||||
|
|
||||||
### 3.3 Chunk identity
|
### 3.3 Chunk identity
|
||||||
`VoxelChunk.h` (the old `FVoxelChunk` coord wrapper) was DELETED — dead since the tile
|
`VoxelChunk.h` (the old `FVoxelChunk` coord wrapper) was DELETED — dead since the tile
|
||||||
@@ -269,7 +274,9 @@ Header is rich with inline docs. Two namespaces + a per-chunk cache system.
|
|||||||
`TransitionType`(79)/`TransitionBlendChunks`(89), `GeneratorType`(102),
|
`TransitionType`(79)/`TransitionBlendChunks`(89), `GeneratorType`(102),
|
||||||
`GenerationParams`(113), `SlabParams`(124), `Biomes[]`+`BiomeMapParams` (the biome list +
|
`GenerationParams`(113), `SlabParams`(124), `Biomes[]`+`BiomeMapParams` (the biome list +
|
||||||
field tuning — empty ⇒ unchanged world, §8.14), `TerrainOperations`(147), visuals/fog/light,
|
field tuning — empty ⇒ unchanged world, §8.14), `TerrainOperations`(147), visuals/fog/light,
|
||||||
content lists, audio, `GameplayTags`(223). EditConditions show/hide param groups by generator type.
|
content lists, audio, `GameplayTags`(223), `bUseOperatorStack` (the OPSTACK A/B opt-in — only bites
|
||||||
|
if the archetype is in `UsesOperatorStackForChunk`'s ported list). EditConditions show/hide param
|
||||||
|
groups by generator type.
|
||||||
|
|
||||||
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
|
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
|
||||||
Maps depth→strate at runtime; owns passages.
|
Maps depth→strate at runtime; owns passages.
|
||||||
@@ -286,6 +293,7 @@ Maps depth→strate at runtime; owns passages.
|
|||||||
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize`). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so editor rebuilds never serve stale data. |
|
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize`). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so editor rebuilds never serve stale data. |
|
||||||
| `GetStrateForChunk` | 466 | Chunk → definition. |
|
| `GetStrateForChunk` | 466 | Chunk → definition. |
|
||||||
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
|
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
|
||||||
|
| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list. **That list is written down here and nowhere else** — an unported archetype ignores the flag, so ticking the box anywhere is harmless. Add a row here when you port one. |
|
||||||
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
|
||||||
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
|
||||||
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
|
||||||
|
|||||||
@@ -836,3 +836,43 @@ undiscovered, while it was worked out the slow way. Worth reading before clearin
|
|||||||
**Next single action unchanged:** build Phase 1 step 3, then the visual A/B on a Maze strate.
|
**Next single action unchanged:** build Phase 1 step 3, then the visual A/B on a Maze strate.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-27 — fresh context: two gaps closed in the step-3 wiring BEFORE the build.
|
||||||
|
|
||||||
|
Resumed from `OPSTACK-HANDOFF.md`. Read the step-3 diff against the test path instead of taking
|
||||||
|
"not compiled" as "nothing to check first" — the symbols all line up (`BuildMazeStack`'s five params
|
||||||
|
match, `Seed`/`OriginSpineRadius` are generator members, `FVoxelOpContext` comes in transitively via
|
||||||
|
`VoxelDensityOp.h`), but **the production path and the test path did not agree on two things.**
|
||||||
|
|
||||||
|
**1. `PrepareChunk` was never called in production.** The test calls it (`…MazeTest.cpp:133`);
|
||||||
|
`GetDensityAt` did not. All seven concrete `PrepareChunk` bodies are empty today, so this changes
|
||||||
|
**nothing** now — which is exactly why it was worth fixing before it could bite. The first op that
|
||||||
|
hoists real per-chunk work would have been **green in test and silently wrong in game**, and that
|
||||||
|
class of bug is expensive to find precisely because the test says yes. `GetDensityAt` now builds an
|
||||||
|
`FVoxelOpContext` (chunk, seed, layout version, strate Z bounds) in the same refetch block and calls
|
||||||
|
`PrepareChunk` on it. `Step` stays 1 — `GetDensityAt` genuinely does not know the mesher's sampling
|
||||||
|
step (T2.b contract); noted rather than guessed.
|
||||||
|
|
||||||
|
**2. The degenerate-strate early-out had no counterpart.** `GetMazeDensity` opens with
|
||||||
|
`if (StrateHeight <= 0.0f) return 1.0f;` — air. The stack has no such early-out **by design** (the
|
||||||
|
test asserts this and refuses to run on a degenerate strate). Unguarded, a zero-height Maze strate
|
||||||
|
would give **air on one path and whatever the spine/seal ops make of a zero-height band on the
|
||||||
|
other**. `GetDensityAt` now falls back to the `switch` in that case, so the reference behaviour is
|
||||||
|
the behaviour. Reachability is not the point — the archetype guard exists, so its port needs one.
|
||||||
|
|
||||||
|
**Docs corrected, since both were now actively false:**
|
||||||
|
- `VoxelDensityOpStack.h`'s banner still said "NOTHING HERE FEEDS THE GAME". It does feed the game
|
||||||
|
now, behind the opt-in. Rewritten to say exactly what is wired (`GetDensityAt`) and what is not
|
||||||
|
(`ClassifyTile` — still hand-written guards, that is Phase 2), plus the §C10 "never compare the
|
||||||
|
two paths" rule at the point of use.
|
||||||
|
- `CODEMAP §3.2d` had the same stale claim; `§3` gained rows for `UsesOperatorStackForChunk` and
|
||||||
|
`bUseOperatorStack`, and the `BuildMazeStack` row now carries the degenerate-strate precondition.
|
||||||
|
|
||||||
|
**UNVERIFIED:** still not compiled — that is Jahni's call and it is the immediate next action.
|
||||||
|
Error spots unchanged, plus one: `FVoxelOpContext` is aggregate-initialised field-by-field, so a
|
||||||
|
field rename would show up here.
|
||||||
|
|
||||||
|
**Next single action unchanged:** build, then the visual A/B on a Maze strate (§2.6's bar —
|
||||||
|
recognisably the same maze, judged on a screenshot). Then Phase 2, starting with the §3.1 question.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -537,9 +537,28 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
if (CP_UseOpStack)
|
if (CP_UseOpStack)
|
||||||
{
|
{
|
||||||
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
||||||
|
FVoxelOpContext OpCtx;
|
||||||
|
OpCtx.ChunkCoord = ChunkCoord;
|
||||||
|
OpCtx.Seed = (uint32)Seed;
|
||||||
|
OpCtx.LayoutVersion = LayoutVersion;
|
||||||
|
|
||||||
switch (CP_GenType)
|
switch (CP_GenType)
|
||||||
{
|
{
|
||||||
case ECaveGeneratorType::Maze:
|
case ECaveGeneratorType::Maze:
|
||||||
|
// GARDE DE STRATE DÉGÉNÉRÉE : GetMazeDensity court-circuite sur `return 1.0f`
|
||||||
|
// (= air) quand la hauteur est nulle ou négative ; cette garde appartient à la
|
||||||
|
// fonction d'archétype et la pile n'en a pas, par conception. Sans ce test, une
|
||||||
|
// strate dégénérée donnerait de l'air sur un chemin et de la géométrie sur
|
||||||
|
// l'autre. On retombe sur le `switch`, qui EST le comportement de référence.
|
||||||
|
// Degenerate-strate guard: the archetype early-outs to air, the stack has no
|
||||||
|
// such early-out by design. Fall back to the switch, which is the reference.
|
||||||
|
if (CP_Maze.StrateTopWorldZ - CP_Maze.StrateBottomWorldZ <= 0.0f)
|
||||||
|
{
|
||||||
|
CP_UseOpStack = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
OpCtx.StrateTopWorldZ = CP_Maze.StrateTopWorldZ;
|
||||||
|
OpCtx.StrateBottomWorldZ = CP_Maze.StrateBottomWorldZ;
|
||||||
VoxelDensityOps::BuildMazeStack(CP_OpStack, CP_Maze, Seed,
|
VoxelDensityOps::BuildMazeStack(CP_OpStack, CP_Maze, Seed,
|
||||||
OriginSpineRadius, StrateManager);
|
OriginSpineRadius, StrateManager);
|
||||||
break;
|
break;
|
||||||
@@ -550,6 +569,18 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
CP_UseOpStack = false;
|
CP_UseOpStack = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Le test appelle PrepareChunk, pas la production : c'est exactement la divergence
|
||||||
|
// qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk`
|
||||||
|
// actuels sont vides, donc ceci ne change RIEN aujourd'hui — c'est le point : le
|
||||||
|
// premier opérateur qui hisse vraiment du travail par chunk doit trouver l'appel
|
||||||
|
// déjà là. `Step` reste 1 : GetDensityAt ne connaît pas le pas d'échantillonnage
|
||||||
|
// du mesher (voir le contrat T2.b dans VoxelDensityOp.h).
|
||||||
|
// The test calls PrepareChunk and production did not — the exact divergence that
|
||||||
|
// makes an op green in test and wrong in game. All seven bodies are empty today,
|
||||||
|
// which is the point: the first op that hoists real per-chunk work must find the
|
||||||
|
// call already here.
|
||||||
|
if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,21 @@
|
|||||||
// La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets.
|
// La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets.
|
||||||
// The STACK: an ordered container of operators, plus the concrete-operator factories.
|
// The STACK: an ordered container of operators, plus the concrete-operator factories.
|
||||||
//
|
//
|
||||||
// ⚠️ RIEN ICI N'ALIMENTE LE JEU. `UVoxelGenerator::GetDensityAt` et `ClassifyTile` ne sont pas
|
// ⚠️ CECI ALIMENTE LE JEU, MAIS SEULEMENT SUR OPT-IN (depuis OPSTACK-PLAN §4, Phase 1, point 3).
|
||||||
// touchés ; le `switch` par archétype reste le seul chemin de production. Cette pile est construite
|
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk et l'évalue à la place du `switch`
|
||||||
// et exercée UNIQUEMENT par le test `VoxelForge.OpStack.MazeEquivalence`, qui la compare point par
|
// UNIQUEMENT quand `UVoxelStrateManager::UsesOperatorStackForChunk` rend true — c.-à-d. quand la
|
||||||
// point à `GetMazeDensity`. Le branchement attend un build vert (OPSTACK-PLAN §4, Phase 1, point 3).
|
// strate a coché `bUseOperatorStack` ET que son archétype figure dans la liste des portés (Maze
|
||||||
|
// seul aujourd'hui). Toute autre strate passe encore par le `switch`, inchangé.
|
||||||
|
// `ClassifyTile` n'est PAS branché : il utilise toujours ses gardes écrites à la main, pas
|
||||||
|
// `ClassifyBox`. C'est la Phase 2.
|
||||||
//
|
//
|
||||||
// NOTHING HERE FEEDS THE GAME. GetDensityAt and ClassifyTile are untouched; the archetype switch is
|
// THIS FEEDS THE GAME, BUT ONLY BEHIND AN OPT-IN. GetDensityAt builds the stack per chunk and
|
||||||
// still the only production path. This stack is built and exercised only by the equivalence test.
|
// evaluates it instead of the switch only when UsesOperatorStackForChunk returns true (strate
|
||||||
|
// ticked bUseOperatorStack AND its archetype is ported — Maze only, today). ClassifyTile is NOT
|
||||||
|
// wired: it still uses its hand-written guards rather than ClassifyBox. That is Phase 2.
|
||||||
|
//
|
||||||
|
// ⛔ NE JAMAIS faire tourner les deux chemins dans le même monde, ni les comparer pour l'égalité :
|
||||||
|
// le résidu de ~1 ULP est INHÉRENT et documenté (AUDIT-2026-07 §C10). La barre est visuelle (§2.6).
|
||||||
//
|
//
|
||||||
// POURQUOI CETTE FORME / WHY THIS SHAPE
|
// POURQUOI CETTE FORME / WHY THIS SHAPE
|
||||||
// La question à laquelle la Phase 1 doit répondre n'est pas « est-ce que ça marche ? » mais
|
// La question à laquelle la Phase 1 doit répondre n'est pas « est-ce que ça marche ? » mais
|
||||||
|
|||||||
Reference in New Issue
Block a user