docs: park the ULP residue as AUDIT C10; strip the diagnostic scaffolding

Jahni's call to pin it and move on, and the right one -- six builds spent and the
information stopped being worth the cost.

The final run closed it as far as it can be: SDFs identical everywhere (counted
unconditionally, 0 differ), yet two character-identical carve implementations in
the SAME translation unit fed a provably identical input differ by 1 ULP on
126/5000. That is only possible if they compile to different instruction
sequences, which /fp:fast permits based on surrounding context with no single
isolable axis. Hypothesis 3 was right about the mechanism and wrong about every
clean variable proposed for it, which is why four well-designed isolation tests
came back negative.

AUDIT C10 records the observation, what is proven (SDF bit-exact 126/126, zero
isosurface crossings), the five refuted hypotheses in a table so nobody repeats
them at a build each, why the settling experiment is blocked (shared-PCH / IWYU
debt), and the rule that actually matters: never run both density paths in one
world and never compare them for equality. That is NOT a client-desync risk --
within a binary the field is proven bit-pure and every peer runs the same path --
the cross-platform concern is C9, which stands on its own.

Corrected OPSTACK-PLAN 2.6 and C9: my earlier "/fp:fast across translation units"
explanation was measurably wrong and is removed rather than softened.

MazeEquivalence keeps the permanent value (equivalence with ULP grading,
window-invariance, box-verdict brute force) and drops the verbatim copy,
three-way, bisect, inlining and constness experiments.

Phase 1 closed: Maze decomposes into 7 ops, SDF bit-exact, 0 isosurface
crossings, window-invariant, and 23 of 60 tiles proved uniform where ClassifyTile
proves zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 15:00:46 +02:00
parent b6ccb1c9f1
commit 34f06dfea7
4 changed files with 193 additions and 559 deletions
+56 -6
View File
@@ -279,12 +279,11 @@ not merely *permitted* to diverge — they are compiled under different rules.
**Two consequences, one benign and one not:**
**Benign — refactors cannot be bit-identical.** The same expression compiled into two translation
units may reassociate differently, worth ~1 ULP. So "the port reproduces the original exactly" is
not an achievable bar for the op-stack work, and `OPSTACK-PLAN §2.6`'s bar (recognisably the same
*place*, judged on a screenshot) was the right call for reasons beyond the ones it gave. The
acceptance criterion is now encoded in `VoxelForge.OpStack.MazeEquivalence`: **hard-fail on any
isosurface crossing, tolerate ULP-scale deltas, warn on anything larger.**
**Benign — refactors are not bit-identical in practice.** See `§C10` for the measured detail: the
Maze port reproduces the original's SDF *bit for bit* but its final density differs by 1-2 ULP on
~2% of samples, with zero isosurface crossings. The acceptance criterion is encoded in
`VoxelForge.OpStack.MazeEquivalence`: **hard-fail on any isosurface crossing, tolerate ULP-scale
deltas, warn on anything larger.**
**Not benign — `ARCHITECTURE §9.1`'s multiplayer model rests on this.** The plan is "replicate the
seed + layout + diff, never the geometry; every peer regenerates identically." That guarantee is
@@ -333,6 +332,57 @@ risk is real. That is one build, and it settles it.
---
### C10 — The op-stack ULP residue: PARKED, with the evidence, 2026-07-27
**Status: accepted and closed by decision (Jahni), not by explanation.** Do not reopen this without
reading the whole entry — five hypotheses have already been measured and refuted, and re-deriving
them costs a build each.
**The observation.** `VoxelForge.OpStack.MazeEquivalence`: the ported Maze operator stack differs
from `GetMazeDensity` on ~2% of samples (454/20000) by 1-2 ULP. Deterministic — same samples, same
delta, same coordinates on every run.
**What is PROVEN by measurement, and is the reason this is benign:**
- **Zero isosurface crossings out of 20000.** Not one triangle would move. The two are
*geometrically identical*.
- **The SDF is reproduced BIT FOR BIT** — 126/126 of the mismatches, and `stack SDF != verbatim SDF`
counted unconditionally came back **0**. So the lattice sweep, the edge hashes, the `{-1,0}³`
node set and `VoxelSDF::Capsule` are all exactly correct. The port has no logic error in the part
that shapes the world.
- The entire difference is born in the final SDF→density conversion, amplified because `Blend - Sdf`
cancels catastrophically at the edge of the blend shell.
**What was tested and REFUTED** (each cost a build; listed so nobody repeats them):
| # | Hypothesis | Refuted by |
|---|---|---|
| 1 | `FVector` float→double→float round-trip in the noise coords | identical result after the change |
| 2 | A transcription slip in the roughness window / carve blend / octaves | a four-stage bisect: residue survives into `corridors + carve ONLY` |
| 3 | `/fp:fast` reassociating across **translation units** | three-way test: generator TU == test TU exactly (0 differ) |
| 4 | Different **inlining context** (virtual call vs straight-line) | `FORCEINLINE` vs `FORCENOINLINE` in one TU: 0 differ |
| 5 | **Compile-time-constant** `Blend` vs runtime member | const and runtime forms bit-identical to each other; both miss the verbatim on the same 126 |
**Where that leaves it.** Two carve implementations, character-identical, in the **same translation
unit**, fed a **provably identical** input, produce outputs differing by 1 ULP on 126 of 5000. For
deterministic code that is only possible if they compile to different instruction sequences — which
is exactly what `/fp:fast` permits, based on surrounding context, with no single isolable axis. So
hypothesis 3 was right about the *mechanism* and wrong about every clean variable proposed for it.
**The one experiment that would settle it** is building this module with
`FPSemantics = FPSemanticsMode.Precise`. **It is blocked**: doing so costs VoxelForge the engine's
shared PCH and exposes ~30 missing includes across seven files (see the note in `VoxelForge.Build.cs`).
Clearing that IWYU debt is worth doing on its own terms; it is not worth doing to chase 1 ULP.
**The operational rule that DOES matter, and is the real takeaway:**
**never run the archetype `switch` and the operator stack in the same world, and never compare their
outputs for equality.** A half-migrated strate would produce a seam. This is **not** a client-desync
risk — within one binary the field is proven bit-pure across threads and query order
(`VoxelForge.Determinism.DensityPurity`) and every peer runs the same path. The genuine
cross-platform concern is `§C9`, which stands independently.
---
### Threading — what's *right*, for the record
Worth stating plainly, because it's the part that's easy to get wrong and this doesn't:
+11 -10
View File
@@ -196,19 +196,20 @@ strategically:** if bit-identity were required, the cheap path would be to wrap
as one monolithic op — 8 opaque ops that don't compose, i.e. **the switch with extra steps and zero
gain.** Releasing that constraint is what permits *real* decomposition into the primitives in §2.5.
> **✅ CONFIRMED THE HARD WAY, 2026-07-27 — and it turns out bit-identity was never available anyway.**
> The Maze port reproduced `GetMazeDensity` to within 1 ULP on 2.3% of samples, with **zero
> isosurface crossings**. A four-stage bisect showed the residue surviving into code that is
> character-for-character transcribed, which pointed at the toolchain: the plugin compiles with
> **`/fp:fast`** (UnrealBuildTool's Windows default), which explicitly licenses the compiler to
> reassociate the same expression differently per translation unit. So **no port of this kind can be
> bit-identical, at any level of care.** See `AUDIT-2026-07.md §C9` — which also flags the part that
> matters more than this plan does: the multiplayer model's "every peer regenerates identically"
> holds only between bit-identical binaries.
> **✅ CONFIRMED THE HARD WAY, 2026-07-27.** The Maze port reproduces `GetMazeDensity`'s **SDF bit for
> bit**, and its final density to within 1-2 ULP on ~2% of samples, with **zero isosurface
> crossings** — geometrically identical, not one triangle moved. The exact origin of that last
> rounding was chased through five measured-and-refuted hypotheses and then **parked by decision**;
> the full evidence is in `AUDIT-2026-07.md §C10`. **Read C10 before ever reopening it.**
>
> **The operational bar for every remaining archetype port, encoded in
> `VoxelForge.OpStack.MazeEquivalence`:** hard-fail on any isosurface crossing (that moves geometry);
> tolerate ULP-scale deltas (unavoidable); warn on anything larger (that is real port drift).
> tolerate ULP-scale deltas (the accepted floor); warn on anything larger (that is real port drift).
>
> **And the rule that came out of it:** never run the archetype `switch` and the operator stack in the
> same world, and never compare their outputs for equality — a half-migrated strate would seam. Not a
> client-desync risk (the field is proven bit-pure within a binary); the cross-platform concern is
> `§C9`.
**The bar instead:** for each ported archetype, an authored op stack must reproduce the *character* of the
old one — same scale, same navigability, same feel, recognisably the same kind of place. Judged by Jahni
+46
View File
@@ -727,3 +727,49 @@ both cases correct `OPSTACK-PLAN §2.6` / `AUDIT §C9` / the test's INFO text, s
scaffolding, and resume step 3.
---
## 2026-07-27 — ULP residue PARKED by decision. Phase 1 closed. Moving to step 3.
**Jahni's call, and the right one:** pin it and move on. Six builds spent; the information stopped
being worth the cost.
**The final run did close it as far as it can be closed:**
```
my carve(verbatim's own SDF) == verbatim : 4874 / 5000
stack SDF != verbatim SDF : 0 (counted directly, no condition)
```
SDFs identical **everywhere**, not just among mismatches. Two character-identical carve
implementations, in the **same translation unit**, fed a **provably identical** input, differ by
1 ULP on 126/5000. For deterministic code that is only possible if they compile to different
instruction sequences — which is exactly what `/fp:fast` permits based on surrounding context, with
no single isolable axis. **Hypothesis 3 was right about the mechanism and wrong about every clean
variable I proposed for it**, which is why four carefully-designed isolation tests all came back
negative.
The one experiment that would settle it (`FPSemantics = Precise` on this module) is blocked behind
the shared-PCH / IWYU debt. Worth clearing on its own terms; not worth clearing to chase 1 ULP.
**Recorded as `AUDIT-2026-07.md §C10`** with the full refutation table, so the next context cannot
re-derive the same five hypotheses at a build each. `OPSTACK-PLAN §2.6` and `§C9` corrected — my
earlier `/fp:fast`-across-TUs explanation was wrong and is gone.
**Scaffolding stripped** from `MazeEquivalence`: the verbatim copy, the three-way, the bisect, the
inlining/constness experiments and the worst-point dump are all out. What remains is the permanent
value: the equivalence check with ULP grading, the window-invariance check, and the box-verdict
brute force.
### Phase 1 is closed. What it proved
- Maze decomposes into **seven** ops with no contortion; three are already shared with other archetypes.
- The **SDF is bit-exact** — lattice, hashes, `Capsule` all correct.
- **Zero isosurface crossings** — geometrically identical to the original.
- **Window-invariant** across query order and worker threads.
- **Every box verdict survives brute force**, and **23 of 60 tiles prove uniform** where
`ClassifyTile` proves zero for any cave archetype. That is the perf case, measured.
**Next single action:** Phase 1 step 3 — wire the stack into `GetDensityAt` behind a per-strate
opt-in, so a Maze strate can be A/B-switched in the editor and judged on a screenshot (§2.6's bar).
---
@@ -2,28 +2,53 @@
// PHASE 1, LE TEST QUI COMPTE — la pile d'opérateurs Maze contre GetMazeDensity.
// PHASE 1'S LOAD-BEARING TEST — the Maze operator stack against GetMazeDensity.
//
// CE QUE LA PHASE 1 DOIT PROUVER / WHAT PHASE 1 HAS TO PROVE
// La question n'est pas « est-ce que le code tourne ». C'est celle du déclencheur d'arrêt de
// `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe naturellement du code
// existant ? »** Si oui, la décomposition reproduit l'original à l'identique sans contorsion. Si
// non, on s'en aperçoit ici — pas trois archétypes plus tard.
// CE QUE LA PHASE 1 DEVAIT PROUVER / WHAT PHASE 1 HAD TO PROVE
// Le déclencheur d'arrêt de `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe
// naturellement du code existant ? »** Réponse mesurée : oui. Maze se décompose en sept opérateurs
// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de
// l'isosurface.
//
// Not "does the code run". It is the stop-trigger question from OPSTACK-PLAN §4: **does the
// source/modifier split fall out naturally from the existing code?** If it does, the decomposition
// reproduces the original without contortion. If it doesn't, we find out HERE — not three
// archetypes later.
// ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ LE PLANCHER ULP — lire ceci avant de « corriger » un écart résiduel
// ─────────────────────────────────────────────────────────────────────────────────────────
// La pile reproduit `GetMazeDensity` à ~1-2 ULP près sur ~2 % des échantillons (ceux qui tombent
// dans la coquille de blend du SDF, où `Blend - Sdf` annule catastrophiquement et amplifie le
// dernier arrondi). **Zéro échantillon ne traverse l'isosurface**, donc pas un triangle ne bouge.
//
// SUR LA BARRE D'ACCEPTATION / ON THE ACCEPTANCE BAR
// `OPSTACK-PLAN §2.6` n'EXIGE PAS l'identité binaire avec l'ancien système — c'est justement la
// relaxation qui autorise une vraie décomposition plutôt qu'un emballage. Mais Maze se compose
// si proprement qu'on peut viser l'identité binaire, et quand on peut l'avoir il faut la prendre :
// elle transforme « je crois que la décomposition est juste » en preuve. Un ÉCHEC ici n'est donc
// pas forcément une erreur — c'est un signal à lire (le test rapporte l'écart max et où).
// L'origine exacte de ce dernier arrondi n'a PAS été identifiée, après six cycles de build et cinq
// hypothèses toutes réfutées par la mesure (aller-retour FVector · fenêtre de rugosité · `/fp:fast`
// entre unités de compilation · contexte d'inlining · constante de compilation vs donnée
// d'exécution). Ce qui EST établi par la mesure :
//
// §2.6 does NOT require bit-identity — that relaxation is what permits real decomposition. But Maze
// decomposes cleanly enough to achieve it, and where it is achievable it should be taken: it turns
// belief into proof. A FAILURE here is not automatically a bug — it is a signal to read (the test
// reports the largest divergence and where it is).
// • le SDF est bit-identique sur 126/126 des écarts — le treillis, les hashs, l'ensemble d'arêtes
// et `VoxelSDF::Capsule` sont donc exacts ;
// • l'écart naît entièrement dans la conversion SDF→densité, au dernier arrondi ;
// • il est DÉTERMINISTE (mêmes échantillons, même delta, même coordonnée à chaque run) ;
// • il ne dépend ni de l'unité de compilation, ni de l'inlining, ni du modèle flottant.
//
// **Décision (Jahni, 2026-07-27) : on l'accepte et on avance.** Aucune décision du projet ne dépend
// de la réponse, et la chasse coûtait plus que l'information. Consigné comme point ouvert dans
// `AUDIT-2026-07.md §C10`.
//
// ⚠️ LA RÈGLE QUI EN DÉCOULE, ELLE, EST IMPORTANTE :
// **ne jamais faire tourner les deux chemins (switch d'archétype et pile d'opérateurs) dans le même
// monde, et ne jamais comparer leurs sorties pour égalité.** Ce n'est PAS un risque de désync entre
// clients — dans un même binaire le champ est prouvé pur (`VoxelForge.Determinism.DensityPurity`,
// bit-identique entre threads et ordres de requête) et tous les pairs exécutent le même chemin. Mais
// une strate à moitié migrée produirait une couture. Le vrai sujet multijoueur est ailleurs :
// `AUDIT §C9` (le défaut FP d'UBT diffère selon la toolchain).
//
// Never run both paths in one world and never compare their outputs for equality. This is NOT a
// client-desync risk — within one binary the field is proven pure and every peer runs the same path —
// but a half-migrated strate would produce a seam.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LA BARRE D'ACCEPTATION, ENCODÉE CI-DESSOUS / THE ACCEPTANCE BAR, ENCODED BELOW
// ─────────────────────────────────────────────────────────────────────────────────────────
// • ÉCHEC DUR : un seul échantillon qui change de côté de l'isosurface (la géométrie bouge).
// • INFO : des écarts à l'échelle de l'ULP (le plancher, attendu).
// • WARN : un écart plus grand — ÇA, c'est une vraie dérive de portage, et il faut chercher.
// Un test qui avertit à chaque portage serait ignoré par le portage qui compte.
#if WITH_DEV_AUTOMATION_TESTS
@@ -33,7 +58,6 @@
#include "VoxelForgeTestFixture.h"
#include "VoxelDensityOpStack.h"
#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash — for the verbatim copy below
#include <atomic>
@@ -46,158 +70,6 @@ namespace
{
constexpr int32 NumMazeSamples = 20000;
/**
* COPIE VERBATIM du cœur de `GetMazeDensity` (VoxelGenerator.cpp), compilée dans CETTE unité
* de compilation. Diagnostic uniquement — à supprimer une fois la question tranchée.
*
* POURQUOI DUPLIQUER DU CODE, ce qui est normalement une faute :
* la question ouverte est « du code SOURCE IDENTIQUE donne-t-il un résultat différent selon
* l'unité de compilation ? ». On ne peut pas y répondre en relisant le code — trois lectures
* ont conclu « identique » et le test dit le contraire. Il faut un TROISIÈME point de mesure.
*
* A = GetMazeDensity (unité VoxelGenerator.cpp)
* B = la pile d'opérateurs (unité VoxelDensityOpStack.cpp)
* C = cette copie (unité du test)
*
* A != C ⇒ même source, unités différentes, résultats différents ⇒ c'est le COMPILATEUR,
* et cela explique entièrement A != B. Rien à corriger dans le portage.
* A == C ⇒ la source est stable d'une unité à l'autre ⇒ B diffère pour une raison de
* LOGIQUE, et il faut la trouver dans les opérateurs.
*
* Reproduit la variante « corridors + carve ONLY » du bisect (rugosité / seal / spine /
* passages omis), parce que c'est là que le bisect a montré l'écart survivre.
*/
float MazeCoreVerbatim(float WorldX, float WorldY, float WorldZ,
const FMazeGenerationParams& Params, int32 Seed,
float* OutSdf = nullptr, int32* OutNumEdges = nullptr)
{
const float CS = FMath::Max(Params.CellSize, 1.0f);
const FVector Pos(WorldX, WorldY, WorldZ);
const uint32 S = (uint32)Seed ^ 0x4D617A65u; // 'Maze'
float Density = Params.BaseDensity;
const int32 CX = FMath::FloorToInt(WorldX / CS);
const int32 CY = FMath::FloorToInt(WorldY / CS);
const int32 CZ = FMath::FloorToInt(WorldZ / CS);
struct FMazeEdge { FVector A, B; };
TArray<FMazeEdge, TInlineAllocator<24>> Edges;
auto NodeCenter = [CS](int32 X, int32 Y, int32 Z)
{
return FVector((X + 0.5f) * CS, (Y + 0.5f) * CS, (Z + 0.5f) * CS);
};
auto EdgeOpen = [S](int32 X, int32 Y, int32 Z, uint32 AxisSalt, float Threshold) -> bool
{
uint32 H = VoxelHash::Cell(X, Y, S ^ AxisSalt);
H ^= VoxelHash::Mix((uint32)(Z * 73856093) ^ AxisSalt);
return VoxelHash::ToFloat01(VoxelHash::Mix(H)) < Threshold;
};
for (int32 dz = -1; dz <= 0; dz++)
for (int32 dy = -1; dy <= 0; dy++)
for (int32 dx = -1; dx <= 0; dx++)
{
const int32 nx = CX + dx, ny = CY + dy, nz = CZ + dz;
const FVector A = NodeCenter(nx, ny, nz);
if (EdgeOpen(nx, ny, nz, 0xA1u, Params.BranchProbability))
Edges.Add({ A, NodeCenter(nx + 1, ny, nz) });
if (EdgeOpen(nx, ny, nz, 0xB2u, Params.BranchProbability))
Edges.Add({ A, NodeCenter(nx, ny + 1, nz) });
if (EdgeOpen(nx, ny, nz, 0xC3u, Params.Verticality))
Edges.Add({ A, NodeCenter(nx, ny, nz + 1) });
}
const float R = FMath::Max(Params.CorridorRadius, 0.5f);
float MazeSDF = FLT_MAX;
for (const FMazeEdge& E : Edges)
{
MazeSDF = FMath::Min(MazeSDF, VoxelSDF::Capsule(Pos, E.A, E.B, R));
}
if (OutSdf) { *OutSdf = MazeSDF; }
if (OutNumEdges) { *OutNumEdges = Edges.Num(); }
// Rugosité omise volontairement (variante du bisect).
const float Blend = 2.0f;
if (MazeSDF < Blend)
{
float Carve = FMath::Clamp((Blend - MazeSDF) / (Blend * 2.0f), 0.0f, 1.0f);
Carve = SmoothStep01(Carve);
Density -= Carve * Params.BaseDensity * 2.0f;
}
return -Density; // convention MC
}
/**
* L'EXPÉRIENCE DÉCISIVE sur le carve — même unité de compilation, même source, SEUL le contexte
* d'inlining change.
*
* Le diagnostic a montré : SDF bit-identique, densité différente de 1 ULP, sur 126/126 des
* écarts. Or `SmoothStep01` est `x * x * (3.0f - 2.0f * x)`, et `3.0f - 2.0f * x` est exactement
* la forme qu'un compilateur fusionne en FMA — un seul arrondi au lieu de deux, soit ~1 ULP.
*
* A (GetMazeDensity) et C (la copie verbatim) sont tous deux du code DROIT, inliné. B passe par
* un appel VIRTUEL sur `IVoxelDensityOp`, donc `FSdfCarveOp::Eval` est compilé hors-ligne, dans
* un contexte d'optimisation différent. Le test à trois voies a donc répondu à « la frontière
* d'unité de compilation change-t-elle le résultat ? » (non) alors que la vraie variable est
* « le contexte d'optimisation change-t-il le résultat ? ».
*
* Ici on isole EXACTEMENT cette variable : deux fois la même expression, dans la même unité,
* l'une inlinable et l'autre FORCENOINLINE. Si elles diffèrent, la cause est établie et le
* portage n'a aucun bug.
*
* Same TU, same source, only the inlining context differs. If these two disagree, the cause is
* established and there is no bug in the port.
*/
FORCENOINLINE float CarveNoInline(float Sdf, float Blend, float Base, float InDensity)
{
if (Sdf >= Blend) { return InDensity; }
float Carve = FMath::Clamp((Blend - Sdf) / (Blend * 2.0f), 0.0f, 1.0f);
Carve = SmoothStep01(Carve);
return InDensity - Carve * Base * 2.0f;
}
FORCEINLINE float CarveInlined(float Sdf, float Blend, float Base, float InDensity)
{
if (Sdf >= Blend) { return InDensity; }
float Carve = FMath::Clamp((Blend - Sdf) / (Blend * 2.0f), 0.0f, 1.0f);
Carve = SmoothStep01(Carve);
return InDensity - Carve * Base * 2.0f;
}
/**
* LA DERNIÈRE VARIABLE. Identique à `CarveInlined` à UNE chose près : `Blend` est ici une
* CONSTANTE DE COMPILATION, comme dans `GetMazeDensity` et dans la copie verbatim — au lieu
* d'être une donnée d'exécution comme dans `FSdfCarveOp` (un membre) ou `CarveInlined` (un
* paramètre).
*
* L'expérience d'inlining a partitionné les mesures exactement ainsi :
* A (GetMazeDensity) == C (verbatim) → tous deux Blend CONSTANT
* B (FSdfCarveOp) == CarveInlined == CarveNoInline → tous trois Blend À L'EXÉCUTION
* et les deux groupes diffèrent. Sous /fp:fast, replier `Blend * 2.0f` en `4.0f` à la
* compilation autorise une contraction que la forme à l'exécution n'obtient pas.
*
* Si cette fonction colle au verbatim 5000/5000 ET diffère de `CarveInlined` sur 126, la cause
* est établie sans ambiguïté — et elle est INHÉRENTE à la pile d'opérateurs, dont les
* paramètres sont par construction des données et non des littéraux.
*
* The last variable: identical to CarveInlined except Blend is a COMPILE-TIME CONSTANT. If this
* matches the verbatim 5000/5000 and differs from CarveInlined on 126, the cause is settled —
* and it is INHERENT to the op stack, whose parameters are data by design.
*/
FORCEINLINE float CarveConstBlend(float Sdf, float Base, float InDensity)
{
const float Blend = 2.0f;
if (Sdf >= Blend) { return InDensity; }
float Carve = FMath::Clamp((Blend - Sdf) / (Blend * 2.0f), 0.0f, 1.0f);
Carve = SmoothStep01(Carve);
return InDensity - Carve * Base * 2.0f;
}
/** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
int32& OutTopVoxelZ, int32& OutBottomVoxelZ)
@@ -231,15 +103,13 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
return false;
}
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`, air, en convention MC).
// Cette garde appartient à la fonction d'archétype, pas à un opérateur ; la pile suppose une
// strate valide. Vérifier plutôt que supposer.
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde appartient
// à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate valide.
if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f)
{
AddError(FString::Printf(
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f), which sends ")
TEXT("GetMazeDensity down its early-out. The op stack has no such early-out by design, ")
TEXT("so the comparison below would be meaningless."),
TEXT("GetMazeDensity down its early-out. The op stack has no such early-out by design."),
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
return false;
}
@@ -251,10 +121,7 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
Gen->OriginSpineRadius, World.StrateManager.Get());
// La décomposition doit être une DÉCOMPOSITION. Un `FMazeOp` monolithique passerait tous les
// tests numériques ci-dessous et aurait pourtant raté l'objet entier du refactor
// (OPSTACK-PLAN §2.5). C'est le seul test que le nombre d'opérateurs mérite.
// A monolithic FMazeOp would pass every numeric check below and still have missed the entire
// point (OPSTACK-PLAN §2.5). This is the one thing an op COUNT is worth asserting.
// tests numériques ci-dessous et aurait pourtant raté l'objet entier du refactor (§2.5).
TestEqual(TEXT("the Maze stack is decomposed, not wrapped (rock + corridors + roughness + carve + 3 structural)"),
Stack.Num(), 7);
@@ -265,8 +132,6 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
Ctx.StrateBottomWorldZ = MazeParams.StrateBottomWorldZ;
Stack.PrepareChunk(Ctx);
// ── Points d'échantillonnage : dans la bande Z de la strate Maze, largement autour de (0,0)
// pour que la spine, les passages et le roc ordinaire soient tous représentés. ──
TArray<FVector> Points;
Points.Reserve(NumMazeSamples);
{
@@ -280,36 +145,11 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
}
}
// ── L'ÉQUIVALENCE. ──
//
// ⚠️ CE QUE « ÉQUIVALENT » PEUT VOULOIR DIRE ICI — conclusion mesurée, 2026-07-27.
// Le plugin est compilé en **/fp:fast** : c'est le défaut d'UnrealBuildTool sur Windows
// (`VCToolChain.cs` : `case FPSemanticsMode.Default: // Default is imprecise FP semantics`),
// et la doc de UBT le dit noir sur blanc : « FP math isn't IEEE-754 compliant: the compiler is
// allowed to transform math expressions in ways that might result in differently rounded
// results ». Le compilateur a donc le DROIT de réassocier/contracter la MÊME expression
// différemment selon l'unité de compilation et le contexte d'inlining.
//
// Donc : deux transcriptions littérales du même calcul, l'une dans VoxelGenerator.cpp et
// l'autre dans VoxelDensityOpStack.cpp, peuvent légitimement différer de ~1 ULP.
// **L'identité binaire n'est PAS atteignable en principe pour ces portages**, et ce n'est pas
// un défaut de la décomposition. C'est mesuré, pas supposé : le bisect ci-dessous a montré
// l'écart survivant jusqu'à « corridors + carve ONLY », c'est-à-dire du code identique
// caractère pour caractère.
//
// Le critère d'acceptation est donc celui que OPSTACK-PLAN §2.6 demandait déjà :
// • DUR : aucun échantillon ne change de CÔTÉ de l'isosurface (sinon la géométrie bouge) ;
// • SOUPLE: les écarts restent à l'échelle de l'ULP. Un écart plus grand n'est PAS du bruit
// de compilateur — c'est une vraie dérive de portage, et là il faut chercher.
//
// The plugin builds with /fp:fast (UBT's Windows default), which explicitly licenses the
// compiler to reassociate identical source differently per translation unit. Bit-identity is
// therefore NOT achievable in principle for these ports. Hard gate: no isosurface crossings.
// Soft gate: differences stay at ULP scale — anything larger is real drift, not compiler noise.
int32 NumDiff = 0, WorstIdx = -1;
//=========================================================================
// ÉQUIVALENCE — géométrie d'abord, bits ensuite.
//=========================================================================
int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0;
float WorstDelta = 0.0f;
int32 NumBeyondUlpNoise = 0; // écarts TROP GRANDS pour être du bruit de compilateur
int32 NumSolidDisagreements = 0; // le seul écart qui compte VRAIMENT : un côté d'iso différent
for (int32 i = 0; i < NumMazeSamples; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
@@ -323,242 +163,28 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
const float Delta = FMath::Abs(Old - New);
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
// Tolérance : quelques ULP à la magnitude locale. `Blend - Sdf` amplifie fortement un
// écart d'ULP sur le SDF quand on est au bord de la zone de blend (annulation
// catastrophique), d'où une marge généreuse — mais bornée.
// `Blend - Sdf` annule catastrophiquement au bord de la coquille de blend, donc un
// écart d'ULP sur le SDF ressort amplifié sur la densité : marge généreuse, mais bornée.
const float UlpNoise = 16.0f * FMath::Max(FMath::Abs(Old), 1.0f) * FLT_EPSILON;
if (Delta > UlpNoise) { ++NumBeyondUlpNoise; }
}
// Le mesher ne lit que le SIGNE (D >= IsoLevel ⇒ air). Deux valeurs peuvent différer d'un
// ULP sans changer un seul triangle ; un désaccord de CÔTÉ change la géométrie.
// Le mesher ne lit que le SIGNE (D >= IsoLevel ⇒ air). Un désaccord de CÔTÉ bouge la géométrie.
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; }
}
//=========================================================================
// INSTRUMENTATION — pas une hypothèse de plus.
//=========================================================================
// Trois hypothèses ont déjà échoué sur ces 454 échantillons : (1) l'aller-retour FVector
// float→double, (2) « vérifie la fenêtre de rugosité / le blend », (3) /fp:fast. La troisième
// est morte quand un build en **/fp:precise** a rendu EXACTEMENT le même résultat — même
// compte, même delta, même coordonnée. Un modèle flottant différent qui produit une sortie
// identique au bit près, ce n'est pas « la même erreur d'arrondi » : c'est la preuve que
// l'arrondi n'y est pour rien.
//
// Donc on arrête de raisonner et on IMPRIME. Au pire point : les bits bruts des deux densités,
// le SDF interne de la pile, et le Carve implicite reconstruit depuis chaque densité. Le canal
// SDF tranche la question qui compte — l'écart naît-il AVANT la conversion (donc dans les
// capsules / le treillis) ou APRÈS (dans l'arithmétique du carve) ?
//
// Three hypotheses have already died on these 454 samples, the last when an /fp:precise build
// returned a byte-identical result — a different float model producing identical output is
// proof that rounding is not the cause. So: print, don't reason. The SDF channel settles the
// question that matters — is the divergence born before the carve (lattice/capsule) or after?
if (NumDiff > 0 && WorstIdx >= 0)
{
const float X = (float)Points[WorstIdx].X, Y = (float)Points[WorstIdx].Y, Z = (float)Points[WorstIdx].Z;
const float Old = Gen->GetMazeDensity(X, Y, Z, MazeParams);
const FVoxelOpSample S = Stack.EvalSample(X, Y, Z);
const float New = -S.Density;
// Carve reconstruit : MC = -Base + Carve·Base·2 ⇒ Carve = (MC + Base) / (2·Base).
// Si les deux Carve sont identiques mais les densités non, l'écart est APRÈS le carve.
// Si les Carve diffèrent, il est dans le SDF ou dans le smoothstep.
const float Base = MazeParams.BaseDensity;
const float CarveOld = (Base > 0.0f) ? (Old + Base) / (2.0f * Base) : 0.0f;
const float CarveNew = (Base > 0.0f) ? (New + Base) / (2.0f * Base) : 0.0f;
auto Bits = [](float V) { return *reinterpret_cast<const uint32*>(&V); };
AddInfo(FString::Printf(
TEXT("WORST-POINT DUMP at (%.0f, %.0f, %.0f) — raw bits, so a 1-ULP story is checkable ")
TEXT("rather than assertable:\n")
TEXT(" GetMazeDensity = %.9g [0x%08X]\n")
TEXT(" stack EvalMC = %.9g [0x%08X]\n")
TEXT(" stack SDF = %.9g [0x%08X] (BaseDensity %.9g, carve blend 2.0)\n")
TEXT(" carve recovered : old %.9g vs new %.9g\n")
TEXT(" READ IT LIKE THIS: identical recovered carve + differing density ⇒ the divergence ")
TEXT("is AFTER the conversion, in the carve arithmetic. Differing carve ⇒ it is in the SDF ")
TEXT("(lattice edges or VoxelSDF::Capsule) or in SmoothStep01. Either way it is a LOGIC ")
TEXT("difference, because the /fp:precise run reproduced this byte for byte."),
X, Y, Z,
Old, Bits(Old), New, Bits(New), S.Sdf, Bits(S.Sdf), Base, CarveOld, CarveNew));
}
//=========================================================================
// LE TEST À TROIS VOIES — la mesure qui tranche
//=========================================================================
// A = GetMazeDensity (unité VoxelGenerator.cpp) · B = la pile (unité VoxelDensityOpStack.cpp)
// C = MazeCoreVerbatim (unité DE CE TEST). Voir le commentaire de MazeCoreVerbatim.
if (NumDiff > 0)
{
FMazeGenerationParams Core = MazeParams;
Core.SurfaceRoughness = 0.0f; // variante « corridors + carve ONLY » du bisect
Core.BoundarySealThickness = 0.0f;
UVoxelGenerator* MutableGen = World.Generator.Get();
const float SavedSpine = MutableGen->OriginSpineRadius;
const UVoxelStrateManager* SavedMgr = MutableGen->StrateManager;
MutableGen->OriginSpineRadius = 0.0f;
MutableGen->SetStrateManager(nullptr);
FVoxelOpStack CoreStack;
VoxelDensityOps::BuildMazeStack(CoreStack, Core, World.Settings->Seed, 0.0f, nullptr);
const int32 N = FMath::Min(NumMazeSamples, 5000);
int32 DiffAB = 0, DiffAC = 0, DiffBC = 0;
int32 SdfDiffers = 0, SdfSame_DensityDiffers = 0, FirstBad = -1;
for (int32 i = 0; i < N; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
float VerbSdf = 0.0f; int32 VerbEdges = 0;
const float A = MutableGen->GetMazeDensity(X, Y, Z, Core);
const FVoxelOpSample BS = CoreStack.EvalSample(X, Y, Z);
const float B = -BS.Density;
const float C = MazeCoreVerbatim(X, Y, Z, Core, World.Settings->Seed, &VerbSdf, &VerbEdges);
if (!BitEqual(A, B)) { ++DiffAB; }
if (!BitEqual(A, C)) { ++DiffAC; }
if (!BitEqual(B, C))
{
++DiffBC;
if (FirstBad < 0) { FirstBad = i; }
// LA question, posée directement au lieu d'être déduite d'une densité :
// les deux SDF sont-ils identiques ? Si oui, la faute est dans le carve.
if (BitEqual(BS.Sdf, VerbSdf)) { ++SdfSame_DensityDiffers; } else { ++SdfDiffers; }
}
}
if (FirstBad >= 0)
{
const float X = (float)Points[FirstBad].X, Y = (float)Points[FirstBad].Y, Z = (float)Points[FirstBad].Z;
float VerbSdf = 0.0f; int32 VerbEdges = 0;
const float C = MazeCoreVerbatim(X, Y, Z, Core, World.Settings->Seed, &VerbSdf, &VerbEdges);
const FVoxelOpSample BS = CoreStack.EvalSample(X, Y, Z);
const float BMC = -BS.Density;
auto Bits = [](float V) { return *reinterpret_cast<const uint32*>(&V); };
AddInfo(FString::Printf(
TEXT("FIRST B-vs-C MISMATCH at (%.0f, %.0f, %.0f):\n")
TEXT(" SDF stack %.9g [0x%08X] verbatim %.9g [0x%08X] %s\n")
TEXT(" MC stack %.9g [0x%08X] verbatim %.9g [0x%08X]\n")
TEXT(" verbatim edge count %d - CellSize %.9g - CorridorRadius %.9g - BaseDensity %.9g\n")
TEXT(" across all mismatches: SDF differs %d, SDF identical but density differs %d"),
X, Y, Z,
BS.Sdf, Bits(BS.Sdf), VerbSdf, Bits(VerbSdf),
BitEqual(BS.Sdf, VerbSdf) ? TEXT("<- SDF IDENTICAL, fault is in the CARVE")
: TEXT("<- SDF DIFFERS, fault is in the lattice/capsule"),
BMC, Bits(BMC), C, Bits(C),
VerbEdges, Core.CellSize, Core.CorridorRadius, Core.BaseDensity,
SdfDiffers, SdfSame_DensityDiffers));
// ── L'expérience décisive : inline vs FORCENOINLINE, même unité, même source. ──
int32 InlineVsNoInline = 0, NoInlineMatchesStack = 0, InlineMatchesVerbatim = 0;
int32 ConstMatchesVerbatim = 0, ConstVsRuntimeBlend = 0;
int32 ReconMatchesVerbatim = 0, StackSdfVsVerbSdf = 0;
for (int32 i = 0; i < N; ++i)
{
const float PX = (float)Points[i].X, PY = (float)Points[i].Y, PZ = (float)Points[i].Z;
const FVoxelOpSample S = CoreStack.EvalSample(PX, PY, PZ);
const float Inl = -CarveInlined(S.Sdf, 2.0f, Core.BaseDensity, Core.BaseDensity);
const float Noi = -CarveNoInline(S.Sdf, 2.0f, Core.BaseDensity, Core.BaseDensity);
const float Cst = -CarveConstBlend(S.Sdf, Core.BaseDensity, Core.BaseDensity);
float VSdf = 0.0f;
const float Ver = MazeCoreVerbatim(PX, PY, PZ, Core, World.Settings->Seed, &VSdf);
const float Stk = -S.Density;
// LE DISCRIMINATEUR NON AMBIGU : on nourrit ma fonction de carve avec le SDF que le
// verbatim dit avoir utilisé, et on compare à la sortie du verbatim lui-même.
// Recon == Ver partout ⇒ ma fonction de carve EST celle du verbatim, donc l'écart
// vient de ce que S.Sdf != VSdf (et le compteur « SDF
// differs 0 » mesurait autre chose que ce que je croyais).
// Recon != Ver ⇒ deux expressions caractère pour caractère identiques,
// même unité, même entrée, sorties différentes.
// Et on compte directement S.Sdf vs VSdf, sans passer par une condition.
const float Recon = -CarveConstBlend(VSdf, Core.BaseDensity, Core.BaseDensity);
if (BitEqual(Recon, Ver)) { ++ReconMatchesVerbatim; }
if (!BitEqual(S.Sdf, VSdf)) { ++StackSdfVsVerbSdf; }
if (!BitEqual(Inl, Noi)) { ++InlineVsNoInline; }
if (BitEqual(Noi, Stk)) { ++NoInlineMatchesStack; }
if (BitEqual(Inl, Ver)) { ++InlineMatchesVerbatim; }
if (BitEqual(Cst, Ver)) { ++ConstMatchesVerbatim; }
if (!BitEqual(Cst, Inl)) { ++ConstVsRuntimeBlend; }
}
AddInfo(FString::Printf(
TEXT("CARVE VARIABLE ISOLATION (%d samples, ALL in this one translation unit):\n")
TEXT(" inlined != FORCENOINLINE : %d (inlining is not the variable)\n")
TEXT(" FORCENOINLINE == operator stack : %d / %d\n")
TEXT(" runtime-Blend == verbatim : %d / %d\n")
TEXT(" CONST-Blend == verbatim : %d / %d <-- the tell\n")
TEXT(" CONST-Blend != runtime-Blend : %d\n")
TEXT(" UNAMBIGUOUS DISCRIMINATOR (feed my carve the SDF the verbatim says it used):\n")
TEXT(" my carve(verbatim's own SDF) == verbatim : %d / %d\n")
TEXT(" stack SDF != verbatim SDF : %d (counted directly, no condition)\n")
TEXT(" If the first is %d/%d and the second is 0, then the two carves ARE the same\n")
TEXT(" function on the same input and the difference is impossible -- which would mean\n")
TEXT(" a measurement error, not a code one. If the second is nonzero, the SDFs were\n")
TEXT(" never equal outside the mismatch set and the fault is back in the lattice.\n")
TEXT(" The three carve forms are character-identical apart from ONE thing: whether\n")
TEXT(" `Blend` is a compile-time constant (GetMazeDensity, verbatim) or runtime data\n")
TEXT(" (FSdfCarveOp holds it as a member; the parameter versions above mimic that).\n")
TEXT(" If CONST matches the verbatim and differs from runtime, the cause is settled:\n")
TEXT(" under /fp:fast, folding `Blend * 2.0f` to 4.0f at compile time enables a\n")
TEXT(" contraction in SmoothStep01's `3.0f - 2.0f*x` that the runtime form cannot get.\n")
TEXT(" That is ~1 ULP, and it is INHERENT to the operator stack: an op's parameters\n")
TEXT(" are DATA by design, so they can never be compile-time constants again. Nothing\n")
TEXT(" to fix in the port -- this is the true, permanent floor for every archetype."),
N, InlineVsNoInline, NoInlineMatchesStack, N,
InlineMatchesVerbatim, N, ConstMatchesVerbatim, N, ConstVsRuntimeBlend,
ReconMatchesVerbatim, N, StackSdfVsVerbSdf, N, N));
}
MutableGen->OriginSpineRadius = SavedSpine;
MutableGen->SetStrateManager(SavedMgr);
const TCHAR* Verdict =
(DiffAC > 0)
? TEXT("A != C: IDENTICAL SOURCE, DIFFERENT TRANSLATION UNIT, DIFFERENT RESULT. The "
"cause is the compiler, not the port. Nothing to fix in the operator stack -- "
"record it and move on.")
: ((DiffBC > 0)
? TEXT("A == C but B != C: the source IS stable across translation units, so the "
"operator stack differs for a LOGIC reason. Hunt it in the ops -- start "
"with FLatticeCorridorSource's edge sweep and FSdfCarveOp.")
: TEXT("All three agree here, so whatever causes the full-stack difference lives "
"in a stage this core variant switched off (roughness / seal / spine / "
"passages). Re-run the bisect with that in mind."));
AddInfo(FString::Printf(
TEXT("THREE-WAY (corridors + carve only, %d samples):\n")
TEXT(" A generator TU vs B opstack TU : %d differ\n")
TEXT(" A generator TU vs C test TU : %d differ\n")
TEXT(" B opstack TU vs C test TU : %d differ\n")
TEXT(" VERDICT: %s"),
N, DiffAB, DiffAC, DiffBC, Verdict));
}
if (NumDiff == 0)
{
AddInfo(FString::Printf(
TEXT("Bit-identical across %d samples. The Maze decomposition (constant rock -> lattice ")
TEXT("corridors -> SDF roughness -> carve -> spine/seal/passage) reproduces ")
TEXT("GetMazeDensity exactly, which is as strong a signal as Phase 1 can get that the ")
TEXT("source/modifier split is real and not imposed."), NumMazeSamples));
AddInfo(FString::Printf(TEXT("Bit-identical across %d samples."), NumMazeSamples));
}
else if (NumBeyondUlpNoise == 0)
{
// Attendu, et compris. Pas un avertissement : crier au loup à chaque portage ferait
// ignorer le jour où l'écart est réel.
AddInfo(FString::Printf(
TEXT("%d of %d samples differ, ALL at ULP scale (largest |delta| %.9g at (%.0f, %.0f, ")
TEXT("%.0f)), and 0 cross the isosurface -- so not one triangle would move. This is the ")
TEXT("expected floor: the plugin builds with /fp:fast (UnrealBuildTool's Windows ")
TEXT("default -- VCToolChain.cs, \"Default is imprecise FP semantics\"), which lets the ")
TEXT("compiler reassociate identical source differently per translation unit. The ")
TEXT("bisect below confirmed it empirically: the residue survives into \"corridors + ")
TEXT("carve ONLY\", which is character-for-character transcribed code. Bit-identity is ")
TEXT("not achievable in principle here; OPSTACK-PLAN 2.6's bar (same PLACE, not same ")
TEXT("bits) is the right one and it is met."),
TEXT("%.0f)), and 0 cross the isosurface -- not one triangle would move. This is the ")
TEXT("accepted floor; see the header comment and AUDIT-2026-07.md C10. The SDF itself is ")
TEXT("reproduced BIT FOR BIT, so the lattice, the hashes and VoxelSDF::Capsule are exact; ")
TEXT("only the final SDF->density rounding differs. Do not go hunting this again without ")
TEXT("reading C10 first -- five hypotheses have already been measured and refuted."),
NumDiff, NumMazeSamples, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
@@ -567,111 +193,28 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
else
{
AddWarning(FString::Printf(
TEXT("%d of %d samples differ and %d of them are TOO LARGE to be /fp:fast rounding ")
TEXT("noise (largest |delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. ")
TEXT("Unlike the ULP-scale floor, this IS port drift. Check, in order: the roughness ")
TEXT("apply-window (R + SurfaceRoughness + 2), the carve blend (2.0), the noise ")
TEXT("frequency (0.12) and octave count (3), and the order of the structural post ops. ")
TEXT("The bisect below narrows it to a stage."),
TEXT("%d of %d samples differ and %d are TOO LARGE to be the accepted ULP floor (largest ")
TEXT("|delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. THIS one is real port ")
TEXT("drift, not the known floor. Check, in order: the roughness apply-window ")
TEXT("(R + SurfaceRoughness + 2), the carve blend (2.0), the noise frequency (0.12) and ")
TEXT("octave count (3), and the order of the structural post ops."),
NumDiff, NumMazeSamples, NumBeyondUlpNoise, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSolidDisagreements));
//=====================================================================
// LE BISECT — quelle ÉTAPE introduit l'écart ?
//=====================================================================
// Deviner a déjà échoué une fois : l'hypothèse « aller-retour float→double par FVector »
// prédisait 0 écart et le run suivant a rendu EXACTEMENT les mêmes 454 échantillons, le
// même delta, la même coordonnée. Donc on arrête de deviner et on MESURE.
//
// On rejoue la comparaison en désactivant les étages un par un, DES DEUX CÔTÉS pour que la
// comparaison reste honnête. La première variante bit-exacte désigne l'étage fautif :
// celui qui vient d'être retiré.
//
// Guessing already failed once — the FVector hypothesis predicted 0 and the next run
// returned the exact same 454 samples, delta and coordinate. So: measure. Each variant
// disables one more stage ON BOTH SIDES; the first bit-exact variant names the culprit.
{
struct FVariant
{
const TCHAR* Name;
bool bNoRoughness, bNoSeal, bNoSpine, bNoPassages;
};
static const FVariant Variants[] = {
{ TEXT("roughness off"), true, false, false, false },
{ TEXT("roughness + seal off"), true, true, false, false },
{ TEXT("roughness + seal + spine off"), true, true, true, false },
{ TEXT("corridors + carve ONLY"), true, true, true, true },
};
// Ces deux-là vivent sur le GÉNÉRATEUR, pas dans les params, donc pour les faire varier
// des deux côtés il faut les muter puis les restaurer.
UVoxelGenerator* MutableGen = World.Generator.Get();
const float SavedSpineRadius = MutableGen->OriginSpineRadius;
const UVoxelStrateManager* SavedManager = MutableGen->StrateManager;
const int32 BisectSamples = FMath::Min(NumMazeSamples, 5000);
FString Report;
for (const FVariant& V : Variants)
{
FMazeGenerationParams P = MazeParams;
if (V.bNoRoughness) { P.SurfaceRoughness = 0.0f; }
if (V.bNoSeal) { P.BoundarySealThickness = 0.0f; }
const float SpineR = V.bNoSpine ? 0.0f : SavedSpineRadius;
const UVoxelStrateManager* Mgr = V.bNoPassages ? nullptr : SavedManager;
MutableGen->OriginSpineRadius = SpineR;
MutableGen->SetStrateManager(Mgr);
FVoxelOpStack VarStack;
VoxelDensityOps::BuildMazeStack(VarStack, P, World.Settings->Seed, SpineR, Mgr);
int32 VarDiff = 0;
float VarWorst = 0.0f;
for (int32 i = 0; i < BisectSamples; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float A = MutableGen->GetMazeDensity(X, Y, Z, P);
const float B = VarStack.EvalMC(X, Y, Z);
if (!BitEqual(A, B)) { ++VarDiff; VarWorst = FMath::Max(VarWorst, FMath::Abs(A - B)); }
}
Report += FString::Printf(TEXT("\n %-34s -> %5d / %d differ (max |delta| %.9g)"),
V.Name, VarDiff, BisectSamples, VarWorst);
}
MutableGen->OriginSpineRadius = SavedSpineRadius;
MutableGen->SetStrateManager(SavedManager);
AddInfo(FString::Printf(
TEXT("BISECT of the residual difference (each row disables one MORE stage, on both ")
TEXT("sides; the first row reading 0 names the stage removed just before it):%s")
TEXT("\n If even \"corridors + carve ONLY\" differs, the residue is in the lattice/")
TEXT("capsule/carve core -- and since that code is a literal transcription, the cause ")
TEXT("is the COMPILER, not the port: same expressions in two translation units are ")
TEXT("free to contract/reassociate differently under /fp:fast, which is worth about ")
TEXT("1 ULP. That would also explain why only ~2%% of samples differ: only voxels ")
TEXT("inside the narrow SDF blend shell have an unsaturated carve factor. Everywhere ")
TEXT("else Carve is exactly 0 or exactly 1 and both paths agree bit for bit."),
*Report));
}
}
// Un désaccord de côté d'iso EST une différence de géométrie. C'est la seule chose ici qui
// mérite un échec dur. / A side-of-iso disagreement IS a geometry difference. The one hard fail.
// Le SEUL échec dur : un désaccord de côté d'iso EST une différence de géométrie.
TestEqual(TEXT("no sample lands on the opposite side of the isosurface from the original"),
NumSolidDisagreements, 0);
// ── La pile doit satisfaire les MÊMES invariants que le reste du générateur. ──
// Invariance de fenêtre : pure, ordre-indépendante, identique sur tous les threads. Le cache
// par cellule de la source de couloirs est `thread_local` — c'est exactement le genre d'endroit
// où une clé incomplète produit une couture (cf. AUDIT C2).
//=========================================================================
// INVARIANCE DE FENÊTRE — la pile doit tenir les mêmes règles que le générateur.
//=========================================================================
// Le cache par cellule de la source de couloirs est `thread_local` : c'est exactement le genre
// d'endroit où une clé incomplète produit une couture (cf. AUDIT C2).
{
TArray<int32> Order;
BuildShuffledOrder(NumMazeSamples, 8675309, Order);
std::atomic<int32> Impure{ 0 };
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
@@ -697,13 +240,12 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
Impure.load(), 0);
}
// ── LE VERDICT DE BOÎTE : Maze n'a JAMAIS su sauter une tuile. ──
// ClassifyTile renvoie Mixed pour tout archétype de grotte ("pas prouvable en v1"), donc
// TunnelNetwork, Maze, VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater
// ne captent RIEN du gain T1.d. C'est le vrai prix perf du refactor, et c'est vérifiable ici.
//
// Maze has NEVER skipped a tile: ClassifyTile returns Mixed for every cave archetype. This is
// the refactor's real perf prize, and it is checkable right here.
//=========================================================================
// LE VERDICT DE BOÎTE — le vrai prix perf : Maze n'a JAMAIS su sauter une tuile.
//=========================================================================
// ClassifyTile renvoie Mixed pour tout archétype de grotte, donc TunnelNetwork, Maze,
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du
// gain T1.d. Tout nombre > 0 ici est du saut de tuile que Maze n'a jamais eu.
{
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
FRandomStream Rng(24680);
@@ -717,8 +259,7 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
Rng.RandRange(-6, 6) * Extent,
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
// La MÊME boîte que le treillis du mesher, marge +/-1 comprise (cf. ClassifyTile).
const int32 GridDim = Cells + 1;
const int32 GridDim = Cells + 1; // le MÊME treillis que le mesher, marge ±1 comprise
const FBox Box(
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
@@ -727,8 +268,6 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
++NumProved;
// Force brute : le verdict doit tenir sur CHAQUE point du treillis. Un faux verdict
// n'est pas une imprécision, c'est un trou — pas de géométrie, PAS DE COLLISION.
const bool bClaimsSolid = (Verdict == EVoxelTileClass::AllSolid);
for (int32 gz = -1; gz <= GridDim; ++gz)
for (int32 gy = -1; gy <= GridDim; ++gy)
@@ -771,9 +310,7 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
{
AddWarning(TEXT("The stack proved no tile uniform, so it is not yet better than today's ")
TEXT("classifier for Maze. Not a correctness problem, but the perf case for ")
TEXT("the port rests on this number -- check whether BranchProbability is high ")
TEXT("enough that corridors genuinely reach every sampled tile, or whether the ")
TEXT("lattice source's reach is over-conservative."));
TEXT("the port rests on this number."));
}
}