diff --git a/OPSTACK-DECOMPOSITION.md b/OPSTACK-DECOMPOSITION.md index 8ab06ca..9e00454 100644 --- a/OPSTACK-DECOMPOSITION.md +++ b/OPSTACK-DECOMPOSITION.md @@ -263,6 +263,15 @@ FGridColumnMod Add world-grid jittered cylinders, infinit That is the entire archetype. Two of the eight collapse into one, and the ceiling's `abs(noise)` (formations hang down only, never punch up) is a two-line flag on the source. +> **Observed in-editor 2026-07-27, and it settles the question:** Jahni reports FlatPlain and +> CrystalChamber render **identical** in the live world. They should — they share +> `FSlabGenerationParams`, and nothing in the content sets them apart. **The enum promised a +> difference the data never delivered**, in the shipped world as well as in the test fixture. +> So the merge does not lose a distinction; it *reveals* that there was none. Making CrystalChamber +> look like a crystal chamber is a **params** job — raise `CeilingRoughness` (6 → ~20, what +> `SlabEquivalence`'s tuned pass uses) and drop `CeilingRelativeHeight` a little. That is authoring, +> which is exactly the outcome the whole refactor is aiming at. + ### 3.1 ✅ RESOLVED 2026-07-27 — Jahni: the Z term can go. Removed. **Decision:** the Z term was not intentional character. It is gone from `GetSlabDensity` (both diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 01b8c27..6279b4e 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -990,3 +990,72 @@ question is whether it still reads as the same kind of place. Then `SurfaceWorld (OPSTACK-DECOMPOSITION §5) — biggest payoff, most care. --- + +## 2026-07-27 — the tuned pass cried wolf. The TEST was wrong, not the port. + +``` +CrystalChamber(tuned): 121/20000 differ, 6 exceed the bound, worst |delta| 1.71661377e-05 + 0 cross the isosurface; box verdicts 32/60 proved, 0 unsound +``` + +**The port is fine. The yardstick was wrong, and it was wrong in a way that only shows up under +large amplitudes — which is precisely why the tuned pass was worth adding.** + +The old bound was `16 · max(|Old|, 1) · FLT_EPSILON` — ULPs measured on the **output density**. +But the density is `min(Z - Floor, Ceil - Z)`, so **near the isosurface the output tends to 0 while +the intermediates (surfaces, world Z, noise amplitudes) are in the HUNDREDS.** A rounding born at +scale ~400 was being judged against a yardstick of scale 1: 400× too tight, and tightest exactly +where the test looks hardest. + +**Measured, not assumed:** + +| | roughness | worst \|delta\| | in ULP of \|Z\| | +|---|---|---|---| +| gentle passes | ceil 6, floor 4 | 3.81e-06 | ~0.08 | +| tuned pass | ceil 20, floor 9 | 1.72e-05 | **0.345** | + +Amplitudes went up ×2.25–3.33, the deltas went up **×4.5**, and the worst one is **sub-ULP at the +scale it is born in**. Error proportional to amplitude is the signature of ordinary rounding, not of +a wrong transcription — a wrong offset or a missing `abs()` would move the surface by **voxels**, +four orders of magnitude above this, not by a factor of four. + +**Fixed:** the bound now scales with the magnitude the error is born in (`max(|Old|, |Z|, strate Z +bounds)`), and — more importantly — **the warning now prints the discriminator instead of just the +alarm**: the density at the offending sample and the delta expressed in ULPs of the working scale. +A few ULP with a near-zero density is cancellation; thousands of ULP is drift. The next context +reads that off the message instead of re-deriving it at a build apiece. + +The test stays discriminating: real drift is 4 orders of magnitude above the new bound. + +### The box verdicts held under the worst case — that is the result that mattered + +32 of 60 proved uniform with **0 unsound**, under tripled ceiling roughness and 3× the columns. +That was the specific thing the tuned pass existed to attack (a wide ceiling band makes the +`Max(CeilZ - noise, FloorSurface + 2)` clamp bind, which is where a false verdict would be a HOLE), +and the bound survived it. Fewer tiles proved than the gentle passes (32 vs 36/40), which is correct +— wider bands mean more genuinely Mixed tiles. + +### Visual A/B — and a content finding worth more than the A/B + +Jahni: *"visually, crystal and plain are identical, same for when opting on or off from the +opstack."* + +**Opt-in on/off identical = the port is confirmed a pure refactor.** Note this is the expected +result and my earlier framing was sloppy: **both** paths compute the post-§3.1 function, so the +Z-term change is invisible in the A/B by construction. It is only visible against the world as it +looked *before* this build. + +**FlatPlain and CrystalChamber rendering identical is the real finding.** They share +`FSlabGenerationParams` and nothing in the content distinguishes them — **the enum promised a +difference the data never delivered**, in the shipped world exactly as in the test fixture. So the +merge loses no distinction; it reveals there was none. Recorded in `OPSTACK-DECOMPOSITION §3`. +Making a crystal chamber look like one is now a **params** job (`CeilingRoughness` 6 → ~20), which +is precisely the outcome the refactor is for. + +**UNVERIFIED:** the corrected bound and the new warning text. + +**Next single action:** rebuild; the tuned pass should drop to INFO with all three passes reporting +sub-ULP-of-scale deltas. Then `SurfaceWorld` (§5) — biggest payoff, most care: the T1.a column cache +and the exact-lattice `ClassifyTile` bound must both survive the port. + +--- diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp index 4c0598b..20ce719 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp @@ -128,8 +128,38 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters) //===================================================================== // 1. ÉQUIVALENCE — géométrie d'abord, bits ensuite. //===================================================================== + // ───────────────────────────────────────────────────────────────────── + // LE BON MÈTRE — corrigé 2026-07-27 après que la passe `tuned` a crié au loup + // ───────────────────────────────────────────────────────────────────── + // Première version : `16 · max(|Old|, 1) · FLT_EPSILON`, c.-à-d. l'ULP mesuré sur la + // DENSITÉ DE SORTIE. C'est le mauvais mètre, et il se trompe exactement là où le test + // regarde le plus : la densité vaut `min(Z - Sol, Plafond - Z)`, donc PRÈS DE L'ISOSURFACE + // la sortie tend vers 0 pendant que les intermédiaires (surfaces, Z monde, amplitudes de + // bruit) valent des CENTAINES. Un arrondi né à l'échelle 400 était jugé contre un mètre + // à l'échelle 1 — 400× trop serré. + // + // Mesuré : la passe `tuned` (rugosités ×2.25 et ×3.33) a vu ses écarts croître ×4.5, et + // son pire écart valait **0.345 ULP de |Z|**. Sous-ULP à l'échelle où l'erreur naît. + // L'erreur est donc proportionnelle à l'AMPLITUDE, ce qui est la signature d'un arrondi + // ordinaire, pas d'une transcription fausse. + // + // Le mètre correct est la magnitude des quantités D'OÙ VIENT l'erreur. Le test reste + // discriminant : une vraie dérive de portage (offset de bruit faux, `abs()` manquant, + // clamp oublié) déplace la surface de plusieurs VOXELS — 4 ordres de grandeur au-dessus + // de ce seuil, pas 4 fois. + // + // The first yardstick measured ULPs on the OUTPUT density, which tends to 0 near the + // isosurface while the intermediates are in the hundreds. Rounding born at scale ~400 was + // judged against a yardstick of scale 1. Real port drift moves the surface by voxels — + // four orders of magnitude above this bound, so the test stays discriminating. + const float SurfaceScale = FMath::Max(FMath::Abs(SlabParams.StrateTopWorldZ), + FMath::Abs(SlabParams.StrateBottomWorldZ)); + int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0; - float WorstDelta = 0.0f; + float WorstDelta = 0.0f, WorstOld = 0.0f, WorstUlpsOfScale = 0.0f; + // Le pire cas PARMI LES DÉPASSEMENTS — c'est lui qui dit si un WARN est du bruit ou une dérive. + float WorstOutlierDelta = 0.0f, WorstOutlierOld = 0.0f, WorstOutlierUlps = 0.0f; + for (int32 i = 0; i < NumSlabSamples; ++i) { const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z; @@ -141,13 +171,26 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters) { ++NumDiff; const float Delta = FMath::Abs(Old - New); - if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; } - // Même forme que le carve de Maze : `(ColBlend - ColumnSDF)` annule au bord de la - // coquille de blend des colonnes, donc un ULP amont ressort amplifié. Marge - // généreuse mais BORNÉE — au-delà, c'est une vraie dérive de portage. - const float UlpNoise = 16.0f * FMath::Max(FMath::Abs(Old), 1.0f) * FLT_EPSILON; - if (Delta > UlpNoise) { ++NumBeyondUlpNoise; } + // L'échelle à laquelle CET échantillon calcule : la sortie, sa propre altitude, et + // les bornes de la strate. C'est le plus grand des trois qui porte l'arrondi. + const float Scale = FMath::Max3(FMath::Abs(Old), FMath::Abs(Z), + FMath::Max(SurfaceScale, 1.0f)); + const float Ulps = Delta / (Scale * FLT_EPSILON); + + if (Delta > WorstDelta) + { + WorstDelta = Delta; WorstIdx = i; WorstOld = Old; WorstUlpsOfScale = Ulps; + } + + if (Delta > 16.0f * Scale * FLT_EPSILON) + { + ++NumBeyondUlpNoise; + if (Delta > WorstOutlierDelta) + { + WorstOutlierDelta = Delta; WorstOutlierOld = Old; WorstOutlierUlps = Ulps; + } + } } // Le mesher ne lit que le SIGNE. Un désaccord de CÔTÉ bouge la géométrie. if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; } @@ -161,23 +204,32 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters) else if (NumBeyondUlpNoise == 0) { AddInfo(FString::Printf( - TEXT("%s: %d of %d samples differ, ALL at ULP scale (largest |delta| %.9g at ") - TEXT("(%.0f, %.0f, %.0f)), and 0 cross the isosurface. Same accepted floor as Maze ") - TEXT("-- see AUDIT-2026-07.md C10 before hunting it."), - SlotName, NumDiff, NumSlabSamples, WorstDelta, + TEXT("%s: %d of %d samples differ, ALL at ULP scale (largest |delta| %.9g = %.3f ULP ") + TEXT("of the working scale, where density = %.6g, at (%.0f, %.0f, %.0f)), and 0 cross ") + TEXT("the isosurface. Same accepted floor as Maze -- see AUDIT-2026-07.md C10."), + SlotName, NumDiff, NumSlabSamples, WorstDelta, WorstUlpsOfScale, WorstOld, WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f, WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f, WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f)); } else { + // Le message porte maintenant LE DISCRIMINANT, pas seulement l'alarme : la densité au + // point fautif et l'écart exprimé en ULP de l'échelle de travail. Un dépassement à + // quelques ULP avec une densité proche de 0 est un artefact de mètre ; un dépassement + // à des milliers d'ULP est une vraie dérive. La différence se lit, elle ne se devine pas. AddWarning(FString::Printf( - TEXT("%s: %d of %d samples differ and %d are TOO LARGE to be the accepted ULP floor ") - TEXT("(largest |delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. THIS is ") - TEXT("real port drift. Check, in order: the floor/ceiling noise offsets (7.3/11.1 and ") - TEXT("17.3+1000/19.7+2000/3000), the abs() on the ceiling noise, the ceiling clamp ") - TEXT("(FloorSurface + 2), the column blend (2.0) and the 0.15/0.7 jitter."), - SlotName, NumDiff, NumSlabSamples, NumBeyondUlpNoise, WorstDelta, + TEXT("%s: %d of %d samples differ and %d exceed the ULP bound. Worst OUTLIER: ") + TEXT("|delta| %.9g = %.1f ULP of the working scale, where density = %.6g. ") + TEXT("(Worst overall: |delta| %.9g at (%.0f, %.0f, %.0f).) %d cross the isosurface. ") + TEXT("READ THE ULP FIGURE BEFORE INVESTIGATING: a few ULP with a near-zero density is ") + TEXT("cancellation near the isosurface, not drift. Thousands of ULP IS drift -- check, ") + TEXT("in order: the floor/ceiling noise offsets (7.3/11.1 and 17.3+1000/19.7+2000/3000), ") + TEXT("the abs() on the ceiling noise, the ceiling clamp (FloorSurface + 2), the column ") + TEXT("blend (2.0) and the 0.15/0.7 jitter."), + SlotName, NumDiff, NumSlabSamples, NumBeyondUlpNoise, + WorstOutlierDelta, WorstOutlierUlps, WorstOutlierOld, + WorstDelta, WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f, WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f, WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,