feat(classify): fold cave boundary categories safely

This commit is contained in:
2026-08-17 01:35:58 +02:00
parent 2303442d8f
commit 4d33321bfa
5 changed files with 167 additions and 43 deletions
+87
View File
@@ -4455,3 +4455,90 @@ four-way split of `Cave Bail Not Op Stack` showed `SoleSlot` and `BoundaryTile`
proved the assets were correctly ticked (Jahni said so twice; I doubted it twice) and pointed at proved the assets were correctly ticked (Jahni said so twice; I doubted it twice) and pointed at
`NoLayout`, a cause nobody had considered. *A counter that can fire for two reasons is not a `NoLayout`, a cause nobody had considered. *A counter that can fire for two reasons is not a
measurement.* measurement.*
## 2026-08-17 (v) — mixed-content split + guarded category fold LANDED, awaiting Jahni's build
**Nothing in this entry was built, compiled, run in the editor, automation-tested, or measured.**
The only validation was static source/diff inspection and `git diff --check`. All rates below are
instructions for the next flight, not claimed results.
### The old 0.77 bucket is split before changing its guard
`Cave Bail Mixed Content` had three call sites and four meanings. It is replaced by:
- `Cave Mix Out Of Layout` — a tile contains one cave slot plus constant-air outside the layout;
- `Cave Mix Gap` — one cave slot plus solid inter-strate bedrock;
- `Cave Mix Surface World` — one cave slot plus a SurfaceWorld slot;
- `Cave Bail Two Cave Slots` — a second cave slot was found; this remains an immediate `Mixed` bail.
The first three are deliberately **presence counters**, incremented once per present category per
tile before the later stack guards. They are not exclusive for an unusually tall tile, so they are
not expected to sum to the old 0.77; each answers one unambiguous yes/no question. This placement is
why the diagnostic survives the fold: a tile can increment `Cave Mix Out Of Layout` and later be
proved `AllAir`. `Two Cave Slots` is still a bail counter because that condition was not relaxed.
The split was applied first with the original `return Mixed` intact. Only after that
behavior-preserving split was inspected was the return deliberately replaced by the fold below.
### Decision: relax one-category, NOT one-cave-slot
Folding is safe, but only in the narrow form suggested by the code:
- Gap already contributes a proved `solid` hypothesis, out-of-layout a proved `air` hypothesis,
and SurfaceWorld classifies its exact mesher lattice through `ComputeSurfaceColumn`.
- The one cave slot records the minimum/maximum **sampled cave Z**. `ClassifyBox` receives that cave
Z sub-box with the tile's full XY extent; asking it about the previous full tile box would model
gap/surface/out-of-layout with cave params and is not valid.
- Opt-in, generator type, and bit-identical params are still checked for **every chunk intersecting
that cave sub-box in XYZ**. They were narrowed in Z only to exclude categories represented by
their own hypotheses; none was removed or weakened.
- The stack verdict folds into the existing `bCanSolid` / `bCanAir`, then SurfaceWorld's exact
column scan still runs. `Tiles Operator Stack Solid/Air` now increments only for the final
non-`Mixed` tile verdict, after all categories have agreed.
The independent **one-cave-slot requirement cannot be relaxed by this fold**. A second cave slot can
carry a different archetype, opt-in, params, and stack; proving it would require building and
guarding a second stack, not stretching the first. `Cave Bail Two Cave Slots` therefore still
returns `Mixed`. Category folding is compatible with the generator-type and bit-identical-param
guards precisely because those guards remain exhaustive over the sole cave sub-box.
`Cave Bail Fold Conflict` was added for a proved cave verdict that contradicts an already proved
category (for example cave=solid plus out-of-layout=air). That is distinct from `Cave Bail Stack
Verdict` (the stack itself could not prove its box) and `Cave Bail Disturbance` (a post-stack feature
killed the remaining hypothesis).
### Exact next-build / next-flight readout
Read these counters together on the same underground route:
1. `Cave Mix Out Of Layout`, `Cave Mix Gap`, `Cave Mix Surface World`, and
`Cave Bail Two Cave Slots` — whichever is largest names the dominant old mixed-content cause.
The first three are presence counts and may overlap; `Cave Bail Mixed Content` should be absent.
2. `Tiles Operator Stack Air` and `Tiles Operator Stack Solid` — either becoming non-zero proves a
cave stack participated in a **final** skip verdict. For the measured cave+open-air population,
the expected success signal is primarily `Tiles Operator Stack Air`.
3. `Tiles Skipped All Air`, `Tiles Skipped All Solid`, `Tiles Meshed`, and `Tiles Classified` — the
accounting must still close: `Meshed + SkippedAllAir + SkippedAllSolid = Classified`. A fold win
raises a skipped counter and lowers `Tiles Meshed`; the pre-fold baseline was 1.15 meshed,
0.73 skipped-air, 1.88 classified.
4. `Cave Bail Fold Conflict`, `Cave Bail Stack Verdict`, `Cave Bail Params`,
`Cave Bail Disturbance`, and `Cave Bail Two Cave Slots` — these explain mixed tiles that remain.
In particular, a high `Fold Conflict` means the dominant mix is genuinely heterogeneous rather
than blocked merely by the former one-category policy.
After Jahni runs the suite, every brute-force `violations` count must remain zero and all eight
operator-stack equivalences must remain bit-identical. Those are correctness requirements, not
results from this unbuilt change.
### Likely compile-error / review watch spots
- declaration/definition/use spelling for the five new UE stats, especially
`STAT_VoxelForgeCaveBailFoldConflict` and `STAT_VoxelForgeCaveBailTwoCaveSlots`;
- the `MAX_int32` / `MIN_int32` cave-extrema initializers and `FMath::Min/Max(int32)` assignments to
`CaveMinZ` / `CaveMaxZ`;
- construction of `FBox CaveBox` from the cave-only integer Z extrema;
- control flow after removing the cave branch's final return: SurfaceWorld columns must run before
the shared final verdict, and `bAnyCave` must imply initialized cave extrema (established in the
same `MemoCat == 2` iteration);
- no protected layout accessor was introduced: the code still uses public
`GetStrateChunkZBounds`, never `FindSlotIndexForChunkZ`.
+63 -34
View File
@@ -2725,8 +2725,8 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (CX * CX + CY * CY <= Reach * Reach) bCanSolid = false; if (CX * CX + CY * CY <= Reach * Reach) bCanSolid = false;
} }
// ── Catégorisation par Z de treillis. v1 : gap bedrock = solide ; SurfaceWorld = test // ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ;
// colonne ; tout le reste (intérieurs de caves, hors layout) = Mixed immédiat. ── // SurfaceWorld = test colonne ; un slot cave opt-in = verdict de pile sur sa sous-boîte. ──
struct FSurfSlot struct FSurfSlot
{ {
int32 BotChunkZ = INT32_MAX; // identité du slot (borne basse de la strate, en chunks) int32 BotChunkZ = INT32_MAX; // identité du slot (borne basse de la strate, en chunks)
@@ -2753,13 +2753,19 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
int32 NumSlots = 0; int32 NumSlots = 0;
// ── T1.d GÉNÉRIQUE : la pile d'opérateurs classe les archétypes de CAVE ── // ── T1.d GÉNÉRIQUE : la pile d'opérateurs classe les archétypes de CAVE ──
// Ces trois-là suivent le slot de cave que la tuile touche. Le verdict de la pile porte sur la // Ces valeurs suivent l'UNIQUE slot de cave que la tuile touche. La pile classera seulement la
// BOÎTE ENTIÈRE, pas sur un z, donc il ne peut être calculé qu'après la boucle — et il n'est // sous-boîte Z de ses échantillons ; les catégories gap/surface/hors-layout plient séparément
// valable que si la tuile ne touche QUE ce slot-là (voir la garde `bAnyNonCave`). // leurs hypothèses dans `bCanSolid` / `bCanAir`.
// These values track the ONE cave slot touched by the tile. The stack classifies only its
// sampled Z sub-box; gap/surface/out-of-layout fold their hypotheses separately.
int32 CaveBotChunkZ = INT32_MAX; // identité du slot de cave (borne basse, en chunks) int32 CaveBotChunkZ = INT32_MAX; // identité du slot de cave (borne basse, en chunks)
int32 CaveRepChunkZ = 0; int32 CaveRepChunkZ = 0;
int32 CaveMinZ = MAX_int32;
int32 CaveMaxZ = MIN_int32;
bool bAnyCave = false; bool bAnyCave = false;
bool bAnyNonCave = false; // gap ou SurfaceWorld dans la même tuile ⇒ on abandonne bool bAnyGap = false;
bool bAnySurface = false;
bool bAnyOutOfLayout = false;
int32 MemoChunkZ = INT32_MAX; int32 MemoChunkZ = INT32_MAX;
int32 MemoCat = -1; // 0 = gap, 1 = surface, 2 = cave (pile), 3 = hors layout int32 MemoCat = -1; // 0 = gap, 1 = surface, 2 = cave (pile), 3 = hors layout
@@ -2783,7 +2789,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (StrateManager->IsGapChunk(CC)) if (StrateManager->IsGapChunk(CC))
{ {
MemoCat = 0; MemoCat = 0;
bAnyNonCave = true; bAnyGap = true;
} }
//================================================================= //=================================================================
// ⛔ HORS LAYOUT = AIR CONSTANT. C'ÉTAIT LE BLOCAGE DE T1.d. // ⛔ HORS LAYOUT = AIR CONSTANT. C'ÉTAIT LE BLOCAGE DE T1.d.
@@ -2809,7 +2815,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
else if (!VF_ChunkZHasSlot(ChunkZ)) else if (!VF_ChunkZHasSlot(ChunkZ))
{ {
MemoCat = 3; MemoCat = 3;
bAnyNonCave = true; // n'entre JAMAIS dans la branche de cave bAnyOutOfLayout = true;
// Les disturbances sont appliquées APRÈS la densité d'archétype et peuvent AJOUTER // Les disturbances sont appliquées APRÈS la densité d'archétype et peuvent AJOUTER
// de la roche (ponts, arêtes). Même prudence que les branches gap et cave : si // de la roche (ponts, arêtes). Même prudence que les branches gap et cave : si
@@ -2861,7 +2867,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) bCanAir = false; if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) bCanAir = false;
} }
MemoCat = 1; MemoCat = 1;
bAnyNonCave = true; bAnySurface = true;
} }
else else
{ {
@@ -2909,12 +2915,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
int32 CaveTopCZ = 0, CaveBotCZ = 0; int32 CaveTopCZ = 0, CaveBotCZ = 0;
if (!StrateManager->GetStrateChunkZBounds(ChunkZ, CaveTopCZ, CaveBotCZ)) if (!StrateManager->GetStrateChunkZBounds(ChunkZ, CaveTopCZ, CaveBotCZ))
{ {
INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent); INC_DWORD_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
return EVoxelTileClass::Mixed; // hors layout return EVoxelTileClass::Mixed; // hors layout
} }
if (CaveBotChunkZ != INT32_MAX && CaveBotChunkZ != CaveBotCZ) if (CaveBotChunkZ != INT32_MAX && CaveBotChunkZ != CaveBotCZ)
{ {
INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent); INC_DWORD_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots);
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
CaveBotChunkZ = CaveBotCZ; CaveBotChunkZ = CaveBotCZ;
@@ -2926,7 +2932,10 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (MemoCat == 2) if (MemoCat == 2)
{ {
// Rien par z : la pile répond pour la boîte entière, après la boucle. // La pile répond après la boucle, sur la sous-boîte Z contenant exactement les
// échantillons cave (XY reste la boîte complète du treillis).
CaveMinZ = FMath::Min(CaveMinZ, Zi);
CaveMaxZ = FMath::Max(CaveMaxZ, Zi);
} }
else if (MemoCat == 0) else if (MemoCat == 0)
{ {
@@ -2970,23 +2979,27 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
// DENSITÉ de cette tuile — même fabrique, mêmes params, même drapeau. // DENSITÉ de cette tuile — même fabrique, mêmes params, même drapeau.
if (bAnyCave) if (bAnyCave)
{ {
// Une tuile mi-cave mi-surface (ou mi-gap) n'est pas classable ainsi : la pile de cave ne // Diagnostic de PRÉSENCE avant les gardes : le signal reste visible même si le pliage rend
// répond que pour SA strate, et sa boîte couvrirait des z appartenant à une autre. // finalement AllSolid/AllAir. Ces compteurs ne sont pas exclusifs entre eux sur une tuile
if (bAnyNonCave) // très haute ; chacun répond exactement à « cette catégorie était-elle aussi présente ? ».
// Presence diagnostics run before the guards, so a successful fold cannot hide the mix.
// They are not mutually exclusive for a very tall tile; each answers one exact question.
if (bAnyOutOfLayout || bAnyGap || bAnySurface)
{ {
INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent); if (bAnyOutOfLayout) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixOutOfLayout); }
return EVoxelTileClass::Mixed; if (bAnyGap) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixGap); }
if (bAnySurface) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixSurfaceWorld); }
} }
const FIntVector RepCC(0, 0, CaveRepChunkZ); const FIntVector RepCC(0, 0, CaveRepChunkZ);
const ECaveGeneratorType CaveType = StrateManager->GetGeneratorTypeForChunk(RepCC); const ECaveGeneratorType CaveType = StrateManager->GetGeneratorTypeForChunk(RepCC);
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// ⚠️ LA GARDE QUI COMPTE : LES PARAMS DOIVENT ÊTRE LES MÊMES SUR TOUTE LA TUILE // ⚠️ LA GARDE QUI COMPTE : LES PARAMS DOIVENT ÊTRE LES MÊMES SUR TOUTE LA SOUS-BOÎTE CAVE
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// `GetGenerationParams` et ses homologues BLENDENT les params dans les bandes de transition : // `GetGenerationParams` et ses homologues BLENDENT les params dans les bandes de transition :
// `Alpha` dépend du chunk Z pour `Gradient`, et du chunk XY EN PLUS pour `Interleaved`. Deux // `Alpha` dépend du chunk Z pour `Gradient`, et du chunk XY EN PLUS pour `Interleaved`. Deux
// chunks d'une même tuile peuvent donc porter des params différents — c'est le constat de // chunks d'une même sous-boîte peuvent donc porter des params différents — c'est le constat de
// `AUDIT §C2`, confirmé par lecture le 2026-07-28 — et UNE pile ne peut pas représenter DEUX // `AUDIT §C2`, confirmé par lecture le 2026-07-28 — et UNE pile ne peut pas représenter DEUX
// champs. On construit donc les params pour CHAQUE coordonnée de chunk que la boîte touche et // champs. On construit donc les params pour CHAQUE coordonnée de chunk que la boîte touche et
// on exige qu'ils soient identiques bit à bit. // on exige qu'ils soient identiques bit à bit.
@@ -2995,7 +3008,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
// `Mixed` de trop. On se trompe du côté du CPU, jamais du côté du trou. // `Mixed` de trop. On se trompe du côté du CPU, jamais du côté du trou.
const int32 CX0 = FloorDivC(MinX, CHUNK_SIZE), CX1 = FloorDivC(MaxX, CHUNK_SIZE); const int32 CX0 = FloorDivC(MinX, CHUNK_SIZE), CX1 = FloorDivC(MaxX, CHUNK_SIZE);
const int32 CY0 = FloorDivC(MinY, CHUNK_SIZE), CY1 = FloorDivC(MaxY, CHUNK_SIZE); const int32 CY0 = FloorDivC(MinY, CHUNK_SIZE), CY1 = FloorDivC(MaxY, CHUNK_SIZE);
const int32 CZ0 = FloorDivC(MinZ, CHUNK_SIZE), CZ1 = FloorDivC(MaxZ, CHUNK_SIZE); // IMPORTANT : les gardes restent complètes, mais seulement sur les chunks où le mesher
// échantillonne réellement CETTE strate cave. Inclure gap/surface/hors-layout ici ferait
// échouer la garde d'archétype avant de pouvoir plier leurs hypothèses indépendantes.
// The guards stay exhaustive over the cave samples. Non-cave chunks are intentionally not
// represented by this stack; their hypotheses were folded separately in the Z pass.
const int32 CZ0 = FloorDivC(CaveMinZ, CHUNK_SIZE), CZ1 = FloorDivC(CaveMaxZ, CHUNK_SIZE);
// Une tuile très étalée (Step élevé) toucherait trop de chunks pour que cette vérification // Une tuile très étalée (Step élevé) toucherait trop de chunks pour que cette vérification
// reste bon marché. Au-delà, `Mixed` — on renonce au gain, jamais à la sûreté. // reste bon marché. Au-delà, `Mixed` — on renonce au gain, jamais à la sûreté.
@@ -3098,7 +3116,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
} }
default: default:
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas arriver ici (bAnyNonCave) return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas être le type du slot cave
} }
bFirst = false; bFirst = false;
@@ -3134,9 +3152,9 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
} }
TileStack.PrepareChunk(OpCtx); TileStack.PrepareChunk(OpCtx);
const FBox TileBox(FVector((float)MinX, (float)MinY, (float)MinZ), const FBox CaveBox(FVector((float)MinX, (float)MinY, (float)CaveMinZ),
FVector((float)MaxX, (float)MaxY, (float)MaxZ)); FVector((float)MaxX, (float)MaxY, (float)CaveMaxZ));
const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(TileBox, OpCtx); const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(CaveBox, OpCtx);
if (StackVerdict == EVoxelTileClass::Mixed) if (StackVerdict == EVoxelTileClass::Mixed)
{ {
INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict); INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict);
@@ -3146,6 +3164,17 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; } if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; }
else { bCanSolid = false; } else { bCanSolid = false; }
// Le verdict cave se plie avec gap=solide, hors-layout=air, seals surface=solide. Si les
// deux hypothèses sont mortes ici, les catégories se contredisent : ce n'est PAS un échec
// de borne de la pile ni une disturbance.
// Fold the cave verdict with gap=solid, out-of-layout=air, and solid surface seals. If both
// hypotheses die here, the categories conflict; this is not a stack-bound/disturbance bail.
if (!bCanSolid && !bCanAir)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailFoldConflict);
return EVoxelTileClass::Mixed;
}
//--------------------------------------------------------------------- //---------------------------------------------------------------------
// ⚠️ LES DISTURBANCES NE SONT PAS DANS LA PILE (`OPSTACK-DECOMPOSITION §10.2`) : // ⚠️ LES DISTURBANCES NE SONT PAS DANS LA PILE (`OPSTACK-DECOMPOSITION §10.2`) :
// `GetDensityAt` les applique APRÈS, sur la densité déjà négatée. Un verdict qui les // `GetDensityAt` les applique APRÈS, sur la densité déjà négatée. Un verdict qui les
@@ -3156,20 +3185,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
if (D.ChasmDensity > 0.0f) { bCanSolid = false; } if (D.ChasmDensity > 0.0f) { bCanSolid = false; }
if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) { bCanAir = false; } if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) { bCanAir = false; }
if (bCanSolid == bCanAir) if (!bCanSolid && !bCanAir)
{ {
INC_DWORD_STAT(STAT_VoxelForgeCaveBailDisturbance); INC_DWORD_STAT(STAT_VoxelForgeCaveBailDisturbance);
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
if (bCanSolid)
{
INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid);
}
else
{
INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir);
}
return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
} }
// ── Balayage des colonnes XY sur le treillis exact du mesher (marge incluse). Une colonne // ── Balayage des colonnes XY sur le treillis exact du mesher (marge incluse). Une colonne
@@ -3228,6 +3248,15 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
// Ici exactement UNE hypothèse doit survivre (chaque point testé en tue une ; les tuiles // Ici exactement UNE hypothèse doit survivre (chaque point testé en tue une ; les tuiles
// sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed. // sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed.
if (bCanSolid == bCanAir) return EVoxelTileClass::Mixed; if (bCanSolid == bCanAir) return EVoxelTileClass::Mixed;
if (bAnyCave)
{
// Compte seulement les verdicts FINAUX qui sautent réellement une tuile. Une pile peut avoir
// prouvé sa sous-boîte cave puis perdre l'hypothèse sur une colonne SurfaceWorld adjacente.
// Count only final verdicts that actually skip a tile; a later surface column may still
// invalidate the hypothesis proved for the cave sub-box.
if (bCanSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); }
else { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); }
}
return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
} }
+5 -1
View File
@@ -14,9 +14,13 @@ DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile); DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout); DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck); DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck);
DEFINE_STAT(STAT_VoxelForgeCaveBailMixedContent); DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
DEFINE_STAT(STAT_VoxelForgeCaveMixGap);
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
DEFINE_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots);
DEFINE_STAT(STAT_VoxelForgeCaveBailParams); DEFINE_STAT(STAT_VoxelForgeCaveBailParams);
DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict); DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict);
DEFINE_STAT(STAT_VoxelForgeCaveBailFoldConflict);
DEFINE_STAT(STAT_VoxelForgeCaveBailDisturbance); DEFINE_STAT(STAT_VoxelForgeCaveBailDisturbance);
DEFINE_STAT(STAT_VoxelForgeCaveBailNoStack); DEFINE_STAT(STAT_VoxelForgeCaveBailNoStack);
DEFINE_STAT(STAT_VoxelForgeColumnMemoHit); DEFINE_STAT(STAT_VoxelForgeColumnMemoHit);
+6 -6
View File
@@ -313,12 +313,12 @@ public:
* (OriginVoxels, Step, CellsPerAxis) = les MÊMES arguments que GenerateMesh ; le verdict * (OriginVoxels, Step, CellsPerAxis) = les MÊMES arguments que GenerateMesh ; le verdict
* porte sur le treillis exact que le mesher échantillonnerait (marge ±1 incluse). * porte sur le treillis exact que le mesher échantillonnerait (marge ±1 incluse).
* *
* v1 : ne prouve que les chunks GAP (bedrock) et les strates SurfaceWorld — colonnes * GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ;
* terrain/plafond évaluées par le MÊME ComputeSurfaceColumn que le chemin densité (donc * SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le
* bit-identiques), bandes de seal solides, gardes spine/passages/disturbances/diff. * chemin densité. Un unique slot cave opt-in peut ajouter le verdict conservatif de sa pile,
* Tout autre archétype (intérieur de caves) ⇒ Mixed. Worker-safe (lecture seule + * sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent
* caches thread_local partagés avec GetDensityAt — un verdict Mixed laisse les colonnes * des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec
* chaudes pour la génération qui suit). * GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit).
*/ */
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const; EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
+5 -1
View File
@@ -18,9 +18,13 @@ DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Sole Slot"), STAT
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Boundary Tile"), STAT_VoxelForgeCaveBailNotOpStackBoundaryTile, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Boundary Tile"), STAT_VoxelForgeCaveBailNotOpStackBoundaryTile, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack No Layout"), STAT_VoxelForgeCaveBailNotOpStackNoLayout, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack No Layout"), STAT_VoxelForgeCaveBailNotOpStackNoLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Recheck"), STAT_VoxelForgeCaveBailNotOpStackRecheck, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Recheck"), STAT_VoxelForgeCaveBailNotOpStackRecheck, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Mixed Content"), STAT_VoxelForgeCaveBailMixedContent, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Two Cave Slots"), STAT_VoxelForgeCaveBailTwoCaveSlots, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Params"), STAT_VoxelForgeCaveBailParams, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Params"), STAT_VoxelForgeCaveBailParams, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Stack Verdict"), STAT_VoxelForgeCaveBailStackVerdict, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Stack Verdict"), STAT_VoxelForgeCaveBailStackVerdict, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Fold Conflict"), STAT_VoxelForgeCaveBailFoldConflict, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Disturbance"), STAT_VoxelForgeCaveBailDisturbance, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Disturbance"), STAT_VoxelForgeCaveBailDisturbance, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail No Stack"), STAT_VoxelForgeCaveBailNoStack, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail No Stack"), STAT_VoxelForgeCaveBailNoStack, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Column Memo Hits"), STAT_VoxelForgeColumnMemoHit, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Column Memo Hits"), STAT_VoxelForgeColumnMemoHit, STATGROUP_VoxelForge, VOXELFORGE_API);