perf(opstack): direct-indexed column box (measured 14.7% miss); + T1.d bail attribution

BUILD RESULTS FIRST: 14/14 tests green, 0 violations. VerticalShafts box
verdicts went 0 -> 30 of 60 (zero was the number for the project's whole life).
TunnelNetwork production held at 11 proved / 14641 voxels, byte-identical --
that line was the isolated diagnostic for cab8e8f, so its stillness proves
Jahni's room/tunnel Min <= Max are correctly ordered. All eight equivalence
tests bit-identical, which TESTS the "bounds only, cannot reach density" claim
made on 7dbdf51 / eaa44bf / cab8e8f rather than asserting it.

PART A -- the column memo thrash is confirmed quantitatively. Measured 14.74%
miss rate against a prediction of 14.0% if thrashing and 1.4% if not; and 8,300
recomputes per tile against a healthy 1,225 (6.8x) from an independent
statistic. Replaced the 4096-slot direct-mapped hashed table with the
direct-indexed box scheme GSurfColCache has always used: no hash, no
collisions, every column computed exactly once. ParamsFingerprint is retained
in the box key -- its absence was the shipped bug that silently deleted the
overhang, and GSurfColCache's own omission of it was deliberately not copied.

PART B -- T1.d never fires in game: the two op-stack counters never appeared,
and since every displayed row has Min 1.00 rather than 0.00, rows only render
when a counter fires. The cave branch has 13 return-Mixed paths; guessing which
is the mistake this project keeps paying for. Six attribution counters now name
the bail category outright. Only edits there are brace-expansions so a counter
fits before each existing return -- every condition and returned value is
byte-identical.

Open falsifiable hypothesis: Tiles Meshed averages 2.13/frame, plausibly the
reason LOD rings update slowly. If misses drop ~10x and LODs speed up, the memo
was the ceiling; if not, they are separate problems, cleanly separated.

Not built -- Jahni builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 18:28:46 +02:00
parent 49a9959aed
commit 3ad2720d0a
7 changed files with 350 additions and 45 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl.
| `../../VoxelForge.uplugin` | Plugin manifest. One Runtime module `VoxelForge`. Beta. | | `../../VoxelForge.uplugin` | Plugin manifest. One Runtime module `VoxelForge`. Beta. |
| `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. | | `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. |
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). | | `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, and operator-stack verdicts. | | `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. |
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it) ### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
| Symbol | Line | Notes | | Symbol | Line | Notes |
@@ -0,0 +1,150 @@
# Codex task 008 — (A) fix the measured column-memo thrash, (B) diagnose why T1.d never fires in game
**Owner:** Codex (Model Luna, xHigh) · **Orchestrator:** Claude · **Branch:** `experimental`
**Status:** specified, not started
Two independent changes in different subsystems, deliberately bundled into one build because their
signals cannot contaminate each other: (A) is SurfaceWorld column caching, (B) is a counter in the
cave branch of `ClassifyTile`.
---
# PART A — replace the hashed column memo with a direct-indexed box
## This is now MEASURED, not suspected
`stat VoxelForge` in the running game, SurfaceWorld-dominated flight:
```
Column Memo Hits avg 102,300.84
Column Memo Misses avg 17,683.60 → miss rate 14.7%
```
Predicted **14.0%** if the table thrashes, **1.4%** if it does not. It thrashes. Second confirmation
from a different statistic: 17,683 misses ÷ 2.13 tiles meshed = **~8,300 column recomputes per
tile**, where a healthy cache does ~1,225 — **6.8×**.
**The cause.** `FSurfaceColumnSource::GetColumn` (`VoxelDensityOpStack.cpp` ~615) uses a
**direct-mapped, 4096-entry hashed** table. A direct-mapped table evicts on *collision*, not on
fullness: 1225 columns per tile in 4096 slots is a load factor of 0.30, at which ~317 columns (26%)
share a slot and evict each other — **on every one of the ~35 Z planes**, because the mesher
pre-samples Z-outermost (`VoxelMarchingCubesMesher.cpp` ~226). Each miss recomputes the entire height
stack: structural source, cliff (four structural resamples), terrace, layer-line, beach, ceiling.
## The fix — copy the scheme that already works, one file away
`GSurfColCache` / `FSurfaceColumnBox` in `VoxelGenerator.cpp` (~152, and its use at ~737) is the
original path's solution to the identical problem: a **direct-indexed box**
`CI = (IY - Box.BaseY) * Dim + (IX - Box.BaseX)` with a `Computed[CI]` flag — centred on the first
sample and rebuilt when a query leaves it. **No hash ⇒ no collisions ⇒ every column computed exactly
once.** Read it before writing; you are porting a proven scheme, not inventing one.
Apply the same structure inside `FSurfaceColumnSource`, keeping it `thread_local`.
## ⚠️ The invariant that must not be lost in the port
The current memo's validity check is `S.Key != ColumnKey || S.X != WorldX || S.Y != WorldY`, and
`ColumnKey` is built in `PrepareChunk` from **strate + layout version + seed + `ParamsFingerprint`**.
**`ParamsFingerprint` is load-bearing and its absence was a real shipped bug** — without it, two
stacks of the same strate with different params shared columns, the overhang silently vanished, and
only 69 of 20000 samples showed it. The comment at the site records this. **The new box's validity
key must still contain all four**, or you reopen a fixed bug. (Note `GSurfColCache` itself keys on
`(box XY, StrateKey, Seed, LayoutVersion)` **without** the fingerprint — do **not** copy that part;
it is the weakness the op-stack memo deliberately closed.)
Also keep the full XY comparison semantics: a lookup must never return a column computed for a
different XY. With a direct-indexed box that is structural (the index *is* the XY), but the box
bounds check must be exact.
## Keep the counters
`ColumnMemoHit` / `ColumnMemoMiss` must keep working, incremented on the same meaning (miss = a
column was recomputed). They are the before/after instrument.
## Invariants
1. **Density must not move by one bit.** This changes *caching*, never a computed value. The eight
equivalence tests — especially `SurfaceHeightEquivalence` and its overhang section — must stay
bit-identical. If your diff changes what `TerrainStack.EvalHeight` / `CeilingStack.EvalHeight`
compute, or the overhang gate maths, you have the wrong site.
2. Both consumers keep working: the source (`~858`) and the overhang (`~923`, via
`Column->GetColumn`). The overhang **must** see the same column the source did — that is
"by construction rather than by convention", and the current code says so.
3. Sizing: state in a comment how many columns a tile needs (a 35×35 grid = 1225) and size the box so
one tile fits without eviction, as `FSurfaceColumnBox` does.
4. `thread_local` stays; no shared mutable state across workers.
## Acceptance
- `ColumnMemoMiss` drops roughly **10×**; miss rate goes from **14.7% → ~1.5%**.
- The eight equivalence tests stay green and bit-identical.
- If the miss rate does **not** fall, say so plainly — a fix that does not move its own instrument is
a failed fix, not a partial one.
---
# PART B — name which guard stops T1.d in the running game
## The problem
`Tiles Operator Stack Solid` / `Air` **never appeared** in `stat VoxelForge`, while the harness proves
11 of 40 tiles at production defaults. Rows only render in frames where a counter fires, so site B is
never reached in game. The cave branch of `UVoxelGenerator::ClassifyTile` has **13 `return
EVoxelTileClass::Mixed` paths** and we cannot tell which one fires.
## What to build
Add DWORD counters to the existing `stat VoxelForge` group (`VoxelStats.h` / `.cpp`) that attribute
the bail, grouped by *reason* rather than one per line:
| counter | fires when |
|---|---|
| `CaveBailNotOpStack` | `UsesOperatorStackForChunk` is false (either the initial check or the per-chunk sweep) |
| `CaveBailMixedContent` | `bAnyNonCave` — the tile also touches a gap or SurfaceWorld chunk — or a second cave slot, or out-of-layout |
| `CaveBailParams` | the params `Memcmp` disagreed across the box, the archetype differed, or `NumChunkCoords > 27` |
| `CaveBailStackVerdict` | the stack built fine but `ClassifyBox` returned `Mixed` |
| `CaveBailDisturbance` | the final `bCanSolid == bCanAir` after disturbances |
| `CaveBailNoStack` | `VF_BuildOpStackForChunk` returned false |
Increment **exactly one** per bail, immediately before the `return`. Together with the existing
`TilesOpStackSolid` / `TilesOpStackAir`, one underground flight then names the cause outright.
## Invariants
1. **DO NOT change any control flow, condition, or return value in `ClassifyTile`.** Every `return`
there is a conservative guard that fails to `Mixed`; a wrong verdict leaves a tile with no
geometry and no collision. Add counters beside the existing returns and nothing else.
2. `ClassifyTile` is `const` and runs on **worker threads**`INC_DWORD_STAT` only, never a
`static int32++`. It routes through `FThreadStats::AddMessage` (per-thread packets), which is why
it is safe.
3. Zero cost when `STATS == 0`: compute nothing outside the macros.
4. Do not touch the non-cave parts of `ClassifyTile` (gap / SurfaceWorld / column scan).
## Acceptance
Fly underground in a TunnelNetwork strate: exactly one bail counter should dominate, or
`TilesOpStackSolid` should finally appear. Either outcome is a result.
---
# Shared rules
- **NEVER build, compile or run the editor or the tests.** Stop when the code is written.
- **Do not `git commit`, `git push`, `git checkout`, `git stash`, `git restore`.** Uncommitted work
in the tree must survive.
- Comments are French + English; match the surrounding file.
- Macro spelling: `KINDA_SMALL_NUMBER`, not the `UE_`-prefixed form.
- When inserting anything into `VoxelDensityOpStack.cpp`, put it **above the labelled end of the
anonymous namespace** — anchoring on the FACTORIES banner puts it outside and the brace closes
nothing. This mistake has been made twice in that file.
## Report
1. The diff for Part A and Part B separately.
2. `git diff --stat`.
3. Explicit confirmation that: no height-stack or overhang maths changed; the new column key still
contains strate + layout + seed + `ParamsFingerprint`; `ClassifyTile`'s control flow and return
values are untouched; exactly one bail counter fires per bail path.
4. Likely compile-error spots, specifically.
5. Anything in this spec that contradicts the code — **stop and say so rather than guessing.**
+60
View File
@@ -3959,3 +3959,63 @@ timeout that proceeds anyway is exactly what the bug already does. The three dan
Edit a strate asset while the world is streaming. Expect a **brief stall, then the edit applies, and Edit a strate asset while the world is streaming. Expect a **brief stall, then the edit applies, and
no crash.** If the `Error` log about a timed-out pause ever appears, the drain deadline (5 s) is too no crash.** If the `Error` log about a timed-out pause ever appears, the drain deadline (5 s) is too
short for the in-flight queue and that is worth knowing rather than guessing. short for the in-flight queue and that is worth knowing rather than guessing.
## 2026-08-16 (l) — BUILD RESULTS, and the column-memo fix they justified
### The build: 14/14 green, 0 violations anywhere
- **VerticalShafts box verdicts: 0 → 30 of 60**, 0 violations. **Zero was the number for this
project's entire life.** `e002bd4`'s connector capsules delivered — *after* `7dbdf51` widened that
archetype's reach by 28%, which pushed the other way.
- **`[production defaults]` TunnelNetwork: 11 proved, 14641 voxels, 0 violations — byte-identical to
the pre-change run.** That line was set up as the isolated diagnostic for `cab8e8f`: nothing else
touched TunnelNetwork, and the fix is a guaranteed no-op iff room/tunnel `Min ≤ Max`. It did not
move ⇒ **Jahni's assets have correctly ordered ranges; that hole was never being hit.**
- **All eight equivalence tests bit-identical** ⇒ the "bounds only, cannot reach density" claim on
`7dbdf51` / `eaa44bf` / `cab8e8f` is now **tested, not asserted**.
- Maze 27/60, FlatPlain 45/60, CrystalChamber 43/60 (tuned 32/60), Islands 9 AllAir — all at the new
8.0× samplers, so not comparable to earlier runs by design.
### ✅ The column-memo thrash: predicted 14.0%, measured 14.7%
```
Column Memo Hits avg 102,300.84
Column Memo Misses avg 17,683.60 → 14.74%
```
Predicted **14.0%** if thrashing, **1.4%** if not. Second confirmation from a different statistic:
17,683 misses ÷ 2.13 tiles meshed ≈ **8,300 recomputes per tile** against a healthy ~1,225 = **6.8×**
(derived 9.8×; the gap is expected since some tiles are cave and have no columns).
**This is the first quantitative confirmation of a perf hypothesis in this project made *before* the
measurement.** Fixed in `CODEX-TASK-008` Part A: the 4096-slot direct-mapped hashed table is replaced
by a **direct-indexed box**, the scheme `GSurfColCache` has always used — no hash, no collisions,
every column computed once. `ParamsFingerprint` is retained in the box key (its absence was the
shipped bug that silently deleted the overhang; `GSurfColCache` itself omits it, and that part was
deliberately **not** copied).
⚠️ Open hypothesis, falsifiable next build: **`Tiles Meshed` averages only 2.13/frame, which is
plausibly why LOD rings take so long to update.** If misses drop ~10× *and* LOD updates visibly
speed up, the memo was the throughput ceiling. If misses drop and LODs stay slow, they are separate
problems — and we will have cleanly separated them.
### ⚠️ T1.d still does NOT fire in production — and now we will know why
`Tiles Operator Stack Solid` / `Air` **never appeared** in `stat VoxelForge`. Every row that *did*
appear has `Min 1.00`, not `0.00` — rows only render in frames where the counter fires — so site B is
genuinely never reached. The `TilesSkippedAllSolid` of 1.18/frame (~45% of classified tiles) is the
**pre-existing** bedrock/surface skipping, not the prize.
The likely reason that sample missed it: 102,300 column-memo hits are `FSurfaceColumnSource`, i.e.
**SurfaceWorld-dominated flight**. But the cave branch has 13 `return Mixed` paths and guessing which
one fires is exactly the mistake this project keeps paying for. Part B adds six attribution counters
`CaveBailNotOpStack / MixedContent / Params / StackVerdict / Disturbance / NoStack` — one per bail
category. **One underground flight now names the cause outright instead of offering candidates.**
Verified in review: Part B's only edits are brace-expansions of one-line `if`s so a counter fits
before the existing `return`. **Every condition and every returned value is byte-identical** — the
control flow of `ClassifyTile` is untouched, which is the invariant that matters there.
### No crashes while editing assets
Good sign for `49a9959`, not proof — it was a race, and races hide. The stronger signal is the
absence of any `generation pause timed out` log: the drain completes well inside its 5 s deadline.
@@ -661,43 +661,79 @@ namespace
} }
/** La colonne complète, exactement les cinq sorties de `ComputeSurfaceColumn`. /** La colonne complète, exactement les cinq sorties de `ComputeSurfaceColumn`.
* Mémoïsée par (instance, X, Y) : la pile évalue tous les Z d'une colonne au même XY, donc * Le mémo est une boîte à index direct, comme `FSurfaceColumnBox` : la pile évalue tous
* le taux de succès est ~1 et l'overhang lit la MÊME colonne que la source, par * les Z d'une colonne au même XY, donc l'overhang lit la MÊME colonne que la source, par
* construction plutôt que par convention. */ * construction plutôt que par convention. */
struct FColumn { float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; }; struct FColumn { float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; };
const FColumn& GetColumn(float WorldX, float WorldY) const const FColumn& GetColumn(float WorldX, float WorldY) const
{ {
// ⚠️ POURQUOI UNE TABLE ET PAS UNE SEULE ENTRÉE. Un mémo à une entrée n'est correct que // Même schéma éprouvé que `GSurfColCache` : index direct dans une boîte XY, puis un
// si l'appelant descend une colonne Z avant de changer de XY. Le mesher n'en promet // drapeau `Computed` par cellule. Une tuile MC pleine résolution demande
// RIEN — s'il itère X en premier dans une tranche Z, chaque voxel raterait et on // (CHUNK_SIZE + 3)² = 35×35 = 1225 colonnes (anneau de marge inclus) ; cette boîte
// relancerait toute la pile de hauteur par voxel, cliff compris (4 resamples // de Dim×Dim, recentrée sur le premier échantillon, les garde toutes sans collision.
// structurels). Ce n'est pas « un peu plus lent », c'est un ordre de grandeur sur // Same proven scheme as `GSurfColCache`: direct XY indexing plus one `Computed` flag per
// l'archétype le plus cher du plugin. // cell. A full-resolution MC tile needs 35×35 = 1225 columns including its margin ring;
// // the box is sized so one tile fits without eviction.
// Table à correspondance directe, clé COMPLÈTE comparée sur touche : une collision ne struct FColumnBox
// peut que coûter un recalcul, jamais rendre une mauvaise colonne. {
// enum : int32 { Halo = CHUNK_SIZE + 8, Dim = 2 * Halo + 1 };
// TAILLE : un chunk fait CHUNK_SIZE² colonnes (1024 à 32³). Les 256 entrées du premier int32 BaseX = 0, BaseY = 0;
// jet ne tenaient donc même pas UN chunk — la table se piétinait elle-même à uint64 Key = 0; // strate + layout + seed + ParamsFingerprint
// l'intérieur d'une seule tuile. 4096 entrées couvrent quatre chunks de front, pour bool bValid = false;
// ~150 Ko par worker : du même ordre qu'une boîte de `GSurfColCache` (~59 Ko × 6). FColumn Cols[Dim * Dim];
// bool Computed[Dim * Dim];
// A chunk is CHUNK_SIZE² columns (1024), so the first draft's 256 entries could not };
// even hold one chunk and thrashed inside a single tile. 4096 covers four chunks. thread_local FColumnBox Box = {};
struct FSlot { uint64 Key; float X, Y; FColumn C; }; thread_local FColumn DirectColumn = {};
thread_local FSlot Slots[4096] = {};
const uint32 HX = *reinterpret_cast<const uint32*>(&WorldX); // The production mesher and the exact-lattice classifier use integer XY. Fractional
const uint32 HY = *reinterpret_cast<const uint32*>(&WorldY); // XY is still valid for the public density/equivalence probes: compute it directly so
const uint32 Idx = ((HX * 0x9E3779B9u) ^ (HY * 0x85EBCA6Bu)) >> 20; // [0,4095] // no integer cell can ever be returned for a different full (WorldX, WorldY) pair.
const bool bIntegerXY = WorldX == FMath::FloorToFloat(WorldX)
&& WorldY == FMath::FloorToFloat(WorldY);
FColumn* MemoColumn = &DirectColumn;
int32 CI = 0;
bool bNeedsCompute = true;
FSlot& S = Slots[Idx]; if (bIntegerXY)
if (S.Key != ColumnKey || S.X != WorldX || S.Y != WorldY) {
const int32 IX = (int32)WorldX;
const int32 IY = (int32)WorldY;
// Bounds are checked exactly before deriving CI; the index itself is the XY key.
// Les bornes sont vérifiées exactement avant CI : l'index EST la clé XY.
if (!Box.bValid || Box.Key != ColumnKey
|| IX < Box.BaseX || IX >= Box.BaseX + FColumnBox::Dim
|| IY < Box.BaseY || IY >= Box.BaseY + FColumnBox::Dim)
{
Box.BaseX = IX - FColumnBox::Halo;
Box.BaseY = IY - FColumnBox::Halo;
Box.Key = ColumnKey;
Box.bValid = true;
FMemory::Memzero(Box.Computed, sizeof(Box.Computed));
}
CI = (IY - Box.BaseY) * FColumnBox::Dim + (IX - Box.BaseX);
MemoColumn = &Box.Cols[CI];
if (Box.Computed[CI])
{
INC_DWORD_STAT(STAT_VoxelForgeColumnMemoHit);
bNeedsCompute = false;
}
else
{
INC_DWORD_STAT(STAT_VoxelForgeColumnMemoMiss);
}
}
else
{ {
INC_DWORD_STAT(STAT_VoxelForgeColumnMemoMiss); INC_DWORD_STAT(STAT_VoxelForgeColumnMemoMiss);
S.Key = ColumnKey; S.X = WorldX; S.Y = WorldY; }
FColumn& C = S.C;
if (bNeedsCompute)
{
FColumn& C = *MemoColumn;
C.TerrainZ = TerrainStack.EvalHeight(WorldX, WorldY); C.TerrainZ = TerrainStack.EvalHeight(WorldX, WorldY);
C.CeilSurf = CeilingStack.EvalHeight(WorldX, WorldY); C.CeilSurf = CeilingStack.EvalHeight(WorldX, WorldY);
@@ -754,12 +790,10 @@ namespace
// plat — mais l'amplitude y vaut 0 de toute façon. // plat — mais l'amplitude y vaut 0 de toute façon.
if (Slope > KINDA_SMALL_NUMBER) { C.DirX = GX / Slope; C.DirY = GY / Slope; } if (Slope > KINDA_SMALL_NUMBER) { C.DirX = GX / Slope; C.DirY = GY / Slope; }
} }
if (bIntegerXY) { Box.Computed[CI] = true; }
} }
else return *MemoColumn;
{
INC_DWORD_STAT(STAT_VoxelForgeColumnMemoHit);
}
return S.C;
} }
/** Le champ structurel nu — l'overhang s'en sert pour emprunter la roche amont. /** Le champ structurel nu — l'overhang s'en sert pour emprunter la roche amont.
+60 -11
View File
@@ -2807,17 +2807,23 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
// Condition 1 : la strate doit RÉELLEMENT être générée par la pile. Sinon on // Condition 1 : la strate doit RÉELLEMENT être générée par la pile. Sinon on
// classerait un champ que le mesher ne produira pas. C'est le même drapeau, lu au // classerait un champ que le mesher ne produira pas. C'est le même drapeau, lu au
// même endroit, que `GetDensityAt`. // même endroit, que `GetDensityAt`.
if (!StrateManager->UsesOperatorStackForChunk(CC)) { return EVoxelTileClass::Mixed; } if (!StrateManager->UsesOperatorStackForChunk(CC))
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStack);
return EVoxelTileClass::Mixed;
}
// Condition 2 : un seul slot de cave par tuile. Deux slots = deux jeux de params = // Condition 2 : un seul slot de cave par tuile. Deux slots = deux jeux de params =
// deux piles, et une pile ne sait répondre que pour SA strate. // deux piles, et une pile ne sait répondre que pour SA strate.
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);
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);
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
CaveBotChunkZ = CaveBotCZ; CaveBotChunkZ = CaveBotCZ;
@@ -2867,7 +2873,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{ {
// Une tuile mi-cave mi-surface (ou mi-gap) n'est pas classable ainsi : la pile de cave ne // Une tuile mi-cave mi-surface (ou mi-gap) n'est pas classable ainsi : la pile de cave ne
// répond que pour SA strate, et sa boîte couvrirait des z appartenant à une autre. // répond que pour SA strate, et sa boîte couvrirait des z appartenant à une autre.
if (bAnyNonCave) { return EVoxelTileClass::Mixed; } if (bAnyNonCave)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent);
return EVoxelTileClass::Mixed;
}
const FIntVector RepCC(0, 0, CaveRepChunkZ); const FIntVector RepCC(0, 0, CaveRepChunkZ);
const ECaveGeneratorType CaveType = StrateManager->GetGeneratorTypeForChunk(RepCC); const ECaveGeneratorType CaveType = StrateManager->GetGeneratorTypeForChunk(RepCC);
@@ -2891,7 +2901,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
// 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é.
const int64 NumChunkCoords = (int64)(CX1 - CX0 + 1) * (int64)(CY1 - CY0 + 1) * (int64)(CZ1 - CZ0 + 1); const int64 NumChunkCoords = (int64)(CX1 - CX0 + 1) * (int64)(CY1 - CY0 + 1) * (int64)(CZ1 - CZ0 + 1);
if (NumChunkCoords > 27) { return EVoxelTileClass::Mixed; } if (NumChunkCoords > 27)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed;
}
FSlabGenerationParams TileSlab; FSlabGenerationParams TileSlab;
FMazeGenerationParams TileMaze; FMazeGenerationParams TileMaze;
@@ -2907,12 +2921,17 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
const FIntVector CC(cx, cy, cz); const FIntVector CC(cx, cy, cz);
if (StrateManager->GetGeneratorTypeForChunk(CC) != CaveType) if (StrateManager->GetGeneratorTypeForChunk(CC) != CaveType)
{ {
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed; // la boîte déborde sur un autre archétype return EVoxelTileClass::Mixed; // la boîte déborde sur un autre archétype
} }
// Le drapeau doit tenir sur TOUS les chunks de la boîte, pas seulement sur celui qui a // Le drapeau doit tenir sur TOUS les chunks de la boîte, pas seulement sur celui qui a
// déclenché la tentative : un seul chunk hors pile invaliderait le verdict. // déclenché la tentative : un seul chunk hors pile invaliderait le verdict.
if (!StrateManager->UsesOperatorStackForChunk(CC)) { return EVoxelTileClass::Mixed; } if (!StrateManager->UsesOperatorStackForChunk(CC))
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStack);
return EVoxelTileClass::Mixed;
}
switch (CaveType) switch (CaveType)
{ {
@@ -2921,28 +2940,44 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{ {
const FSlabGenerationParams Q = StrateManager->GetSlabParamsForChunk(CC); const FSlabGenerationParams Q = StrateManager->GetSlabParamsForChunk(CC);
if (bFirst) { TileSlab = Q; } if (bFirst) { TileSlab = Q; }
else if (FMemory::Memcmp(&Q, &TileSlab, sizeof(Q)) != 0) { return EVoxelTileClass::Mixed; } else if (FMemory::Memcmp(&Q, &TileSlab, sizeof(Q)) != 0)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed;
}
break; break;
} }
case ECaveGeneratorType::Maze: case ECaveGeneratorType::Maze:
{ {
const FMazeGenerationParams Q = StrateManager->GetMazeParamsForChunk(CC); const FMazeGenerationParams Q = StrateManager->GetMazeParamsForChunk(CC);
if (bFirst) { TileMaze = Q; } if (bFirst) { TileMaze = Q; }
else if (FMemory::Memcmp(&Q, &TileMaze, sizeof(Q)) != 0) { return EVoxelTileClass::Mixed; } else if (FMemory::Memcmp(&Q, &TileMaze, sizeof(Q)) != 0)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed;
}
break; break;
} }
case ECaveGeneratorType::VerticalShafts: case ECaveGeneratorType::VerticalShafts:
{ {
const FVerticalShaftParams Q = StrateManager->GetVerticalShaftParamsForChunk(CC); const FVerticalShaftParams Q = StrateManager->GetVerticalShaftParamsForChunk(CC);
if (bFirst) { TileVert = Q; } if (bFirst) { TileVert = Q; }
else if (FMemory::Memcmp(&Q, &TileVert, sizeof(Q)) != 0) { return EVoxelTileClass::Mixed; } else if (FMemory::Memcmp(&Q, &TileVert, sizeof(Q)) != 0)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed;
}
break; break;
} }
case ECaveGeneratorType::FloatingIslands: case ECaveGeneratorType::FloatingIslands:
{ {
const FFloatingIslandParams Q = StrateManager->GetFloatingIslandParamsForChunk(CC); const FFloatingIslandParams Q = StrateManager->GetFloatingIslandParamsForChunk(CC);
if (bFirst) { TileFloat = Q; } if (bFirst) { TileFloat = Q; }
else if (FMemory::Memcmp(&Q, &TileFloat, sizeof(Q)) != 0) { return EVoxelTileClass::Mixed; } else if (FMemory::Memcmp(&Q, &TileFloat, sizeof(Q)) != 0)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed;
}
break; break;
} }
case ECaveGeneratorType::Underwater: case ECaveGeneratorType::Underwater:
@@ -2950,10 +2985,15 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{ {
const FStrateGenerationParams Q = StrateManager->GetGenerationParams(CC); const FStrateGenerationParams Q = StrateManager->GetGenerationParams(CC);
if (bFirst) { TileTunnel = Q; } if (bFirst) { TileTunnel = Q; }
else if (FMemory::Memcmp(&Q, &TileTunnel, sizeof(Q)) != 0) { return EVoxelTileClass::Mixed; } else if (FMemory::Memcmp(&Q, &TileTunnel, sizeof(Q)) != 0)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed;
}
break; break;
} }
default: default:
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas arriver ici (bAnyNonCave) return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas arriver ici (bAnyNonCave)
} }
@@ -2985,6 +3025,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
{ {
// Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`, // Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`,
// donc la pile ne décrit pas ce que le mesher verra. Aucun verdict. // donc la pile ne décrit pas ce que le mesher verra. Aucun verdict.
INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack);
return EVoxelTileClass::Mixed; return EVoxelTileClass::Mixed;
} }
TileStack.PrepareChunk(OpCtx); TileStack.PrepareChunk(OpCtx);
@@ -2992,7 +3033,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
const FBox TileBox(FVector((float)MinX, (float)MinY, (float)MinZ), const FBox TileBox(FVector((float)MinX, (float)MinY, (float)MinZ),
FVector((float)MaxX, (float)MaxY, (float)MaxZ)); FVector((float)MaxX, (float)MaxY, (float)MaxZ));
const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(TileBox, OpCtx); const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(TileBox, OpCtx);
if (StackVerdict == EVoxelTileClass::Mixed) { return EVoxelTileClass::Mixed; } if (StackVerdict == EVoxelTileClass::Mixed)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict);
return EVoxelTileClass::Mixed;
}
if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; } if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; }
else { bCanSolid = false; } else { bCanSolid = false; }
@@ -3007,7 +3052,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) { return EVoxelTileClass::Mixed; } if (bCanSolid == bCanAir)
{
INC_DWORD_STAT(STAT_VoxelForgeCaveBailDisturbance);
return EVoxelTileClass::Mixed;
}
if (bCanSolid) if (bCanSolid)
{ {
INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid);
+6
View File
@@ -10,5 +10,11 @@ DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir);
DEFINE_STAT(STAT_VoxelForgeTilesMeshed); DEFINE_STAT(STAT_VoxelForgeTilesMeshed);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid); DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir); DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStack);
DEFINE_STAT(STAT_VoxelForgeCaveBailMixedContent);
DEFINE_STAT(STAT_VoxelForgeCaveBailParams);
DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict);
DEFINE_STAT(STAT_VoxelForgeCaveBailDisturbance);
DEFINE_STAT(STAT_VoxelForgeCaveBailNoStack);
DEFINE_STAT(STAT_VoxelForgeColumnMemoHit); DEFINE_STAT(STAT_VoxelForgeColumnMemoHit);
DEFINE_STAT(STAT_VoxelForgeColumnMemoMiss); DEFINE_STAT(STAT_VoxelForgeColumnMemoMiss);
+6
View File
@@ -14,5 +14,11 @@ DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Air"), STAT_VoxelForge
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack"), STAT_VoxelForgeCaveBailNotOpStack, 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 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 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("Column Memo Hits"), STAT_VoxelForgeColumnMemoHit, 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 Misses"), STAT_VoxelForgeColumnMemoMiss, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Column Memo Misses"), STAT_VoxelForgeColumnMemoMiss, STATGROUP_VoxelForge, VOXELFORGE_API);