test(opstack B5): prove the bNearCaveSurface gate -- a voxel outside it is bit-identically untouched
STAGE B group 5 of 5, and the only one that adds no operator. Stage B is complete: 19 ops, all
twelve detail modifiers ported.
THE DECISION, restated where it is paid for
The gate is a repeated early-out inside each operator (VF_NearCaveSurface), not a scoping
container. Reasons live in the code at that function; the short form is that the stack is a flat
list ClassifyBox folds op by op, a container would have to re-implement VF_FoldOp and would hide
its children from the fold, and an op that only exists inside a container cannot become a Phase-3
asset. What that choice buys in composability it pays for in risk: TWELVE places to forget the
gate instead of one. Hence a check aimed at exactly that.
NEW CHECK 1d
Classify each sample by its final SDF (nothing downstream of the room source writes that channel),
then compare the full stack against one with the eleven controllable amplitudes zeroed:
- outside the gate the two must be BIT-IDENTICAL -- asserted, and the leak count is printed;
- inside the gate they must differ often -- printed, and ZERO is an error, because "nothing
leaked" is worthless if the answer is "nothing happened anywhere". Same trap as a coverage
guard that only fires at zero, one level up: a check can be vacuous as well as a counter.
- the outside-gate sample count is printed and warns below 10%, since deep rock is the common
case in production and a sample set that never leaves the cave does not exercise the gate.
WHAT THIS CHECK CANNOT COVER, stated in the output rather than left implicit: the column operator
(STEP 4d) has no amplitude to zero, so it cannot appear in the zeroed stack. Its gate rides on
check 1's bit-identity against the original instead. That is sound but invisible, which is why the
info line says so.
Also corrected in the test header: the modifier count is TWELVE, not the thirteen that had been
carried in the notes, and only ELEVEN of them read the per-room param copy -- surface roughness
(4b) sits before the shadow declaration and reads strate params. Both numbers matter for C1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,7 @@
|
||||
// POURQUOI UN TEST D'UNE PILE INCOMPLÈTE
|
||||
// `GetDensityWithParams` fait ~1080 lignes et douze modificateurs de détail (le chiffre « treize »
|
||||
// traînait dans les notes ; il y en a douze, et onze seulement lisent la copie de params par salle).
|
||||
// Tout porter avant de
|
||||
// pouvoir rien vérifier, ce serait écrire ~600 lignes non compilées par-dessus ~200 non vérifiées —
|
||||
// Tout porter avant de pouvoir rien vérifier, ce serait écrire ~600 lignes non compilées par-dessus ~200 non vérifiées —
|
||||
// exactement le motif que `AUDIT §P3` documente et que ce refactor a évité six fois de suite.
|
||||
//
|
||||
// La sortie : **tous les modificateurs de détail sont pilotés par une amplitude**, et
|
||||
@@ -17,14 +16,17 @@
|
||||
// Même discipline que la passe « défauts puis tous les ops ON » du test de pile de hauteur, prise
|
||||
// dans l'autre sens.
|
||||
//
|
||||
// L'ÉTAPE B A REMONTÉ CES AMPLITUDES UN GROUPE À LA FOIS, dans l'autre sens : chaque groupe porté sortait
|
||||
// de la liste des amplitudes éteintes pour entrer dans `EnableTunnelFeatures`, avec (i) une sonde de couverture
|
||||
// qui prouve qu'il a réellement bougé quelque chose et (ii) le compte d'ops de la pile qui augmente.
|
||||
// L'ÉTAPE B A REMONTÉ CES AMPLITUDES UN GROUPE À LA FOIS, dans l'autre sens : chaque groupe porté
|
||||
// sortait de la liste des amplitudes éteintes pour entrer dans `EnableTunnelFeatures`, avec (i) une
|
||||
// sonde de couverture prouvant qu'il a réellement bougé quelque chose, et (ii) le compte d'ops qui monte.
|
||||
// • B1 : rugosité de paroi, STEP 4b.
|
||||
// • B2 : terrasses, lignes de strates, nervures — STEP 4c.
|
||||
// • B3 : surplombs, falaise, festons, arches — STEP 4c.
|
||||
// • B4 (ce commit) : colonnes (4d), dômes (4g), pincement (4h), biais de sol. La liste des
|
||||
// amplitudes éteintes est vide : **l'étape B est complète**.
|
||||
// • B4 : colonnes (4d), dômes (4g), pincement (4h), biais de sol. La liste des amplitudes
|
||||
// éteintes est vide : les douze modificateurs sont portés.
|
||||
// • B5 (ce commit) : le GATE lui-même. Aucun opérateur ajouté — la décision (early-out répété par
|
||||
// opérateur plutôt que conteneur de portée) est écrite dans `VF_NearCaveSurface`, et le
|
||||
// contrôle 1d la paie : hors gate, la pile complète doit être BIT À BIT celle sans modificateurs.
|
||||
//
|
||||
// CE QUE CE TEST NE PROUVE PAS (et le dit) : rien sur l'override d'op PAR SALLE — onze des douze
|
||||
// modificateurs lisent, dans l'original, une copie des params où l'op de la salle la plus proche a
|
||||
@@ -612,6 +614,105 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// 1d. LE GATE `bNearCaveSurface` — UN VOXEL HORS PORTÉE EST-IL VRAIMENT INTACT ? (ÉTAPE B5)
|
||||
//=========================================================================
|
||||
// ⚠️ LA DÉCISION DE L'ÉTAPE B5, RAPPELÉE ICI PARCE QUE C'EST CE CONTRÔLE QUI LA PAIE.
|
||||
// Dans l'original les douze modificateurs vivent dans UN SEUL `if (bNearCaveSurface)`. La pile
|
||||
// est une LISTE PLATE, donc chaque opérateur re-teste le gate lui-même (`VF_NearCaveSurface`,
|
||||
// VoxelDensityOpStack.cpp, où le raisonnement complet est écrit). Un opérateur conteneur aurait
|
||||
// dû replier ses enfants à la place de `ClassifyBox` et les lui aurait cachés.
|
||||
//
|
||||
// Le risque que ce choix introduit est précis : **douze occasions d'oublier le gate au lieu
|
||||
// d'une**. Un oubli ne se verrait pas forcément dans l'équivalence globale (le modificateur
|
||||
// s'appliquerait aussi dans l'original si son propre range était large), d'où un contrôle dédié.
|
||||
//
|
||||
// MÉTHODE : on classe chaque échantillon par son SDF final (aucun opérateur en aval du graphe de
|
||||
// salles n'écrit ce canal), puis on compare la pile COMPLÈTE à une pile dont les onze amplitudes
|
||||
// pilotables sont à zéro. **Hors gate, les deux doivent être bit à bit identiques** ; dedans,
|
||||
// elles doivent différer souvent — sinon le contrôle ne compare rien.
|
||||
//
|
||||
// ⚠️ CE QUE CE CONTRÔLE NE PEUT PAS COUVRIR : l'opérateur de COLONNES, qui n'a aucune amplitude
|
||||
// à éteindre (cf. la note du contrôle 3b). Son gate à lui n'est couvert que par la bit-identité
|
||||
// globale du contrôle 1 face à l'original — ce qui suffit, mais ne se voit pas ici.
|
||||
{
|
||||
FStrateGenerationParams PNoMods = P;
|
||||
PNoMods.SurfaceRoughness = 0.0f;
|
||||
PNoMods.TerraceStepHeight = 0.0f;
|
||||
PNoMods.LayerLineSpacing = 0.0f;
|
||||
PNoMods.RibbingSpacing = 0.0f;
|
||||
PNoMods.OverhangStrength = 0.0f;
|
||||
PNoMods.CliffStrength = 0.0f;
|
||||
PNoMods.ScallopStrength = 0.0f;
|
||||
PNoMods.ArchDensity = 0.0f;
|
||||
PNoMods.DomeDensity = 0.0f;
|
||||
PNoMods.PinchDensity = 0.0f;
|
||||
PNoMods.FloorBias = 0.0f;
|
||||
|
||||
FVoxelOpStack NoModStack;
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(NoModStack, PNoMods, World.Settings->Seed,
|
||||
Gen->OriginSpineRadius, World.StrateManager.Get());
|
||||
NoModStack.PrepareChunk(Ctx);
|
||||
|
||||
TArray<float> NoModVals;
|
||||
NoModVals.SetNumUninitialized(NumTunnelSamples);
|
||||
TArray<uint8> bOutsideGate;
|
||||
bOutsideGate.SetNumUninitialized(NumTunnelSamples);
|
||||
|
||||
const float DetailThreshold = P.SDFBlendRadius * 3.0f;
|
||||
|
||||
for (int32 i = 0; i < NumTunnelSamples; ++i)
|
||||
{
|
||||
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
|
||||
const FVoxelOpSample S = NoModStack.EvalSample(X, Y, Z);
|
||||
NoModVals[i] = -S.Density; // même négation MC que EvalMC
|
||||
// Le gate, écrit exactement comme l'original et comme VF_NearCaveSurface.
|
||||
bOutsideGate[i] = ((S.Sdf < DetailThreshold) && (S.Sdf < FLT_MAX)) ? 0 : 1;
|
||||
}
|
||||
|
||||
int32 NumOutside = 0, NumLeaked = 0, NumInsideMoved = 0;
|
||||
for (int32 i = 0; i < NumTunnelSamples; ++i)
|
||||
{
|
||||
if (bOutsideGate[i])
|
||||
{
|
||||
++NumOutside;
|
||||
if (!BitEqual(NoModVals[i], FullVals[i])) { ++NumLeaked; }
|
||||
}
|
||||
else if (!BitEqual(NoModVals[i], FullVals[i]))
|
||||
{
|
||||
++NumInsideMoved;
|
||||
}
|
||||
}
|
||||
|
||||
TestEqual(TEXT("no detail modifier touches a voxel outside the bNearCaveSurface gate"),
|
||||
NumLeaked, 0);
|
||||
|
||||
AddInfo(FString::Printf(
|
||||
TEXT("Gate check (stage B5): %d of %d samples (%.1f%%) sit OUTSIDE the gate ")
|
||||
TEXT("(SDF >= SDFBlendRadius*3), and %d of them leaked. Inside the gate, %d samples ")
|
||||
TEXT("move when the eleven amplitudes are zeroed -- that second number is what says ")
|
||||
TEXT("the comparison is not vacuous: if it were 0, 'nothing leaked' would only mean ")
|
||||
TEXT("'nothing happened anywhere'. The column operator (STEP 4d) is NOT covered here, ")
|
||||
TEXT("having no amplitude to zero; its gate rides on check 1's bit-identity instead."),
|
||||
NumOutside, NumTunnelSamples, 100.0f * (float)NumOutside / (float)NumTunnelSamples,
|
||||
NumLeaked, NumInsideMoved));
|
||||
|
||||
if (NumOutside < NumTunnelSamples / 10)
|
||||
{
|
||||
AddWarning(FString::Printf(
|
||||
TEXT("Only %d of %d samples are outside the gate, so this check barely asked its ")
|
||||
TEXT("question. Deep rock is the common case in production -- a sampling set that ")
|
||||
TEXT("almost never leaves the cave is not representative of what the gate skips."),
|
||||
NumOutside, NumTunnelSamples));
|
||||
}
|
||||
if (NumInsideMoved == 0)
|
||||
{
|
||||
AddError(TEXT("Inside the gate, zeroing all eleven modifier amplitudes changed NOTHING. ")
|
||||
TEXT("The gate check above is therefore vacuous, and so, probably, is a large ")
|
||||
TEXT("part of check 1b. Suspect the params never reaching the operators."));
|
||||
}
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// 2. INVARIANCE DE FENÊTRE — le test qui compte le plus sur cet archétype
|
||||
//=========================================================================
|
||||
|
||||
Reference in New Issue
Block a user