test(opstack): measure the box verdict on BOTH densities -- the fixture cannot prove a tile
The widened sampler worked: 39 of 40 tiles now land clear of the (0,0) spine.
The verdict did not move -- rooms still hit 40 of 40. By my own pre-registered
rule that means the room-lattice model was wrong, and it was, for a reason the
report had been printing at me for three runs: "= 7.6 x RoomSpacing 42".
RoomSpacing is 42 here, not the 80 I did the arithmetic with. I read the
UPROPERTY default out of the header instead of the fixture that overrides it.
Fifth premise this refactor that reversed on being checked, and the plainest:
the number was on screen.
EnableTunnelFeatures densifies on purpose -- RoomSpacing 80 -> 42, RoomDensity
0.35 -> 0.85 -- because at the defaults only 1.1% of samples were in open cave
and the equivalence compared solid rock to solid rock. That densification is what
makes check 1 mean anything, and it is exactly antagonistic to provability:
room cull radius = max(R*1.5, R*HeightRatio) + 3*SDFBlendRadius
= 1.5R + 12 for R in [10,30] => 27..57, mean ~42
lattice spacing = 42 at 85% occupancy
The mean cull radius equals the lattice spacing, so cull spheres cover that world
~3.6x over and NO box can be outside all of them. "6.3 of 8.3 rooms reach" is not
a loose test, it is a saturated world. No sampler change and no tunnel tightening
can move it -- which is why widening the sampler correctly changed nothing.
So 0 proved on this fixture is the RIGHT answer, and informative: the prize
shrinks to nothing as caves saturate. It is simply not an answer about
production.
Check 4 is now a lambda run twice -- parameterised rather than copy-pasted,
because two copies of the criterion would drift and the second would lie:
[dense fixture] as before, expected to stay ~0 and now documented as correct;
[production defaults] the same 40 tiles against RoomSpacing 80 / RoomDensity
0.35, the real UVoxelStrateDefinition defaults. Both brute-forced voxel by voxel.
Not "widen it until it passes": the dense run stays in the report, must stay ~0,
and a false verdict in either still fails. The two stacks deliberately share
FRoomGraphSource's thread_local caches, so this run and check 3 now watch each
other through the params fingerprint in the key.
Unbuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2850,3 +2850,88 @@ tunnels.
|
|||||||
|
|
||||||
`SpanCells` / `SpanVoxelsReported` / `NumTilesAwayFromSpine` are new locals hoisted **outside** the
|
`SpanCells` / `SpanVoxelsReported` / `NumTilesAwayFromSpine` are new locals hoisted **outside** the
|
||||||
tile loop (the report line needs them); `P.OriginRoomRadius` and `P.RoomSpacing` are read in check 4.
|
tile loop (the report line needs them); `P.OriginRoomRadius` and `P.RoomSpacing` are read in check 4.
|
||||||
|
|
||||||
|
## 2026-07-28 — the sampler fix worked, the prediction failed, and the fixture is the reason
|
||||||
|
|
||||||
|
The widened sampler did exactly what it was meant to: **39 of 40 tiles now land clear of the (0,0)
|
||||||
|
spine.** And the verdict did not move.
|
||||||
|
|
||||||
|
```
|
||||||
|
[before] rooms 40, tunnels 40 | 4.9 of 7.2 rooms, 69.2 of 80.4 tunnels (XY ±32)
|
||||||
|
[after ] rooms 40, tunnels 40 | 6.3 of 8.3 rooms, 78.0 of 87.7 tunnels (XY ±320, 39/40 off-spine)
|
||||||
|
```
|
||||||
|
|
||||||
|
My stated prediction was *"rooms should stop hitting every tile."* **They did not.** By my own
|
||||||
|
pre-registered rule that means the room-lattice model was wrong — and it was, for a reason printed in
|
||||||
|
the report I had been reading past for three runs:
|
||||||
|
|
||||||
|
> `= 7.6 x RoomSpacing 42`
|
||||||
|
|
||||||
|
**`RoomSpacing` is 42 here, not the 80 I computed with.** I read the `UPROPERTY` default out of the
|
||||||
|
header instead of the fixture, which overrides it. Fifth premise this refactor that reversed on being
|
||||||
|
checked, and the plainest one: *the number was on screen.*
|
||||||
|
|
||||||
|
### Why the fixture cannot ever prove a tile — arithmetic, not opinion
|
||||||
|
|
||||||
|
`EnableTunnelFeatures` densifies **on purpose**, and says so: `RoomSpacing` 80 → 42,
|
||||||
|
`RoomDensity` 0.35 → 0.85, because at the defaults the first run had **1.1 %** of samples in open
|
||||||
|
cave and the equivalence was comparing solid rock to solid rock. That densification is what makes
|
||||||
|
check 1 mean anything.
|
||||||
|
|
||||||
|
It is also exactly antagonistic to provability:
|
||||||
|
|
||||||
|
```
|
||||||
|
room cull radius = max(R·1.5, R·HeightRatio) + 3·SDFBlendRadius
|
||||||
|
= 1.5R + 12 for R ∈ [10,30] ⇒ 27 … 57, mean ≈ 42
|
||||||
|
lattice spacing = 42, occupancy 0.85
|
||||||
|
```
|
||||||
|
|
||||||
|
**The mean cull radius equals the lattice spacing.** Sphere volume over cell volume gives ~3.6×
|
||||||
|
redundant coverage — *no box in that world can be outside all room cull spheres.* `6.3 of 8.3 rooms
|
||||||
|
reach` is not a symptom of a loose test; it is the world being saturated. No sampler change and no
|
||||||
|
tightening of tunnels can move it, which is why widening the sampler correctly changed nothing.
|
||||||
|
|
||||||
|
**So `0 proved` on this fixture is the right answer**, and it is informative: the tile-skipping prize
|
||||||
|
shrinks to nothing as caves saturate. What it is *not* is an answer about production.
|
||||||
|
|
||||||
|
### Both worlds, one criterion
|
||||||
|
|
||||||
|
Check 4 is now a lambda run twice — **parameterised, not copy-pasted**, because two copies of the
|
||||||
|
criterion would drift and the second one would lie:
|
||||||
|
|
||||||
|
- **`[dense fixture]`** — as before. Expected to keep reporting ~0, and that is now written down as
|
||||||
|
the correct result rather than read as a failure.
|
||||||
|
- **`[production defaults]`** — the same 40 tiles against `RoomSpacing 80 / RoomDensity 0.35`, the
|
||||||
|
actual `UVoxelStrateDefinition` defaults, which is the world the question "how many tiles can we
|
||||||
|
skip" is really about. At that spacing, cull spheres of mean radius 42 on an 80-lattice at 35 %
|
||||||
|
occupancy cover roughly a fifth of space.
|
||||||
|
|
||||||
|
Both are brute-forced voxel by voxel. This is **not** "widen it until it passes": the dense run stays
|
||||||
|
in the report and must stay ~0, and a false verdict in either run still fails the assertion. The two
|
||||||
|
stacks deliberately share `FRoomGraphSource`'s `thread_local` caches — the params fingerprint in the
|
||||||
|
key is what keeps them apart, so this run and check 3 now watch each other.
|
||||||
|
|
||||||
|
### The lesson, and it is not the one I expected to write
|
||||||
|
|
||||||
|
Three runs in a row I reasoned from `RoomSpacing = 80` while the test printed 42 at me. The
|
||||||
|
instrument was right, the arithmetic done on it was right, and the *input to the arithmetic* was
|
||||||
|
taken from the wrong file. **"Verify the premise" has to include the premises you are confident
|
||||||
|
enough about not to look up** — especially a default, when a fixture exists whose entire job is to
|
||||||
|
override defaults.
|
||||||
|
|
||||||
|
### Ready to build. Compile-error spots
|
||||||
|
|
||||||
|
1. Check 4 is now `auto RunTileScan = [&](const FVoxelOpStack& S, const FStrateGenerationParams& TP,
|
||||||
|
const FVoxelOpContext& TCtx, const TCHAR* Label)`, called twice. Verified mechanically: zero bare
|
||||||
|
`Stack` / `P.` / `Ctx` references survive inside the lambda body, braces and parens balance.
|
||||||
|
2. Five `FString::Printf` sites gained a leading `%s` **and** the matching `Label` argument — the
|
||||||
|
mismatch was caught and fixed before commit; worth re-checking if the log looks scrambled.
|
||||||
|
3. `SparseStack` is a second `FVoxelOpStack` built by `BuildTunnelNetworkStack` and `PrepareChunk`ed.
|
||||||
|
|
||||||
|
### What to read
|
||||||
|
|
||||||
|
**`[production defaults] Box verdicts`** — the only new number that matters. Non-zero is the first
|
||||||
|
real T1.d saving in the plugin. If it is *also* zero, then the per-class line under it says whether
|
||||||
|
rooms still saturate at 80 spacing (my model is wrong again, look there) or whether tunnels are
|
||||||
|
finally alone (the segment-vs-box disjunction from the previous entry is then the whole remaining
|
||||||
|
job, and its three unverified premises are listed there).
|
||||||
|
|||||||
@@ -1102,193 +1102,250 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
|||||||
// would now forbid the very gain this change makes. What replaces it is the assertion that
|
// would now forbid the very gain this change makes. What replaces it is the assertion that
|
||||||
// actually matters — every proved tile is brute-forced voxel by voxel, because a false verdict
|
// actually matters — every proved tile is brute-forced voxel by voxel, because a false verdict
|
||||||
// means no geometry and NO COLLISION until a player falls through it.
|
// means no geometry and NO COLLISION until a player falls through it.
|
||||||
|
// ⚠️ UNE SEULE DÉFINITION DU BALAYAGE, DEUX MONDES. Voir le commentaire d'appel plus bas :
|
||||||
|
// la densité de la fixture rend ce verdict STRUCTURELLEMENT impossible, donc mesurer sur elle
|
||||||
|
// seule ne dit rien de la production. Copier-coller le balayage aurait donné deux critères qui
|
||||||
|
// divergent ; c'est un paramètre, pas un doublon.
|
||||||
|
auto RunTileScan = [&](const FVoxelOpStack& S, const FStrateGenerationParams& TP,
|
||||||
|
const FVoxelOpContext& TCtx, const TCHAR* Label)
|
||||||
{
|
{
|
||||||
int32 NumProved = 0, NumMixed = 0, NumSolid = 0, NumAir = 0;
|
int32 NumProved = 0, NumMixed = 0, NumSolid = 0, NumAir = 0;
|
||||||
int32 NumBruteSamples = 0, NumViolations = 0;
|
int32 NumBruteSamples = 0, NumViolations = 0;
|
||||||
float WorstViolation = 0.0f;
|
float WorstViolation = 0.0f;
|
||||||
TMap<FString, int32> SolidKillerCounts;
|
TMap<FString, int32> SolidKillerCounts;
|
||||||
int32 NumRoomKilled = 0, NumTilesAwayFromSpine = 0;
|
int32 NumRoomKilled = 0, NumTilesAwayFromSpine = 0;
|
||||||
// ±320 voxels = 4 x RoomSpacing. Hors de la boucle : la ligne de rapport en a besoin, et
|
// ±320 voxels = 4 x RoomSpacing. Hors de la boucle : la ligne de rapport en a besoin, et
|
||||||
// une étendue d'échantillonnage qu'on ne peut pas citer est une étendue qu'on ne surveille pas.
|
// une étendue d'échantillonnage qu'on ne peut pas citer est une étendue qu'on ne surveille pas.
|
||||||
const int32 SpanCells = 40;
|
const int32 SpanCells = 40;
|
||||||
const int32 SpanVoxelsReported = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
const int32 SpanVoxelsReported = SpanCells * 8; // Extent = Step * Cells = 1 * 8
|
||||||
int32 TilesHitByRooms = 0, TilesHitByTunnels = 0, TilesHitByPits = 0, TilesHitByChimneys = 0;
|
int32 TilesHitByRooms = 0, TilesHitByTunnels = 0, TilesHitByPits = 0, TilesHitByChimneys = 0;
|
||||||
int32 SumHitRooms = 0, SumNumRooms = 0, SumHitTunnels = 0, SumNumTunnels = 0;
|
int32 SumHitRooms = 0, SumNumRooms = 0, SumHitTunnels = 0, SumNumTunnels = 0;
|
||||||
|
|
||||||
FRandomStream Rng(97531);
|
FRandomStream Rng(97531);
|
||||||
for (int32 t = 0; t < 40; ++t)
|
for (int32 t = 0; t < 40; ++t)
|
||||||
{
|
|
||||||
const int32 Step = 1, Cells = 8;
|
|
||||||
const int32 Extent = Step * Cells;
|
|
||||||
|
|
||||||
// ⚠️ L'ÉTENDUE XY ÉTAIT ±32 VOXELS, ET C'EST CE QUI RENDAIT CE BLOC INEXPLOITABLE.
|
|
||||||
// `RandRange(-4, 4) * 8` échantillonnait 40 tuiles dans un cube de ±32 voxels autour de
|
|
||||||
// (0,0) — c'est-à-dire l'endroit le PLUS creusé du monde entier, et de loin :
|
|
||||||
// • `RoomSpacing = 80`, donc ±32 ne couvre même pas la moitié d'UNE cellule de salle ;
|
|
||||||
// • `OriginRoomRadius = 20` garantit une grosse salle exactement à (0,0), de rayon de
|
|
||||||
// cull `max(20·1.5, 8) + 3·4 = 42` — qui avale la quasi-totalité de la fenêtre ;
|
|
||||||
// • la spine (0,0) descend précisément là.
|
|
||||||
// La mesure « 4.9 salles sur 7.2 atteignent la boîte » ne décrivait donc pas la densité
|
|
||||||
// de grottes du monde, elle décrivait le hub de la spine. Aucune conclusion sur la
|
|
||||||
// prouvabilité du roc profond ne pouvait sortir de cet échantillon.
|
|
||||||
//
|
|
||||||
// ⚠️ CE N'EST PAS « ÉLARGIR JUSQU'À CE QUE ÇA PASSE ». Le verdict de chaque tuile reste
|
|
||||||
// brute-forcé voxel par voxel juste en dessous : un échantillonneur plus large qui
|
|
||||||
// produirait un verdict FAUX échoue exactement comme avant. On corrige ce que la mesure
|
|
||||||
// REGARDE, pas ce qu'elle exige.
|
|
||||||
//
|
|
||||||
// The XY extent was ±32 voxels around (0,0) -- with RoomSpacing = 80 and a guaranteed
|
|
||||||
// OriginRoomRadius = 20 room at the origin, that samples the single most cave-dense spot
|
|
||||||
// in the world and says nothing about deep rock. Widening changes what the measurement
|
|
||||||
// LOOKS AT, not what it demands: every verdict is still brute-forced below.
|
|
||||||
const FIntVector Origin(
|
|
||||||
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
|
||||||
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
|
||||||
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
|
||||||
|
|
||||||
// Combien de tuiles échappent vraiment au hub de la spine : sans ce compte, un futur
|
|
||||||
// resserrement de l'étendue redeviendrait invisible.
|
|
||||||
if (FMath::Square((float)Origin.X) + FMath::Square((float)Origin.Y)
|
|
||||||
> FMath::Square(3.0f * P.OriginRoomRadius))
|
|
||||||
{
|
{
|
||||||
++NumTilesAwayFromSpine;
|
const int32 Step = 1, Cells = 8;
|
||||||
}
|
const int32 Extent = Step * Cells;
|
||||||
const int32 GridDim = Cells + 1;
|
|
||||||
const FBox Box(
|
|
||||||
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
|
||||||
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
|
|
||||||
|
|
||||||
// ATTRIBUTION — le même pliage, mais il dit QUI tue chaque hypothèse. Le premier build
|
// ⚠️ L'ÉTENDUE XY ÉTAIT ±32 VOXELS, ET C'EST CE QUI RENDAIT CE BLOC INEXPLOITABLE.
|
||||||
// de l'`EffectOverBox` spatial est revenu vert avec 0 tuile prouvée, et le rapport ne
|
// `RandRange(-4, 4) * 8` échantillonnait 40 tuiles dans un cube de ±32 voxels autour de
|
||||||
// savait nommer aucun coupable : les deux causes que la mise en garde proposait étaient
|
// (0,0) — c'est-à-dire l'endroit le PLUS creusé du monde entier, et de loin :
|
||||||
// toutes les deux fausses, la vraie étant un troisième opérateur. On ne redevine pas.
|
// • `RoomSpacing = 80`, donc ±32 ne couvre même pas la moitié d'UNE cellule de salle ;
|
||||||
int32 SolidKiller = INDEX_NONE, AirKiller = INDEX_NONE;
|
// • `OriginRoomRadius = 20` garantit une grosse salle exactement à (0,0), de rayon de
|
||||||
const EVoxelTileClass Verdict = Stack.ClassifyBoxAttributed(Box, Ctx, SolidKiller, AirKiller);
|
// cull `max(20·1.5, 8) + 3·4 = 42` — qui avale la quasi-totalité de la fenêtre ;
|
||||||
|
// • la spine (0,0) descend précisément là.
|
||||||
|
// La mesure « 4.9 salles sur 7.2 atteignent la boîte » ne décrivait donc pas la densité
|
||||||
|
// de grottes du monde, elle décrivait le hub de la spine. Aucune conclusion sur la
|
||||||
|
// prouvabilité du roc profond ne pouvait sortir de cet échantillon.
|
||||||
|
//
|
||||||
|
// ⚠️ CE N'EST PAS « ÉLARGIR JUSQU'À CE QUE ÇA PASSE ». Le verdict de chaque tuile reste
|
||||||
|
// brute-forcé voxel par voxel juste en dessous : un échantillonneur plus large qui
|
||||||
|
// produirait un verdict FAUX échoue exactement comme avant. On corrige ce que la mesure
|
||||||
|
// REGARDE, pas ce qu'elle exige.
|
||||||
|
//
|
||||||
|
// The XY extent was ±32 voxels around (0,0) -- with RoomSpacing = 80 and a guaranteed
|
||||||
|
// OriginRoomRadius = 20 room at the origin, that samples the single most cave-dense spot
|
||||||
|
// in the world and says nothing about deep rock. Widening changes what the measurement
|
||||||
|
// LOOKS AT, not what it demands: every verdict is still brute-forced below.
|
||||||
|
const FIntVector Origin(
|
||||||
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||||
|
Rng.RandRange(-SpanCells, SpanCells) * Extent,
|
||||||
|
FMath::Clamp(Rng.RandRange(BottomVoxelZ / Extent, TopVoxelZ / Extent), -4096, 4096) * Extent);
|
||||||
|
|
||||||
if (SolidKiller != INDEX_NONE)
|
// Combien de tuiles échappent vraiment au hub de la spine : sans ce compte, un futur
|
||||||
{
|
// resserrement de l'étendue redeviendrait invisible.
|
||||||
const FString KillerName = Stack.GetOpDebugName(SolidKiller);
|
if (FMath::Square((float)Origin.X) + FMath::Square((float)Origin.Y)
|
||||||
SolidKillerCounts.FindOrAdd(KillerName)++;
|
> FMath::Square(3.0f * TP.OriginRoomRadius))
|
||||||
|
|
||||||
// VENTILATION PAR CLASSE DE PRIMITIVE. Quand c'est la source de salles qui tue,
|
|
||||||
// « les tuiles traversent une grotte » n'est pas une réponse : les salles, les
|
|
||||||
// tunnels, les pits et les cheminées ont chacun leur borne, de finesse très
|
|
||||||
// différente (une sphère englobante de capsule est un très mauvais tunnel). On lit
|
|
||||||
// ce que l'opérateur a RÉELLEMENT calculé plutôt que de rejouer le critère ici.
|
|
||||||
if (KillerName == TEXT("RoomGraphSource"))
|
|
||||||
{
|
{
|
||||||
const VoxelDensityOps::FRoomBoxDiagnostic D =
|
++NumTilesAwayFromSpine;
|
||||||
VoxelDensityOps::GetLastRoomBoxDiagnostic();
|
}
|
||||||
++NumRoomKilled;
|
const int32 GridDim = Cells + 1;
|
||||||
if (D.HitRooms > 0) { ++TilesHitByRooms; }
|
const FBox Box(
|
||||||
if (D.HitTunnels > 0) { ++TilesHitByTunnels; }
|
FVector(Origin.X - Step, Origin.Y - Step, Origin.Z - Step),
|
||||||
if (D.HitPits > 0) { ++TilesHitByPits; }
|
FVector(Origin.X + GridDim * Step, Origin.Y + GridDim * Step, Origin.Z + GridDim * Step));
|
||||||
if (D.HitChimneys > 0) { ++TilesHitByChimneys; }
|
|
||||||
SumHitRooms += D.HitRooms; SumNumRooms += D.NumRooms;
|
// ATTRIBUTION — le même pliage, mais il dit QUI tue chaque hypothèse. Le premier build
|
||||||
SumHitTunnels += D.HitTunnels; SumNumTunnels += D.NumTunnels;
|
// de l'`EffectOverBox` spatial est revenu vert avec 0 tuile prouvée, et le rapport ne
|
||||||
|
// savait nommer aucun coupable : les deux causes que la mise en garde proposait étaient
|
||||||
|
// toutes les deux fausses, la vraie étant un troisième opérateur. On ne redevine pas.
|
||||||
|
int32 SolidKiller = INDEX_NONE, AirKiller = INDEX_NONE;
|
||||||
|
const EVoxelTileClass Verdict = S.ClassifyBoxAttributed(Box, TCtx, SolidKiller, AirKiller);
|
||||||
|
|
||||||
|
if (SolidKiller != INDEX_NONE)
|
||||||
|
{
|
||||||
|
const FString KillerName = S.GetOpDebugName(SolidKiller);
|
||||||
|
SolidKillerCounts.FindOrAdd(KillerName)++;
|
||||||
|
|
||||||
|
// VENTILATION PAR CLASSE DE PRIMITIVE. Quand c'est la source de salles qui tue,
|
||||||
|
// « les tuiles traversent une grotte » n'est pas une réponse : les salles, les
|
||||||
|
// tunnels, les pits et les cheminées ont chacun leur borne, de finesse très
|
||||||
|
// différente (une sphère englobante de capsule est un très mauvais tunnel). On lit
|
||||||
|
// ce que l'opérateur a RÉELLEMENT calculé plutôt que de rejouer le critère ici.
|
||||||
|
if (KillerName == TEXT("RoomGraphSource"))
|
||||||
|
{
|
||||||
|
const VoxelDensityOps::FRoomBoxDiagnostic D =
|
||||||
|
VoxelDensityOps::GetLastRoomBoxDiagnostic();
|
||||||
|
++NumRoomKilled;
|
||||||
|
if (D.HitRooms > 0) { ++TilesHitByRooms; }
|
||||||
|
if (D.HitTunnels > 0) { ++TilesHitByTunnels; }
|
||||||
|
if (D.HitPits > 0) { ++TilesHitByPits; }
|
||||||
|
if (D.HitChimneys > 0) { ++TilesHitByChimneys; }
|
||||||
|
SumHitRooms += D.HitRooms; SumNumRooms += D.NumRooms;
|
||||||
|
SumHitTunnels += D.HitTunnels; SumNumTunnels += D.NumTunnels;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
|
||||||
|
|
||||||
|
++NumProved;
|
||||||
|
const bool bClaimSolid = (Verdict == EVoxelTileClass::AllSolid);
|
||||||
|
if (bClaimSolid) { ++NumSolid; } else { ++NumAir; }
|
||||||
|
|
||||||
|
// BRUTE FORCE — la boîte entière, pas un échantillonnage. `EvalMC` rend la convention
|
||||||
|
// du mesher (négatif = solide), donc « tout solide » veut dire qu'aucun échantillon
|
||||||
|
// n'est du côté air. On teste le SIGNE, c'est-à-dire l'existence d'une traversée
|
||||||
|
// d'isosurface : c'est exactement la propriété sur laquelle le mesher est sauté.
|
||||||
|
for (float Z = (float)Box.Min.Z; Z <= (float)Box.Max.Z; Z += 1.0f)
|
||||||
|
for (float Y = (float)Box.Min.Y; Y <= (float)Box.Max.Y; Y += 1.0f)
|
||||||
|
for (float X = (float)Box.Min.X; X <= (float)Box.Max.X; X += 1.0f)
|
||||||
|
{
|
||||||
|
const float D = S.EvalMC(X, Y, Z);
|
||||||
|
++NumBruteSamples;
|
||||||
|
const bool bViolates = bClaimSolid ? (D > 0.0f) : (D < 0.0f);
|
||||||
|
if (bViolates)
|
||||||
|
{
|
||||||
|
++NumViolations;
|
||||||
|
WorstViolation = FMath::Max(WorstViolation, FMath::Abs(D));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Verdict == EVoxelTileClass::Mixed) { ++NumMixed; continue; }
|
|
||||||
|
|
||||||
++NumProved;
|
|
||||||
const bool bClaimSolid = (Verdict == EVoxelTileClass::AllSolid);
|
|
||||||
if (bClaimSolid) { ++NumSolid; } else { ++NumAir; }
|
|
||||||
|
|
||||||
// BRUTE FORCE — la boîte entière, pas un échantillonnage. `EvalMC` rend la convention
|
|
||||||
// du mesher (négatif = solide), donc « tout solide » veut dire qu'aucun échantillon
|
|
||||||
// n'est du côté air. On teste le SIGNE, c'est-à-dire l'existence d'une traversée
|
|
||||||
// d'isosurface : c'est exactement la propriété sur laquelle le mesher est sauté.
|
|
||||||
for (float Z = (float)Box.Min.Z; Z <= (float)Box.Max.Z; Z += 1.0f)
|
|
||||||
for (float Y = (float)Box.Min.Y; Y <= (float)Box.Max.Y; Y += 1.0f)
|
|
||||||
for (float X = (float)Box.Min.X; X <= (float)Box.Max.X; X += 1.0f)
|
|
||||||
{
|
|
||||||
const float D = Stack.EvalMC(X, Y, Z);
|
|
||||||
++NumBruteSamples;
|
|
||||||
const bool bViolates = bClaimSolid ? (D > 0.0f) : (D < 0.0f);
|
|
||||||
if (bViolates)
|
|
||||||
{
|
|
||||||
++NumViolations;
|
|
||||||
WorstViolation = FMath::Max(WorstViolation, FMath::Abs(D));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
|
||||||
TEXT("Tile sampler: 40 tiles of 10 voxels, XY drawn from +/-%d voxels (= %.1f x ")
|
|
||||||
TEXT("RoomSpacing %.0f), Z across the strate; %d of 40 landed further than 3 x ")
|
|
||||||
TEXT("OriginRoomRadius from the (0,0) spine. THIS LINE EXISTS BECAUSE THE SAMPLER WAS ")
|
|
||||||
TEXT("THE BUG ONCE: it drew XY from +/-32 voxels, i.e. entirely inside the origin room's ")
|
|
||||||
TEXT("cull sphere, so every number below described the spine hub rather than the world. ")
|
|
||||||
TEXT("If the last count is low, nothing below says anything about deep rock."),
|
|
||||||
SpanVoxelsReported, (float)SpanVoxelsReported / FMath::Max(P.RoomSpacing, 1.0f),
|
|
||||||
P.RoomSpacing, NumTilesAwayFromSpine));
|
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
|
||||||
TEXT("Box verdicts over 40 TunnelNetwork tiles: %d proved (%d AllSolid, %d AllAir), ")
|
|
||||||
TEXT("%d Mixed -- brute-forced over %d voxels, %d violations. This number was 0 proved / ")
|
|
||||||
TEXT("40 Mixed until FRoomGraphSource::EffectOverBox learned to answer spatially, and it ")
|
|
||||||
TEXT("is the single largest perf item of the whole plan (OPSTACK-DECOMPOSITION 0.2): a ")
|
|
||||||
TEXT("proved tile skips GenerateMesh entirely, so it trades one BuildChunkCache against ")
|
|
||||||
TEXT("30000+ density evaluations. Read the PROVED count as a measurement, never as a ")
|
|
||||||
TEXT("contract -- what is asserted below is that none of them is WRONG, because a false ")
|
|
||||||
TEXT("verdict leaves no geometry and no collision behind it."),
|
|
||||||
NumProved, NumSolid, NumAir, NumMixed, NumBruteSamples, NumViolations));
|
|
||||||
|
|
||||||
// QUI TUE `AllSolid`, ET COMBIEN DE FOIS. Toujours imprimé, pas seulement en cas d'échec :
|
|
||||||
// c'est aussi la ligne qui dit, quand des tuiles SONT prouvées, ce qui bloque les autres.
|
|
||||||
{
|
|
||||||
SolidKillerCounts.ValueSort([](int32 A, int32 B) { return A > B; });
|
|
||||||
FString Breakdown;
|
|
||||||
for (const TPair<FString, int32>& Kv : SolidKillerCounts)
|
|
||||||
{
|
|
||||||
if (!Breakdown.IsEmpty()) { Breakdown += TEXT(", "); }
|
|
||||||
Breakdown += FString::Printf(TEXT("%s x%d"), *Kv.Key, Kv.Value);
|
|
||||||
}
|
|
||||||
if (Breakdown.IsEmpty()) { Breakdown = TEXT("nothing -- AllSolid survived every tile"); }
|
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("AllSolid killed by: %s. This is the line that replaced a guess. The first ")
|
TEXT("[%s] Tile sampler: 40 tiles of 10 voxels, XY drawn from +/-%d voxels (= %.1f x ")
|
||||||
TEXT("build of the spatial EffectOverBox reported 0 proved of 40, and the warning ")
|
TEXT("RoomSpacing %.0f), Z across the strate; %d of 40 landed further than 3 x ")
|
||||||
TEXT("offered two candidate causes -- BOTH WRONG. The real one was a third operator ")
|
TEXT("OriginRoomRadius from the (0,0) spine. THIS LINE EXISTS BECAUSE THE SAMPLER WAS ")
|
||||||
TEXT("nobody was looking at: FWormFieldSource answered CarveOnly everywhere, and ")
|
TEXT("THE BUG ONCE: it drew XY from +/-32 voxels, i.e. entirely inside the origin room's ")
|
||||||
TEXT("since BaseDensity=8 < WormStrength=10 BY DEFAULT, its provable amplitude bound ")
|
TEXT("cull sphere, so every number below described the spine hub rather than the world. ")
|
||||||
TEXT("alone drove SolidMargin negative on every tile in the world. Attribution is ")
|
TEXT("If the last count is low, nothing below says anything about deep rock."),
|
||||||
TEXT("cheap; a second wrong guess is not."),
|
Label, SpanVoxelsReported, (float)SpanVoxelsReported / FMath::Max(TP.RoomSpacing, 1.0f),
|
||||||
*Breakdown));
|
TP.RoomSpacing, NumTilesAwayFromSpine));
|
||||||
|
|
||||||
if (NumRoomKilled > 0)
|
AddInfo(FString::Printf(
|
||||||
|
TEXT("[%s] Box verdicts over 40 TunnelNetwork tiles: %d proved (%d AllSolid, %d AllAir), ")
|
||||||
|
TEXT("%d Mixed -- brute-forced over %d voxels, %d violations. This number was 0 proved / ")
|
||||||
|
TEXT("40 Mixed until FRoomGraphSource::EffectOverBox learned to answer spatially, and it ")
|
||||||
|
TEXT("is the single largest perf item of the whole plan (OPSTACK-DECOMPOSITION 0.2): a ")
|
||||||
|
TEXT("proved tile skips GenerateMesh entirely, so it trades one BuildChunkCache against ")
|
||||||
|
TEXT("30000+ density evaluations. Read the PROVED count as a measurement, never as a ")
|
||||||
|
TEXT("contract -- what is asserted below is that none of them is WRONG, because a false ")
|
||||||
|
TEXT("verdict leaves no geometry and no collision behind it."),
|
||||||
|
Label, NumProved, NumSolid, NumAir, NumMixed, NumBruteSamples, NumViolations));
|
||||||
|
|
||||||
|
// QUI TUE `AllSolid`, ET COMBIEN DE FOIS. Toujours imprimé, pas seulement en cas d'échec :
|
||||||
|
// c'est aussi la ligne qui dit, quand des tuiles SONT prouvées, ce qui bloque les autres.
|
||||||
{
|
{
|
||||||
|
SolidKillerCounts.ValueSort([](int32 A, int32 B) { return A > B; });
|
||||||
|
FString Breakdown;
|
||||||
|
for (const TPair<FString, int32>& Kv : SolidKillerCounts)
|
||||||
|
{
|
||||||
|
if (!Breakdown.IsEmpty()) { Breakdown += TEXT(", "); }
|
||||||
|
Breakdown += FString::Printf(TEXT("%s x%d"), *Kv.Key, Kv.Value);
|
||||||
|
}
|
||||||
|
if (Breakdown.IsEmpty()) { Breakdown = TEXT("nothing -- AllSolid survived every tile"); }
|
||||||
|
|
||||||
AddInfo(FString::Printf(
|
AddInfo(FString::Printf(
|
||||||
TEXT("...and when RoomGraphSource is the killer (%d tiles), WHICH primitive class ")
|
TEXT("[%s] AllSolid killed by: %s. This is the line that replaced a guess. The first ")
|
||||||
TEXT("reaches the box: rooms %d, tunnels %d, pits %d, chimneys %d (tiles, not ")
|
TEXT("build of the spatial EffectOverBox reported 0 proved of 40, and the warning ")
|
||||||
TEXT("primitives -- a tile can be hit by several). Averages per killed tile: ")
|
TEXT("offered two candidate causes -- BOTH WRONG. The real one was a third operator ")
|
||||||
TEXT("%.1f of %.1f rooms reach, %.1f of %.1f tunnels reach. THIS is the line that ")
|
TEXT("nobody was looking at: FWormFieldSource answered CarveOnly everywhere, and ")
|
||||||
TEXT("says what to tighten. A tunnel is culled per voxel by its BOUNDING SPHERE, ")
|
TEXT("since BaseDensity=8 < WormStrength=10 BY DEFAULT, its provable amplitude bound ")
|
||||||
TEXT("which for a long thin capsule is an enormous over-estimate; a room's cull ")
|
TEXT("alone drove SolidMargin negative on every tile in the world. Attribution is ")
|
||||||
TEXT("sphere is a fair fit. So tunnels >> rooms here would mean the box test is ")
|
TEXT("cheap; a second wrong guess is not."),
|
||||||
TEXT("losing to capsule bounding spheres, not to real cave -- and the fix would ")
|
Label, *Breakdown));
|
||||||
TEXT("be a segment-vs-box distance, not anything about the sampler."),
|
|
||||||
NumRoomKilled, TilesHitByRooms, TilesHitByTunnels, TilesHitByPits, TilesHitByChimneys,
|
if (NumRoomKilled > 0)
|
||||||
(float)SumHitRooms / (float)NumRoomKilled, (float)SumNumRooms / (float)NumRoomKilled,
|
{
|
||||||
(float)SumHitTunnels / (float)NumRoomKilled, (float)SumNumTunnels / (float)NumRoomKilled));
|
AddInfo(FString::Printf(
|
||||||
|
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 is the line that ")
|
||||||
|
TEXT("says what to tighten. A tunnel is culled per voxel by its BOUNDING SPHERE, ")
|
||||||
|
TEXT("which for a long thin capsule is an enormous over-estimate; a room's cull ")
|
||||||
|
TEXT("sphere is a fair fit. So tunnels >> rooms here would mean the box test is ")
|
||||||
|
TEXT("losing to capsule bounding spheres, not to real cave -- and the fix would ")
|
||||||
|
TEXT("be a segment-vs-box distance, not anything about the sampler."),
|
||||||
|
Label, NumRoomKilled, TilesHitByRooms, TilesHitByTunnels, TilesHitByPits, TilesHitByChimneys,
|
||||||
|
(float)SumHitRooms / (float)NumRoomKilled, (float)SumNumRooms / (float)NumRoomKilled,
|
||||||
|
(float)SumHitTunnels / (float)NumRoomKilled, (float)SumNumTunnels / (float)NumRoomKilled));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (NumProved == 0)
|
if (NumProved == 0)
|
||||||
{
|
{
|
||||||
AddWarning(TEXT("No TunnelNetwork tile was proved, so the brute force below verified ")
|
AddWarning(FString::Printf(
|
||||||
TEXT("nothing -- it has no verdict to contradict. Do NOT re-derive the cause: ")
|
TEXT("[%s] No tile was proved, so the brute force verified nothing -- it has no ")
|
||||||
TEXT("read the 'AllSolid killed by' line above, which names the operator and ")
|
TEXT("verdict to contradict. Do NOT re-derive the cause: read the two lines above, ")
|
||||||
TEXT("counts how often. If it names RoomGraphSource, the tiles genuinely ")
|
TEXT("which name the operator and then the primitive class. ⚠️ On the DENSE ")
|
||||||
TEXT("straddle cave (check the bake-coverage line of 5b); anything else is an ")
|
TEXT("FIXTURE this is the EXPECTED and correct result, not a defect: room cull ")
|
||||||
TEXT("operator whose box answer is more pessimistic than its Eval."));
|
TEXT("radius (1.5R + 3*SDFBlendRadius, mean ~42) equals RoomSpacing 42 at 85%% ")
|
||||||
}
|
TEXT("occupancy, so cull spheres cover that world ~3.6x over and no box can be ")
|
||||||
|
TEXT("outside all of them. It is the 'production defaults' run that answers ")
|
||||||
|
TEXT("whether real worlds have skippable rock."),
|
||||||
|
Label));
|
||||||
|
}
|
||||||
|
|
||||||
TestEqual(FString::Printf(
|
TestEqual(FString::Printf(
|
||||||
TEXT("every proved TunnelNetwork tile survives brute force (worst |density| ")
|
TEXT("[%s] every proved TunnelNetwork tile survives brute force (worst ")
|
||||||
TEXT("on the wrong side: %.9g)"), WorstViolation),
|
TEXT("|density| on the wrong side: %.9g)"), Label, WorstViolation),
|
||||||
NumViolations, 0);
|
NumViolations, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// LES DEUX MONDES, ET POURQUOI IL EN FAUT DEUX
|
||||||
|
//-------------------------------------------------------------------------
|
||||||
|
// ⚠️ LA FIXTURE REND CE VERDICT STRUCTURELLEMENT IMPOSSIBLE, ET CE N'EST PAS UN DÉFAUT DE LA
|
||||||
|
// FIXTURE. `EnableTunnelFeatures` densifie délibérément (`RoomSpacing` 80 → 42,
|
||||||
|
// `RoomDensity` 0.35 → 0.85) parce qu'aux défauts le premier run n'avait que 1,1 % des
|
||||||
|
// échantillons en grotte — l'équivalence comparait du roc plein à du roc plein. Cette
|
||||||
|
// densification est ce qui rend le contrôle 1 SIGNIFIANT.
|
||||||
|
//
|
||||||
|
// Mais elle est exactement ANTAGONISTE de la prouvabilité, et l'arithmétique le dit sans
|
||||||
|
// ambiguïté : le rayon de cull d'une salle vaut `max(R·1.5, R·HeightRatio) + 3·SDFBlendRadius`,
|
||||||
|
// soit `1.5R + 12` ⇒ entre 27 et 57 pour `R ∈ [10, 30]`, moyenne ≈ 42 — c'est-à-dire
|
||||||
|
// **exactement le pas du réseau**, à 85 % d'occupation. Des sphères de cull de rayon égal au pas
|
||||||
|
// du réseau recouvrent l'espace ~3,6 fois. **Aucune boîte de ce monde ne peut être hors de
|
||||||
|
// toutes les sphères de cull.** Le « 6.3 salles sur 8.3 atteignent la boîte » mesuré est
|
||||||
|
// exactement ça, et élargir l'échantillonneur n'y a rien changé (39 tuiles sur 40 étaient déjà
|
||||||
|
// loin de la spine, et le compte est resté 40 sur 40).
|
||||||
|
//
|
||||||
|
// Donc on mesure les DEUX : la fixture dense, où `0 prouvé` est le résultat CORRECT et
|
||||||
|
// informatif (le gain disparaît quand les grottes saturent), et un jeu de params aux DÉFAUTS
|
||||||
|
// DE PRODUCTION, qui est le monde dont la question « combien de tuiles peut-on sauter » parle
|
||||||
|
// réellement. Ce n'est pas « élargir jusqu'à ce que ça passe » : les deux sont rapportés, les
|
||||||
|
// deux sont brute-forcés, et le dense DOIT continuer à rendre ~0.
|
||||||
|
//
|
||||||
|
// Two worlds on purpose: the fixture is deliberately densified so the equivalence check means
|
||||||
|
// something, and that same densification makes tile-proving structurally impossible (room cull
|
||||||
|
// radius ~= the lattice spacing, at 85% occupancy). Both are measured and both are brute-forced;
|
||||||
|
// the dense one reporting ~0 is the correct answer, not a failure.
|
||||||
|
RunTileScan(Stack, P, Ctx, TEXT("dense fixture"));
|
||||||
|
|
||||||
|
{
|
||||||
|
FStrateGenerationParams SparseP = P;
|
||||||
|
SparseP.RoomSpacing = 80.0f; // le défaut d'`UVoxelStrateDefinition`
|
||||||
|
SparseP.RoomDensity = 0.35f; // idem — voir `EnableTunnelFeatures`
|
||||||
|
|
||||||
|
FVoxelOpStack SparseStack;
|
||||||
|
VoxelDensityOps::BuildTunnelNetworkStack(SparseStack, SparseP, World.Settings->Seed,
|
||||||
|
Gen->OriginSpineRadius, World.StrateManager.Get());
|
||||||
|
SparseStack.PrepareChunk(Ctx);
|
||||||
|
|
||||||
|
// ⚠️ Les deux piles partagent les caches `thread_local` de `FRoomGraphSource`. C'est VOULU,
|
||||||
|
// et c'est exactement ce que le contrôle 3 vérifie : l'empreinte de params est dans la clé,
|
||||||
|
// donc l'une ne peut pas se servir les salles de l'autre. Le jour où ce contrôle tombe,
|
||||||
|
// cette ligne-ci devient fausse en même temps — elles se surveillent mutuellement.
|
||||||
|
RunTileScan(SparseStack, SparseP, Ctx, TEXT("production defaults"));
|
||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user