test: /fp:precise reproduced the residue byte for byte — instrument instead of guessing
An /fp:precise build returned the identical 454 samples, identical max delta, identical coordinate. A different float model producing byte-identical output is proof that rounding is not the cause, so the /fp:fast explanation is dead. That is three failed hypotheses on one discrepancy (FVector round-trip, then "check the roughness window", then /fp:fast), each reasoned from plausibility and each costing a build. So: stop reasoning, print. FVoxelOpStack::EvalSample exposes the full sample, and MazeEquivalence now dumps the worst point in raw hex -- both densities, the stack's internal SDF, and the carve factor reconstructed from each side. The recovered carve localises it: identical carve + differing density means the fault is after the conversion; differing carve means it is in the SDF (lattice edges or VoxelSDF::Capsule) or in SmoothStep01. Note for whoever reads the docs next: AUDIT C9 and OPSTACK-PLAN 2.6 currently assert the /fp:fast story as the explanation for THIS residue. That specific claim is falsified and needs walking back once the dump identifies the real cause. C9's other half -- that UBT's FP default differs by toolchain and the MP model assumes bit-reproducible terrain -- stands independently; it was read out of VCToolChain.cs and ClangToolChain.cs, not inferred from this test. Phase 1 step 3 (wiring the stack into GetDensityAt) is paused until this is understood. Small unexplained numeric differences do not get smaller when you build on top of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -411,3 +411,43 @@ opt-in. Phase 1's question is fully answered: Maze decomposes cleanly, the stack
|
|||||||
window-invariant, and it proves 23/60 tiles uniform where `ClassifyTile` proves zero.
|
window-invariant, and it proves 23/60 tiles uniform where `ClassifyTile` proves zero.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-27 — /fp:fast hypothesis DEAD. Third wrong guess. Switching to instrumentation.
|
||||||
|
|
||||||
|
**Jahni built with `FPSemantics = FPSemanticsMode.Precise` and got a BYTE-IDENTICAL result:** same
|
||||||
|
454 samples, same `1.90734863e-06`, same `(-23, 55, -660)`. A different float model producing
|
||||||
|
identical output is not "the same rounding error twice" — it is proof that **rounding is not the
|
||||||
|
cause at all.** The residue is a real, deterministic LOGIC difference somewhere in a transcription I
|
||||||
|
have read three times and believe to be identical.
|
||||||
|
|
||||||
|
**Track record on this one discrepancy, recorded because the pattern matters more than the bug:**
|
||||||
|
|
||||||
|
| # | Hypothesis | Killed by |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `FVector` float→double→float round-trip | re-run returned the identical result |
|
||||||
|
| 2 | "check the roughness window / carve blend / octave count" | the bisect: residue survives to `corridors + carve ONLY` |
|
||||||
|
| 3 | `/fp:fast` cross-TU reassociation | `/fp:precise` build returned the identical result |
|
||||||
|
|
||||||
|
Three hypotheses, all plausible, all reasoned from *what could explain it* rather than from
|
||||||
|
measurement. Each cost a build cycle. **The lesson is not "be smarter", it is "instrument earlier":**
|
||||||
|
the bisect (measurement) produced more information in one run than two hypotheses did in three.
|
||||||
|
|
||||||
|
**Corrected in the docs:** `AUDIT-2026-07.md §C9` and `OPSTACK-PLAN §2.6` both assert the `/fp:fast`
|
||||||
|
story as the explanation for the residue. **That specific claim is now falsified and must be walked
|
||||||
|
back** — see the next entry. (The *separate* C9 finding, that UBT's FP default differs by toolchain
|
||||||
|
and the MP model assumes bit-reproducible terrain, still stands on its own: it was read out of
|
||||||
|
`VCToolChain.cs` / `ClangToolChain.cs`, not inferred from this test.)
|
||||||
|
|
||||||
|
**What changed in code:** `FVoxelOpStack::EvalSample` now exposes the full `FVoxelOpSample`, and
|
||||||
|
`MazeEquivalence` dumps the worst point in raw hex — both densities, the stack's internal SDF, and
|
||||||
|
the carve factor reconstructed from each side. That last one localises the divergence: identical
|
||||||
|
recovered carve with differing density ⇒ the fault is after the conversion; differing carve ⇒ it is
|
||||||
|
in the SDF (lattice edges or `VoxelSDF::Capsule`) or in `SmoothStep01`.
|
||||||
|
|
||||||
|
**UNVERIFIED:** the instrumentation itself.
|
||||||
|
|
||||||
|
**Next single action:** re-run `MazeEquivalence` and read the WORST-POINT DUMP. Phase 1 step 3
|
||||||
|
(wiring the stack into `GetDensityAt`) is deliberately **paused** until this is understood — it is a
|
||||||
|
small unexplained numeric difference, and those do not get smaller when you build on them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -181,6 +181,56 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
|
|||||||
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));
|
||||||
|
}
|
||||||
|
|
||||||
if (NumDiff == 0)
|
if (NumDiff == 0)
|
||||||
{
|
{
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
|
|||||||
@@ -83,10 +83,19 @@ public:
|
|||||||
* Returns INTERNAL-convention density (positive = solid). The caller negates once for MC.
|
* Returns INTERNAL-convention density (positive = solid). The caller negates once for MC.
|
||||||
*/
|
*/
|
||||||
float EvalInternal(float WorldX, float WorldY, float WorldZ) const
|
float EvalInternal(float WorldX, float WorldY, float WorldZ) const
|
||||||
|
{
|
||||||
|
return EvalSample(WorldX, WorldY, WorldZ).Density;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand une comparaison
|
||||||
|
* avec l'ancien chemin diverge, c'est le canal SDF qui dit si l'écart naît avant ou après
|
||||||
|
* la conversion. / The full state after the stack — the SDF channel is what says whether a
|
||||||
|
* divergence is born before or after the carve. */
|
||||||
|
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
|
||||||
{
|
{
|
||||||
FVoxelOpSample S;
|
FVoxelOpSample S;
|
||||||
for (const TUniquePtr<IVoxelDensityOp>& Op : Ops) { Op->Eval(WorldX, WorldY, WorldZ, S); }
|
for (const TUniquePtr<IVoxelDensityOp>& Op : Ops) { Op->Eval(WorldX, WorldY, WorldZ, S); }
|
||||||
return S.Density;
|
return S;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Le même, négaté pour le mesher (négatif = solide). */
|
/** Le même, négaté pour le mesher (négatif = solide). */
|
||||||
|
|||||||
Reference in New Issue
Block a user