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:
2026-07-27 15:15:59 +02:00
parent f7cccb044b
commit 974e795b66
4 changed files with 97 additions and 10 deletions
@@ -537,9 +537,28 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
if (CP_UseOpStack)
{
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)
{
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,
OriginSpineRadius, StrateManager);
break;
@@ -550,6 +569,18 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
CP_UseOpStack = false;
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); }
}
}
+14 -6
View File
@@ -2,13 +2,21 @@
// 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.
//
// ⚠️ RIEN ICI N'ALIMENTE LE JEU. `UVoxelGenerator::GetDensityAt` et `ClassifyTile` ne sont pas
// touchés ; le `switch` par archétype reste le seul chemin de production. Cette pile est construite
// et exercée UNIQUEMENT par le test `VoxelForge.OpStack.MazeEquivalence`, qui la compare point par
// point à `GetMazeDensity`. Le branchement attend un build vert (OPSTACK-PLAN §4, Phase 1, point 3).
// ⚠️ CECI ALIMENTE LE JEU, MAIS SEULEMENT SUR OPT-IN (depuis OPSTACK-PLAN §4, Phase 1, point 3).
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk et l'évalue à la place du `switch`
// UNIQUEMENT quand `UVoxelStrateManager::UsesOperatorStackForChunk` rend true — c.-à-d. quand la
// 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
// still the only production path. This stack is built and exercised only by the equivalence test.
// THIS FEEDS THE GAME, BUT ONLY BEHIND AN OPT-IN. GetDensityAt builds the stack per chunk and
// 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
// La question à laquelle la Phase 1 doit répondre n'est pas « est-ce que ça marche ? » mais