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:
+56
-6
@@ -279,12 +279,11 @@ not merely *permitted* to diverge — they are compiled under different rules.
|
|||||||
|
|
||||||
**Two consequences, one benign and one not:**
|
**Two consequences, one benign and one not:**
|
||||||
|
|
||||||
**Benign — refactors cannot be bit-identical.** The same expression compiled into two translation
|
**Benign — refactors are not bit-identical in practice.** See `§C10` for the measured detail: the
|
||||||
units may reassociate differently, worth ~1 ULP. So "the port reproduces the original exactly" is
|
Maze port reproduces the original's SDF *bit for bit* but its final density differs by 1-2 ULP on
|
||||||
not an achievable bar for the op-stack work, and `OPSTACK-PLAN §2.6`'s bar (recognisably the same
|
~2% of samples, with zero isosurface crossings. The acceptance criterion is encoded in
|
||||||
*place*, judged on a screenshot) was the right call for reasons beyond the ones it gave. The
|
`VoxelForge.OpStack.MazeEquivalence`: **hard-fail on any isosurface crossing, tolerate ULP-scale
|
||||||
acceptance criterion is now encoded in `VoxelForge.OpStack.MazeEquivalence`: **hard-fail on any
|
deltas, warn on anything larger.**
|
||||||
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
|
**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
|
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
|
### 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:
|
Worth stating plainly, because it's the part that's easy to get wrong and this doesn't:
|
||||||
|
|||||||
+11
-10
@@ -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
|
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.
|
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.**
|
> **✅ CONFIRMED THE HARD WAY, 2026-07-27.** The Maze port reproduces `GetMazeDensity`'s **SDF bit for
|
||||||
> The Maze port reproduced `GetMazeDensity` to within 1 ULP on 2.3% of samples, with **zero
|
> bit**, and its final density to within 1-2 ULP on ~2% of samples, with **zero isosurface
|
||||||
> isosurface crossings**. A four-stage bisect showed the residue surviving into code that is
|
> crossings** — geometrically identical, not one triangle moved. The exact origin of that last
|
||||||
> character-for-character transcribed, which pointed at the toolchain: the plugin compiles with
|
> rounding was chased through five measured-and-refuted hypotheses and then **parked by decision**;
|
||||||
> **`/fp:fast`** (UnrealBuildTool's Windows default), which explicitly licenses the compiler to
|
> the full evidence is in `AUDIT-2026-07.md §C10`. **Read C10 before ever reopening it.**
|
||||||
> 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.
|
|
||||||
>
|
>
|
||||||
> **The operational bar for every remaining archetype port, encoded in
|
> **The operational bar for every remaining archetype port, encoded in
|
||||||
> `VoxelForge.OpStack.MazeEquivalence`:** hard-fail on any isosurface crossing (that moves geometry);
|
> `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
|
**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
|
old one — same scale, same navigability, same feel, recognisably the same kind of place. Judged by Jahni
|
||||||
|
|||||||
@@ -727,3 +727,49 @@ both cases correct `OPSTACK-PLAN §2.6` / `AUDIT §C9` / the test's INFO text, s
|
|||||||
scaffolding, and resume step 3.
|
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, LE TEST QUI COMPTE — la pile d'opérateurs Maze contre GetMazeDensity.
|
||||||
// PHASE 1'S LOAD-BEARING TEST — the Maze operator stack against 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
|
// CE QUE LA PHASE 1 DEVAIT PROUVER / WHAT PHASE 1 HAD TO PROVE
|
||||||
// La question n'est pas « est-ce que le code tourne ». C'est celle du déclencheur d'arrêt de
|
// Le déclencheur d'arrêt de `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe
|
||||||
// `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe naturellement du code
|
// naturellement du code existant ? »** Réponse mesurée : oui. Maze se décompose en sept opérateurs
|
||||||
// existant ? »** Si oui, la décomposition reproduit l'original à l'identique sans contorsion. Si
|
// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de
|
||||||
// non, on s'en aperçoit ici — pas trois archétypes plus tard.
|
// 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
|
// ⚠️ LE PLANCHER ULP — lire ceci avant de « corriger » un écart résiduel
|
||||||
// reproduces the original without contortion. If it doesn't, we find out HERE — not three
|
// ─────────────────────────────────────────────────────────────────────────────────────────
|
||||||
// archetypes later.
|
// 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
|
// L'origine exacte de ce dernier arrondi n'a PAS été identifiée, après six cycles de build et cinq
|
||||||
// `OPSTACK-PLAN §2.6` n'EXIGE PAS l'identité binaire avec l'ancien système — c'est justement la
|
// hypothèses toutes réfutées par la mesure (aller-retour FVector · fenêtre de rugosité · `/fp:fast`
|
||||||
// relaxation qui autorise une vraie décomposition plutôt qu'un emballage. Mais Maze se décompose
|
// entre unités de compilation · contexte d'inlining · constante de compilation vs donnée
|
||||||
// si proprement qu'on peut viser l'identité binaire, et quand on peut l'avoir il faut la prendre :
|
// d'exécution). Ce qui EST établi par la mesure :
|
||||||
// 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ù).
|
|
||||||
//
|
//
|
||||||
// §2.6 does NOT require bit-identity — that relaxation is what permits real decomposition. But Maze
|
// • le SDF est bit-identique sur 126/126 des écarts — le treillis, les hashs, l'ensemble d'arêtes
|
||||||
// decomposes cleanly enough to achieve it, and where it is achievable it should be taken: it turns
|
// et `VoxelSDF::Capsule` sont donc exacts ;
|
||||||
// belief into proof. A FAILURE here is not automatically a bug — it is a signal to read (the test
|
// • l'écart naît entièrement dans la conversion SDF→densité, au dernier arrondi ;
|
||||||
// reports the largest divergence and where it is).
|
// • 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
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
|
||||||
@@ -33,7 +58,6 @@
|
|||||||
|
|
||||||
#include "VoxelForgeTestFixture.h"
|
#include "VoxelForgeTestFixture.h"
|
||||||
#include "VoxelDensityOpStack.h"
|
#include "VoxelDensityOpStack.h"
|
||||||
#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash — for the verbatim copy below
|
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
@@ -46,158 +70,6 @@ namespace
|
|||||||
{
|
{
|
||||||
constexpr int32 NumMazeSamples = 20000;
|
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. */
|
/** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */
|
||||||
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
|
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
|
||||||
int32& OutTopVoxelZ, int32& OutBottomVoxelZ)
|
int32& OutTopVoxelZ, int32& OutBottomVoxelZ)
|
||||||
@@ -231,15 +103,13 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`, air, en convention MC).
|
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde appartient
|
||||||
// Cette garde appartient à la fonction d'archétype, pas à un opérateur ; la pile suppose une
|
// à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate valide.
|
||||||
// strate valide. Vérifier plutôt que supposer.
|
|
||||||
if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f)
|
if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f)
|
||||||
{
|
{
|
||||||
AddError(FString::Printf(
|
AddError(FString::Printf(
|
||||||
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f), which sends ")
|
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("GetMazeDensity down its early-out. The op stack has no such early-out by design."),
|
||||||
TEXT("so the comparison below would be meaningless."),
|
|
||||||
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
|
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -251,10 +121,7 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
Gen->OriginSpineRadius, World.StrateManager.Get());
|
Gen->OriginSpineRadius, World.StrateManager.Get());
|
||||||
|
|
||||||
// La décomposition doit être une DÉCOMPOSITION. Un `FMazeOp` monolithique passerait tous les
|
// 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
|
// tests numériques ci-dessous et aurait pourtant raté l'objet entier du refactor (§2.5).
|
||||||
// (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.
|
|
||||||
TestEqual(TEXT("the Maze stack is decomposed, not wrapped (rock + corridors + roughness + carve + 3 structural)"),
|
TestEqual(TEXT("the Maze stack is decomposed, not wrapped (rock + corridors + roughness + carve + 3 structural)"),
|
||||||
Stack.Num(), 7);
|
Stack.Num(), 7);
|
||||||
|
|
||||||
@@ -265,8 +132,6 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
Ctx.StrateBottomWorldZ = MazeParams.StrateBottomWorldZ;
|
Ctx.StrateBottomWorldZ = MazeParams.StrateBottomWorldZ;
|
||||||
Stack.PrepareChunk(Ctx);
|
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;
|
TArray<FVector> Points;
|
||||||
Points.Reserve(NumMazeSamples);
|
Points.Reserve(NumMazeSamples);
|
||||||
{
|
{
|
||||||
@@ -280,36 +145,11 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── L'ÉQUIVALENCE. ──
|
//=========================================================================
|
||||||
//
|
// ÉQUIVALENCE — géométrie d'abord, bits ensuite.
|
||||||
// ⚠️ 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
|
int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0;
|
||||||
// (`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;
|
|
||||||
float WorstDelta = 0.0f;
|
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)
|
for (int32 i = 0; i < NumMazeSamples; ++i)
|
||||||
{
|
{
|
||||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
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);
|
const float Delta = FMath::Abs(Old - New);
|
||||||
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
|
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
|
||||||
|
|
||||||
// Tolérance : quelques ULP à la magnitude locale. `Blend - Sdf` amplifie fortement un
|
// `Blend - Sdf` annule catastrophiquement au bord de la coquille de blend, donc un
|
||||||
// écart d'ULP sur le SDF quand on est au bord de la zone de blend (annulation
|
// écart d'ULP sur le SDF ressort amplifié sur la densité : marge généreuse, mais bornée.
|
||||||
// catastrophique), d'où une marge généreuse — mais bornée.
|
|
||||||
const float UlpNoise = 16.0f * FMath::Max(FMath::Abs(Old), 1.0f) * FLT_EPSILON;
|
const float UlpNoise = 16.0f * FMath::Max(FMath::Abs(Old), 1.0f) * FLT_EPSILON;
|
||||||
if (Delta > UlpNoise) { ++NumBeyondUlpNoise; }
|
if (Delta > UlpNoise) { ++NumBeyondUlpNoise; }
|
||||||
}
|
}
|
||||||
// Le mesher ne lit que le SIGNE (D >= IsoLevel ⇒ air). Deux valeurs peuvent différer d'un
|
// Le mesher ne lit que le SIGNE (D >= IsoLevel ⇒ air). Un désaccord de CÔTÉ bouge la géométrie.
|
||||||
// ULP sans changer un seul triangle ; un désaccord de CÔTÉ change la géométrie.
|
|
||||||
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; }
|
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)
|
if (NumDiff == 0)
|
||||||
{
|
{
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(TEXT("Bit-identical across %d samples."), NumMazeSamples));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
else if (NumBeyondUlpNoise == 0)
|
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(
|
AddInfo(FString::Printf(
|
||||||
TEXT("%d of %d samples differ, ALL at ULP scale (largest |delta| %.9g at (%.0f, %.0f, ")
|
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("%.0f)), and 0 cross the isosurface -- not one triangle would move. This is the ")
|
||||||
TEXT("expected floor: the plugin builds with /fp:fast (UnrealBuildTool's Windows ")
|
TEXT("accepted floor; see the header comment and AUDIT-2026-07.md C10. The SDF itself is ")
|
||||||
TEXT("default -- VCToolChain.cs, \"Default is imprecise FP semantics\"), which lets the ")
|
TEXT("reproduced BIT FOR BIT, so the lattice, the hashes and VoxelSDF::Capsule are exact; ")
|
||||||
TEXT("compiler reassociate identical source differently per translation unit. The ")
|
TEXT("only the final SDF->density rounding differs. Do not go hunting this again without ")
|
||||||
TEXT("bisect below confirmed it empirically: the residue survives into \"corridors + ")
|
TEXT("reading C10 first -- five hypotheses have already been measured and refuted."),
|
||||||
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."),
|
|
||||||
NumDiff, NumMazeSamples, WorstDelta,
|
NumDiff, NumMazeSamples, WorstDelta,
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
||||||
@@ -567,111 +193,28 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddWarning(FString::Printf(
|
AddWarning(FString::Printf(
|
||||||
TEXT("%d of %d samples differ and %d of them are TOO LARGE to be /fp:fast rounding ")
|
TEXT("%d of %d samples differ and %d are TOO LARGE to be the accepted ULP floor (largest ")
|
||||||
TEXT("noise (largest |delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. ")
|
TEXT("|delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. THIS one is real port ")
|
||||||
TEXT("Unlike the ULP-scale floor, this IS port drift. Check, in order: the roughness ")
|
TEXT("drift, not the known floor. Check, in order: the roughness apply-window ")
|
||||||
TEXT("apply-window (R + SurfaceRoughness + 2), the carve blend (2.0), the noise ")
|
TEXT("(R + SurfaceRoughness + 2), the carve blend (2.0), the noise frequency (0.12) and ")
|
||||||
TEXT("frequency (0.12) and octave count (3), and the order of the structural post ops. ")
|
TEXT("octave count (3), and the order of the structural post ops."),
|
||||||
TEXT("The bisect below narrows it to a stage."),
|
|
||||||
NumDiff, NumMazeSamples, NumBeyondUlpNoise, WorstDelta,
|
NumDiff, NumMazeSamples, NumBeyondUlpNoise, WorstDelta,
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
|
||||||
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
|
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
|
||||||
NumSolidDisagreements));
|
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
|
// Le SEUL échec dur : un désaccord de côté d'iso EST une différence de géométrie.
|
||||||
// mérite un échec dur. / A side-of-iso disagreement IS a geometry difference. The one hard fail.
|
|
||||||
TestEqual(TEXT("no sample lands on the opposite side of the isosurface from the original"),
|
TestEqual(TEXT("no sample lands on the opposite side of the isosurface from the original"),
|
||||||
NumSolidDisagreements, 0);
|
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
|
// INVARIANCE DE FENÊTRE — la pile doit tenir les mêmes règles que le générateur.
|
||||||
// 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).
|
// 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 };
|
std::atomic<int32> Impure{ 0 };
|
||||||
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
|
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
|
||||||
|
|
||||||
@@ -697,13 +240,12 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
Impure.load(), 0);
|
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
|
// LE VERDICT DE BOÎTE — le vrai prix perf : Maze n'a JAMAIS su sauter une tuile.
|
||||||
// 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.
|
// ClassifyTile renvoie Mixed pour tout archétype de grotte, donc TunnelNetwork, Maze,
|
||||||
//
|
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du
|
||||||
// Maze has NEVER skipped a tile: ClassifyTile returns Mixed for every cave archetype. This is
|
// gain T1.d. Tout nombre > 0 ici est du saut de tuile que Maze n'a jamais eu.
|
||||||
// the refactor's real perf prize, and it is checkable right here.
|
|
||||||
{
|
{
|
||||||
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0;
|
||||||
FRandomStream Rng(24680);
|
FRandomStream Rng(24680);
|
||||||
@@ -717,8 +259,7 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
Rng.RandRange(-6, 6) * Extent,
|
Rng.RandRange(-6, 6) * Extent,
|
||||||
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * 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; // le MÊME treillis que le mesher, marge ±1 comprise
|
||||||
const int32 GridDim = Cells + 1;
|
|
||||||
const FBox Box(
|
const FBox Box(
|
||||||
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
||||||
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * 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; }
|
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
|
||||||
++NumProved;
|
++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);
|
const bool bClaimsSolid = (Verdict == EVoxelTileClass::AllSolid);
|
||||||
for (int32 gz = -1; gz <= GridDim; ++gz)
|
for (int32 gz = -1; gz <= GridDim; ++gz)
|
||||||
for (int32 gy = -1; gy <= GridDim; ++gy)
|
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 ")
|
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("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("the port rests on this number."));
|
||||||
TEXT("enough that corridors genuinely reach every sampled tile, or whether the ")
|
|
||||||
TEXT("lattice source's reach is over-conservative."));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user