fix(opstack): the warp bound was the dominant term all along -- 2.0 -> 1.5, and MEASURE it

Jahni asked whether I was in a loop. I was, and re-reading the original found it
in one line.

PerlinAbsBound was set to 2.0 in the first commit of the spatial EffectOverBox
and never revisited. The dilation is CaveWarpStrength * VOXEL_NOISE_SCALE *
PerlinAbsBound, and CaveWarpStrength is 8.0 by default:

  8 * 1.25 * 2.0 = 20 voxels, applied +/- on every axis
  tile 10 voxels -> query box 50 per side -> 125x the tile volume

So the tunnel test I called "an order of magnitude tighter" actually needed
DistToAxis >= 78 against a cull rejecting at ~107. 27% tighter, not 10x -- and
the run said so: tunnels reaching went 78.0 -> 58.5, a 25% cut. The instrument
was right and I credited the tunnels.

Four rounds -- worm, columns, sampler, tunnel disjunction -- each individually
correct, every one of them tightening around a term nobody had measured. I kept
instrumenting what I had just changed and never instrumented what I had assumed.

What re-reading showed: BuildChunkCache is called everywhere with
Expansion = CaveWarpStrength + 2.0f, which is only correct if |Perlin3D| <= 0.8.
The whole plugin has always bet on 0.8. I picked 2.0 -- 2.5x more conservative
than the assumption the cache's own correctness already rests on.

The corrected bound is derived, not guessed. GradDot returns +-u +-v with u and
v two DISTINCT components (checked on all four hash branches). Splitting the
eight corners by i gives, per axis, sum w*|dx| = (1-su)*fx + su*(1-fx) <= 0.5
(max at fx = 0.5). Hence |Perlin3D| <= S_x + S_y + S_z <= 1.5, with no case
analysis on the hashes. Dilation 20 -> 15.

And the instrument that should have existed from the start: EffectOverBox now
also runs every room/tunnel test with the warp dilation set to ZERO and reports
both, so Hit* - Hit*NoWarp is exactly the blocking caused by my box rather than
by geometry. The report says, in the output, that if that gap dominates the next
move is the warp bound and not the primitives.

Separately: the diagnostics had turned into narrative, printing hardcoded numbers
from previous runs next to live ones ("32 of 34 tiles vs 21 for rooms" while the
live figures said 21 of 28). Unreadable, and I wrote all of it. Diagnostics now
report THIS run; the history stays in OPSTACK-PROGRESS.

Unbuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 03:21:37 +02:00
parent 9733179723
commit b2938d38f1
4 changed files with 179 additions and 19 deletions
@@ -1120,6 +1120,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
const int32 SpanVoxelsReported = SpanCells * 8; // Extent = Step * Cells = 1 * 8
int32 TilesHitByRooms = 0, TilesHitByTunnels = 0, TilesHitByPits = 0, TilesHitByChimneys = 0;
int32 SumHitRooms = 0, SumNumRooms = 0, SumHitTunnels = 0, SumNumTunnels = 0;
int32 SumHitRoomsNoWarp = 0, SumHitTunnelsNoWarp = 0;
float LastWarpDilation = 0.0f;
FRandomStream Rng(97531);
for (int32 t = 0; t < 40; ++t)
@@ -1192,6 +1194,9 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
if (D.HitChimneys > 0) { ++TilesHitByChimneys; }
SumHitRooms += D.HitRooms; SumNumRooms += D.NumRooms;
SumHitTunnels += D.HitTunnels; SumNumTunnels += D.NumTunnels;
SumHitRoomsNoWarp += D.HitRoomsNoWarp;
SumHitTunnelsNoWarp += D.HitTunnelsNoWarp;
LastWarpDilation = D.WarpDilation;
}
}
@@ -1254,13 +1259,11 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
if (Breakdown.IsEmpty()) { Breakdown = TEXT("nothing -- AllSolid survived every tile"); }
AddInfo(FString::Printf(
TEXT("[%s] AllSolid killed by: %s. This is the line that replaced a guess. The first ")
TEXT("build of the spatial EffectOverBox reported 0 proved of 40, and the warning ")
TEXT("offered two candidate causes -- BOTH WRONG. The real one was a third operator ")
TEXT("nobody was looking at: FWormFieldSource answered CarveOnly everywhere, and ")
TEXT("since BaseDensity=8 < WormStrength=10 BY DEFAULT, its provable amplitude bound ")
TEXT("alone drove SolidMargin negative on every tile in the world. Attribution is ")
TEXT("cheap; a second wrong guess is not."),
TEXT("[%s] AllSolid killed by: %s. Names the first operator to kill the ")
TEXT("hypothesis, counted per tile. (Why this line exists, and the wrong guesses ")
TEXT("that preceded it, live in OPSTACK-PROGRESS and are deliberately NOT ")
TEXT("repeated here: a diagnostic that carries narrative gets its live numbers ")
TEXT("read as history and its history read as live numbers.)"),
Label, *Breakdown));
if (NumRoomKilled > 0)
@@ -1269,17 +1272,19 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
TEXT("[%s] ...and when RoomGraphSource is the killer (%d tiles), WHICH primitive class ")
TEXT("reaches the box: rooms %d, tunnels %d, pits %d, chimneys %d (tiles, not ")
TEXT("primitives -- a tile can be hit by several). Averages per killed tile: ")
TEXT("%.1f of %.1f rooms reach, %.1f of %.1f tunnels reach. This line named ")
TEXT("the tunnels (32 of 34 tiles vs 21 for rooms), and they have since been ")
TEXT("given a second test: a tunnel now also passes if its own SDF stays >= ")
TEXT("T+K over the box, which beats its bounding-sphere cull badly for a long ")
TEXT("thin capsule. So a tunnel counted HERE is one genuinely close to the ")
TEXT("box, not a bounding-sphere artefact. Rooms keep the cull test alone, ")
TEXT("because for them the cull (Rmax+3K) is tighter than the threshold ")
TEXT("(Rmax+T+K) -- that is arithmetic, not an omission."),
TEXT("%.1f of %.1f rooms reach, %.1f of %.1f tunnels reach. ")
TEXT("WARP SHARE: the query box is dilated by +/-%.1f voxels per axis for the ")
TEXT("warped room/tunnel query; with that dilation set to ZERO the same tests ")
TEXT("would keep only %.1f rooms and %.1f tunnels. The gap between those ")
TEXT("pairs is blocking caused by MY BOX rather than by geometry -- the term ")
TEXT("that went unmeasured while three rounds of tightening happened around ")
TEXT("it. If the gap dominates, tighten the warp bound, not the primitives."),
Label, NumRoomKilled, TilesHitByRooms, TilesHitByTunnels, TilesHitByPits, TilesHitByChimneys,
(float)SumHitRooms / (float)NumRoomKilled, (float)SumNumRooms / (float)NumRoomKilled,
(float)SumHitTunnels / (float)NumRoomKilled, (float)SumNumTunnels / (float)NumRoomKilled));
(float)SumHitTunnels / (float)NumRoomKilled, (float)SumNumTunnels / (float)NumRoomKilled,
LastWarpDilation,
(float)SumHitRoomsNoWarp / (float)NumRoomKilled,
(float)SumHitTunnelsNoWarp / (float)NumRoomKilled));
}
}
@@ -2181,6 +2181,11 @@ namespace
* `VoxelDensityOps::GetLastRoomBoxDiagnostic`. N'entre dans aucune décision. */
int32 HitRooms = 0, HitTunnels = 0, HitPits = 0, HitChimneys = 0;
int32 NumRooms = 0, NumTunnels = 0, NumPits = 0, NumChimneys = 0;
/** Les mêmes comptes avec une dilatation de warp NULLE, et de combien de voxels la
* boîte est dilatée. C'est la mesure qui manquait pendant trois builds : sans elle,
* « les tunnels bloquent » et « ma boîte est 125x trop grosse » sont indiscernables. */
int32 HitRoomsNoWarp = 0, HitTunnelsNoWarp = 0;
float WarpDilation = 0.0f;
};
static FBoxState& BoxState()
@@ -2204,10 +2209,43 @@ namespace
* gradients.) Se tromper ici coûte une boîte de recherche un peu plus large, jamais un
* verdict faux : plus large ⇒ SUR-ensemble de primitives ⇒ `Identity` plus rare.
*
* Provable bound rather than the header's observed one: GradDot returns ru+rv with both in
* [-1,1], and a trilinear lerp stays inside the hull of its inputs. Erring high costs CPU.
* ⚠️⚠️ **CORRIGÉ DE 2.0 À 1.5 LE 2026-07-28, ET CETTE CONSTANTE ÉTAIT LE TERME DOMINANT DE
* TOUTE LA FONCTION PENDANT TROIS BUILDS.** À lire avant d'y retoucher.
*
* La dilatation vaut `CaveWarpStrength · VOXEL_NOISE_SCALE · CETTE BORNE`. Avec les défauts
* (`CaveWarpStrength = 8`, `SCALE = 1.25`) elle valait **20 voxels** — appliquée des deux
* côtés de chaque axe d'une tuile de **10 voxels**, soit une boîte de requête de 50 voxels,
* **125× le volume de la tuile**. Trois passes de resserrement (le ver, les colonnes,
* l'échantillonneur, la disjonction des tunnels) ont été faites AUTOUR de ce terme sans que
* personne ne le mesure. Le test des tunnels, annoncé « un ordre de grandeur plus serré », ne
* gagnait en pratique que 25 % — exactement parce que `BoxHalfDiag` était dominé par cette
* dilatation et non par la géométrie.
*
* ⚠️ ET LE RESTE DU PLUGIN N'A JAMAIS ÉTÉ AUSSI PRUDENT : `BuildChunkCache` est appelée avec
* `Expansion = CaveWarpStrength + 2` (ici comme dans `GetDensityWithParams`), ce qui suppose
* `|Perlin3D| · SCALE ≤ CaveWarpStrength`, donc `|Perlin3D| ≤ 0.8`. Le code qui tourne en
* production depuis toujours parie déjà là-dessus. Prendre 2.0 était 2,5× plus conservateur
* que l'hypothèse dont dépend déjà la correction du cache.
*
* LA BORNE 1.5, DÉMONTRÉE (et non observée) :
* 1. `GradDot` rend `±u ± v` où `u` et `v` sont deux composantes **distinctes** de l'offset
* du coin — vérifié sur les quatre branches du `switch` de hash, pas supposé.
* 2. Pour l'axe x : les coins à `i=0` portent le poids `(1su)` et l'offset `fx`, ceux à
* `i=1` le poids `su` et l'offset `1fx`. Donc `Σ_c w_c·|dx_c| = (1su)·fx + su·(1fx)`,
* dont le maximum sur `[0,1]` vaut **0.5** (atteint en `fx = 0.5`, où `su = 0.5` ;
* 0.302 en 0.25 comme en 0.75).
* 3. `|Perlin| ≤ Σ_c w_c(|a_c| + |b_c|) ≤ S_x + S_y + S_z ≤ 3 × 0.5 = 1.5.`
* (Le vrai maximum est plus bas encore — seuls DEUX axes apparaissent par coin — mais 1.5
* est la borne qui se démontre sans analyse de cas sur les hash. `√3/2 ≈ 0.87`, la borne
* classique de Perlin 3D, dépend du jeu de gradients : on ne s'appuie pas dessus.)
*
* Was 2.0, and that constant was the dominant term of this whole function for three builds:
* it inflated a 10-voxel tile into a 50-voxel query box (125x the volume), which is why the
* "order of magnitude tighter" tunnel test only won 25%. The rest of the plugin has always
* assumed |Perlin3D| <= 0.8 (BuildChunkCache's Expansion = CaveWarpStrength + 2). 1.5 is
* PROVED above from GradDot's two-distinct-axes form and the per-axis weighted bound of 0.5.
*/
static constexpr float PerlinAbsBound = 2.0f;
static constexpr float PerlinAbsBound = 1.5f;
/**
* ✅ LA RÉPONSE SPATIALE. La dette annoncée ici pendant tout le portage est payée.
@@ -2430,9 +2468,17 @@ namespace
B.NumChimneys = B.Cache.Chimneys.Num();
B.HitRooms = B.HitTunnels = B.HitPits = B.HitChimneys = 0;
// La MÊME boîte sans dilatation de warp — diagnostic seulement, voir plus bas.
const FVector NWMin((float)VoxelBox.Min.X, (float)VoxelBox.Min.Y, EffZ((float)VoxelBox.Min.Z));
const FVector NWMax((float)VoxelBox.Max.X, (float)VoxelBox.Max.Y, EffZ((float)VoxelBox.Max.Z));
const float NoWarpHalfDiag = 0.5f * (float)(NWMax - NWMin).Size();
B.HitRoomsNoWarp = B.HitTunnelsNoWarp = 0;
B.WarpDilation = Warp;
for (const FCachedRoom& R : B.Cache.Rooms)
{
if (SphereHitsBox(R.Center, R.CullRadiusSq, QMin, QMax)) { ++B.HitRooms; }
if (SphereHitsBox(R.Center, R.CullRadiusSq, NWMin, NWMax)) { ++B.HitRoomsNoWarp; }
}
//-----------------------------------------------------------------
// LES TUNNELS ONT DROIT À UN SECOND TEST, ET C'EST LÀ QUE SE TROUVE LE GAIN
@@ -2488,6 +2534,13 @@ namespace
if (DistToAxis - BoxHalfDiag - MaxR >= TunnelClear) { continue; }
++B.HitTunnels;
// DIAGNOSTIC — le MÊME test avec une dilatation de warp NULLE. Ne participe à aucun
// verdict ; il répond à la seule question que trois builds de resserrement n'ont
// jamais posée : « combien de ce blocage est de la géométrie, et combien est ma
// propre boîte dilatée ? ». `HitTunnels - HitTunnelsNoWarp` est exactement la part
// que le warp coûte.
if (DistToAxis - NoWarpHalfDiag - MaxR < TunnelClear) { ++B.HitTunnelsNoWarp; }
}
// Miroir exact des deux `continue` de `Eval` : actif si `Z < TopZ + BlendK` ET
// `Z >= TopZ - Depth - BlendK`.
@@ -3834,6 +3887,9 @@ VoxelDensityOps::FRoomBoxDiagnostic VoxelDensityOps::GetLastRoomBoxDiagnostic()
D.NumTunnels = B.NumTunnels;
D.NumPits = B.NumPits;
D.NumChimneys = B.NumChimneys;
D.HitRoomsNoWarp = B.HitRoomsNoWarp;
D.HitTunnelsNoWarp = B.HitTunnelsNoWarp;
D.WarpDilation = B.WarpDilation;
return D;
}
@@ -351,6 +351,13 @@ namespace VoxelDensityOps
{
int32 HitRooms = 0, HitTunnels = 0, HitPits = 0, HitChimneys = 0;
int32 NumRooms = 0, NumTunnels = 0, NumPits = 0, NumChimneys = 0;
/** Les mêmes comptes si la dilatation de warp valait ZÉRO, et de combien de voxels la boîte
* est effectivement dilatée. `Hit* - Hit*NoWarp` = la part du blocage due à MA boîte plutôt
* qu'à la géométrie. Cette mesure manquait, et son absence a coûté trois builds de
* resserrement autour du mauvais terme. */
int32 HitRoomsNoWarp = 0, HitTunnelsNoWarp = 0;
float WarpDilation = 0.0f;
};
VOXELFORGE_API FRoomBoxDiagnostic GetLastRoomBoxDiagnostic();