test: stage A is bit-identical — strengthen the test its own counters indicted

The port is correct across 6000 samples. The only failure was arithmetic in
the assertion: 4 ops + 3 structural = 7, and I wrote 6.

But two numbers that PASSED are the real finding. 65 of 6000 samples landed
in open cave (1.1%), so the equivalence mostly compared solid rock to solid
rock — the carve, pits, chimneys and worm carve only run near the network.
And 3 of 400 probe points genuinely differed between the two param sets, so
the stale-cache check asked its question three times and said "fine" 397
times about points that could never have answered it.

Both counters were written to say exactly this, and did. Both guards only
fired at ZERO, so the run went green with the coverage of a much smaller
test. A coverage guard that only trips at zero does not measure coverage,
it notices absence. Both are now fraction thresholds that print a percent.

RoomSpacing 80 -> 42, RoomDensity 0.35 -> 0.85.

New check 3b: rebuild the stack with PitDensity = ChimneyDensity = 0 and
count the points that move. Enabling a feature in the params is not evidence
it fired — SDFCache.Pits can come back empty and the test stays green — and
these are the two loops DECOMPOSITION §2 calls the fiddliest in the plugin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 18:41:56 +02:00
parent ef5bda3d8a
commit 5bf61c1815
2 changed files with 172 additions and 20 deletions
+54
View File
@@ -1992,3 +1992,57 @@ and the test's `GetGenerationParams` signature.
`VoxelForge.OpStack.TunnelNetworkSpineEquivalence`. Then stage B (the 13 modifiers). `VoxelForge.OpStack.TunnelNetworkSpineEquivalence`. Then stage B (the 13 modifiers).
--- ---
## 2026-07-28 — Stage A is bit-identical. And its own counters say the test was thin.
```
TunnelNetwork STAGE A: bit-identical across 6000 samples in 24 chunks
(65 in open cave, 5636 in rock)
Params-fingerprint : 3 of 400 probe points genuinely differ, 0 served wrong
Box verdicts : 0 proved, 40 Mixed ← asserted, expected
Op count : expected 6, got 7 ← FAIL
```
**The port is correct.** Vertical scale, cave warp, the shared `BuildChunkCache`, pits and chimneys
at unwarped coords, the floored carve divisor and the worm network mask all reproduce the original
bit for bit. The only failure was **my arithmetic in the assertion**: 4 ops + 3 structural = 7, and I
wrote 6. Fixed.
### The two numbers that matter are the ones that passed
**65 of 6000 samples in open cave — 1.1 %.** Green, and mostly comparing solid rock to solid rock:
the carve, the pits, the chimneys and the worm carve only run near the network. At `RoomSpacing = 80`
/ `RoomDensity = 0.35` a 32-voxel chunk usually contains no cave at all.
**3 of 400 probe points differ between the two param sets — 0.75 %.** So the stale-cache check asked
its question three times and answered "not stale" 397 times about points that could never have
revealed staleness.
Both counters exist precisely to say this, and both said it. **Both guards, however, only fired at
ZERO** — so the run was green with the coverage of a much smaller test. That is the same lesson as
"a test that prints nothing on success is indistinguishable from one that never ran", one notch
finer: *a coverage guard that only trips at zero does not measure coverage, it only notices absence.*
Both are now **fraction thresholds** (≥ 10 % cave, ≥ 5 % differing probes) and both print a
percentage.
Fixes applied: `RoomSpacing 80 → 42`, `RoomDensity 0.35 → 0.85`.
### New check 3b — did the pits and chimneys actually fire?
`§2` calls those two loops the fiddliest thing in the whole decomposition (unwarped coords
`SmoothMin`'d into the warped room SDF). Setting `PitDensity = 0.55` proves nothing: `SDFCache.Pits`
can still come back empty and the test stays green. So the new check builds the same stack with
`PitDensity = ChimneyDensity = 0` and counts the points that MOVE. Zero ⇒ those loops contributed
nothing and the equivalence says nothing about them, whatever colour the test is.
Same move as the FloatingIslands "samples inside island rock" counter, applied to a sub-feature
instead of a whole archetype: **enabling a feature is not evidence it fired.**
**UNVERIFIED:** the strengthened test is not compiled. Only the test file changed; the operators are
untouched and already proven bit-identical.
**Next single action:** re-run the filter (13 tests). Expect stage A green with a cave fraction in
the tens of percent, a fingerprint check in the double digits, and a non-zero pit/chimney count.
Then stage B — the 13 detail modifiers.
---
@@ -70,15 +70,34 @@ namespace
P.FloorBias = 0.0f; P.FloorBias = 0.0f;
} }
/** Pits et cheminées sont à 0 par défaut — or ce sont précisément les deux boucles que `§2` /**
* annonçait comme « le plus retors de toute la décomposition » (coordonnées NON warpées * Pits et cheminées sont à 0 par défaut — or ce sont précisément les deux boucles que `§2`
* mélangées au SDF warpé). Les laisser au repos testerait tout sauf le morceau difficile. */ * annonçait comme « le plus retors de toute la décomposition » (coordonnées NON warpées
* mélangées au SDF warpé). Les laisser au repos testerait tout sauf le morceau difficile.
*
* ⚠️ ET ON DENSIFIE LE RÉSEAU, corrigé après le premier run vert. Aux défauts
* (`RoomSpacing = 80`, `RoomDensity = 0.35`) le premier passage a rendu **65 échantillons en
* grotte sur 6000, soit 1,1 %** : bit-identique, oui, mais en comparant surtout du roc plein à
* du roc plein, là où le carve, les pits, les cheminées et les vers ne s'exécutent même pas.
* Le compteur avait été écrit exactement pour dire ça, et il l'a dit ; l'avertissement, lui, ne
* se déclenchait qu'à ZÉRO — trop tard pour être utile. Les deux sont corrigés ici.
*
* Densified after the first green run: 65 of 6000 samples in open cave (1.1%) means the port was
* mostly compared solid-rock-to-solid-rock. The counter said so; the warning threshold (only at
* zero) did not. Both fixed.
*/
void EnableTunnelFeatures(FStrateGenerationParams& P) void EnableTunnelFeatures(FStrateGenerationParams& P)
{ {
P.RoomSpacing = 42.0f; // 80 → 42 : des salles à portée de chaque chunk échantillonné
P.RoomDensity = 0.85f; // 0.35 → 0.85
P.PitDensity = 0.55f; P.PitDensity = 0.55f;
P.ChimneyDensity = 0.55f; P.ChimneyDensity = 0.55f;
P.VerticalScale = 1.35f; // ≠ 1 ⇒ le Z « effectif » diverge du Z monde partout P.VerticalScale = 1.35f; // ≠ 1 ⇒ le Z « effectif » diverge du Z monde partout
} }
/** Fraction minimale d'échantillons devant tomber en grotte ouverte pour que l'équivalence
* signifie quelque chose. 10 % est modeste et très au-dessus du 1,1 % observé. */
constexpr float MinCaveFraction = 0.10f;
} }
bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters) bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
@@ -119,9 +138,11 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
VoxelDensityOps::BuildTunnelNetworkStack(Stack, P, World.Settings->Seed, VoxelDensityOps::BuildTunnelNetworkStack(Stack, P, World.Settings->Seed,
Gen->OriginSpineRadius, World.StrateManager.Get()); Gen->OriginSpineRadius, World.StrateManager.Get());
// rock + roomgraph + carve + worms + 3 structurels. Les 13 modificateurs de détail viendront // rock + roomgraph + carve + worms + 3 structurels = 7. (Le premier run a dit 7 contre un 6
// s'insérer entre le carve et les vers — ce nombre DOIT bouger à l'étape B. // attendu : faute d'arithmétique dans l'attente, pas dans la pile — 4 + 3, comme les îles.)
TestEqual(TEXT("the stage-A tunnel stack is decomposed into 6 ops"), Stack.Num(), 6); // Les 13 modificateurs de détail viendront s'insérer entre le carve et les vers, donc ce nombre
// DOIT bouger à l'étape B.
TestEqual(TEXT("the stage-A tunnel stack is decomposed into 7 ops"), Stack.Num(), 7);
FVoxelOpContext Ctx; FVoxelOpContext Ctx;
Ctx.Seed = (uint32)World.Settings->Seed; Ctx.Seed = (uint32)World.Settings->Seed;
@@ -194,6 +215,13 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
TEXT("its floored divisor, and the worm carve with its network mask. NOT covered: the ") TEXT("its floored divisor, and the worm carve with its network mask. NOT covered: the ")
TEXT("13 detail modifiers, the per-room op override, and any tile verdict."), TEXT("13 detail modifiers, the per-room op override, and any tile verdict."),
NumTunnelSamples, NumTunnelChunks, NumInCave, NumInRock)); NumTunnelSamples, NumTunnelChunks, NumInCave, NumInRock));
AddInfo(FString::Printf(
TEXT("Cave coverage: %.1f%% of samples are in open cave (floor %.0f%%). This is the ")
TEXT("number that says whether the equivalence MEANS anything -- the carve, the pits, ")
TEXT("the chimneys and the worm carve only execute near the network, so a run dominated ")
TEXT("by deep rock would be green while proving almost nothing."),
100.0f * (float)NumInCave / (float)NumTunnelSamples, 100.0f * MinCaveFraction));
} }
else else
{ {
@@ -216,12 +244,17 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
TestEqual(TEXT("no sample lands on the opposite side of the isosurface"), NumSideDisagree, 0); TestEqual(TEXT("no sample lands on the opposite side of the isosurface"), NumSideDisagree, 0);
if (NumInCave == 0) // ⚠️ SEUIL EN FRACTION, PAS « > 0 ». La version « == 0 » de ce garde-fou a laissé passer un run
// à 1,1 % en silence. Un test qui ne se plaint qu'au zéro absolu ne mesure pas la couverture,
// il constate seulement qu'elle n'est pas vide.
if ((float)NumInCave < MinCaveFraction * (float)NumTunnelSamples)
{ {
AddWarning(TEXT("No sample landed in open cave away from the seal bands: the room graph, ") AddWarning(FString::Printf(
TEXT("the carve, the pits and the worms were never meaningfully exercised, so ") TEXT("Only %.1f%% of samples landed in open cave (want >= %.0f%%): the carve, the pits, ")
TEXT("the equivalence above mostly compares solid rock to solid rock. Raise ") TEXT("the chimneys and the worm carve were barely exercised, so the equivalence above ")
TEXT("RoomDensity or lower RoomSpacing.")); TEXT("mostly compares solid rock to solid rock. Lower RoomSpacing or raise RoomDensity ")
TEXT("in EnableTunnelFeatures."),
100.0f * (float)NumInCave / (float)NumTunnelSamples, 100.0f * MinCaveFraction));
} }
//========================================================================= //=========================================================================
@@ -324,17 +357,82 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
NumWrong, 0); NumWrong, 0);
AddInfo(FString::Printf( AddInfo(FString::Printf(
TEXT("Params-fingerprint check: %d of %d probe points genuinely differ between the two ") TEXT("Params-fingerprint check: %d of %d probe points (%.1f%%) genuinely differ between ")
TEXT("param sets, and %d were served wrong under A/B interleaving. A zero in the FIRST ") TEXT("the two param sets, and %d were served wrong under A/B interleaving. The FIRST ")
TEXT("number would mean the check proved nothing -- the two param sets must actually ") TEXT("number is the check's real strength: only those points could ever reveal a stale ")
TEXT("produce different rock for a stale cache to be detectable."), TEXT("cache, so it is a count of how many times the question was actually asked."),
NumActuallyDifferent, Probe, NumWrong)); NumActuallyDifferent, Probe, 100.0f * (float)NumActuallyDifferent / (float)Probe,
NumWrong));
if (NumActuallyDifferent == 0) // Le premier run a donné 3 sur 400 (0,75 %) : 397 sondes ne pouvaient RIEN distinguer. Même
// correction que la couverture de grotte — un seuil en fraction, pas « non nul ».
if ((float)NumActuallyDifferent < 0.05f * (float)Probe)
{ {
AddWarning(TEXT("The two param sets produced identical density at every probe point, so ") AddWarning(FString::Printf(
TEXT("this check cannot distinguish a correct cache from a stale one. Make ") TEXT("Only %d of %d probe points differ between the two param sets (want >= 5%%), so ")
TEXT("P2 differ more.")); TEXT("this check asked its question %d times, not %d. A stale cache would go ")
TEXT("unnoticed at every other point. Make P2 differ more, or probe nearer the ")
TEXT("network."),
NumActuallyDifferent, Probe, NumActuallyDifferent, Probe));
}
}
//=========================================================================
// 3b. LES PITS ET LES CHEMINÉES ONT-ILS RÉELLEMENT CONTRIBUÉ ?
//=========================================================================
// `§2` désigne ces deux boucles comme « le plus retors de toute la décomposition » : elles
// écrivent le MÊME canal SDF que le graphe de salles mais à des coordonnées NON warpées. Les
// activer dans les params ne prouve pas qu'elles ont changé quoi que ce soit — un test peut très
// bien être vert avec `SDFCache.Pits` vide.
//
// On le MESURE : la même pile avec `PitDensity = ChimneyDensity = 0`, et on compte les points où
// la densité bouge. Zéro ⇒ les deux boucles n'ont rien fait et le morceau difficile n'est pas
// couvert, quelle que soit la couleur du test.
//
// Enabling a feature in the params is not evidence it fired. This measures it.
{
FStrateGenerationParams PNoShafts = P;
PNoShafts.PitDensity = 0.0f;
PNoShafts.ChimneyDensity = 0.0f;
FVoxelOpStack StackNoShafts;
VoxelDensityOps::BuildTunnelNetworkStack(StackNoShafts, PNoShafts, World.Settings->Seed,
Gen->OriginSpineRadius, World.StrateManager.Get());
StackNoShafts.PrepareChunk(Ctx);
const int32 Probe = FMath::Min(1500, NumTunnelSamples);
// Deux passes SOLO (pas d'alternance) : on ne teste pas une clé de cache ici, seulement une
// contribution — et alterner ferait reconstruire le cache SDF à chaque point pour rien.
TArray<float> WithShafts;
WithShafts.SetNumUninitialized(Probe);
for (int32 i = 0; i < Probe; ++i)
{
WithShafts[i] = Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (float)Points[i].Z);
}
int32 NumMoved = 0;
for (int32 i = 0; i < Probe; ++i)
{
const float V = StackNoShafts.EvalMC((float)Points[i].X, (float)Points[i].Y,
(float)Points[i].Z);
if (!BitEqual(V, WithShafts[i])) { ++NumMoved; }
}
AddInfo(FString::Printf(
TEXT("Pit/chimney contribution: %d of %d probe points move when PitDensity and ")
TEXT("ChimneyDensity are zeroed. These are the two loops OPSTACK-DECOMPOSITION 2 calls ")
TEXT("the fiddliest thing in the decomposition (unwarped coords SmoothMin'd into the ")
TEXT("warped room SDF), so this number is the difference between covering them and ")
TEXT("merely having switched them on."),
NumMoved, Probe));
if (NumMoved == 0)
{
AddWarning(TEXT("Zeroing PitDensity and ChimneyDensity changed nothing, so those two ")
TEXT("loops never contributed a single voxel and the equivalence says ")
TEXT("NOTHING about them. Most likely BuildChunkCache baked no pits at this ")
TEXT("RoomSpacing/strate height -- check SDFCache.Pits, not the params."));
} }
} }