diff --git a/CODEMAP.md b/CODEMAP.md index 6b5743a..4b821cc 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -79,7 +79,7 @@ Paths relative to `Source/VoxelForge/`. `Public/` = headers, `Private/` = impl. | `../../VoxelForge.uplugin` | Plugin manifest. One Runtime module `VoxelForge`. Beta. | | `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. | | `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) | Symbol | Line | Notes | diff --git a/CODEX-TASK-008-column-memo-and-t1d-diagnosis.md b/CODEX-TASK-008-column-memo-and-t1d-diagnosis.md new file mode 100644 index 0000000..8becb63 --- /dev/null +++ b/CODEX-TASK-008-column-memo-and-t1d-diagnosis.md @@ -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.** diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 203451c..a5165e4 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -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 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. + +## 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. diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index f222b6c..f943889 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -661,43 +661,79 @@ namespace } /** 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 taux de succès est ~1 et l'overhang lit la MÊME colonne que la source, par + * Le mémo est une boîte à index direct, comme `FSurfaceColumnBox` : la pile évalue tous + * 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. */ struct FColumn { float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; }; 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 - // si l'appelant descend une colonne Z avant de changer de XY. Le mesher n'en promet - // RIEN — s'il itère X en premier dans une tranche Z, chaque voxel raterait et on - // relancerait toute la pile de hauteur par voxel, cliff compris (4 resamples - // structurels). Ce n'est pas « un peu plus lent », c'est un ordre de grandeur sur - // l'archétype le plus cher du plugin. - // - // Table à correspondance directe, clé COMPLÈTE comparée sur touche : une collision ne - // peut que coûter un recalcul, jamais rendre une mauvaise colonne. - // - // TAILLE : un chunk fait CHUNK_SIZE² colonnes (1024 à 32³). Les 256 entrées du premier - // jet ne tenaient donc même pas UN chunk — la table se piétinait elle-même à - // l'intérieur d'une seule tuile. 4096 entrées couvrent quatre chunks de front, pour - // ~150 Ko par worker : du même ordre qu'une boîte de `GSurfColCache` (~59 Ko × 6). - // - // 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. - struct FSlot { uint64 Key; float X, Y; FColumn C; }; - thread_local FSlot Slots[4096] = {}; + // Même schéma éprouvé que `GSurfColCache` : index direct dans une boîte XY, puis un + // drapeau `Computed` par cellule. Une tuile MC pleine résolution demande + // (CHUNK_SIZE + 3)² = 35×35 = 1225 colonnes (anneau de marge inclus) ; cette boîte + // de Dim×Dim, recentrée sur le premier échantillon, les garde toutes sans collision. + // Same proven scheme as `GSurfColCache`: direct XY indexing plus one `Computed` flag per + // 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. + struct FColumnBox + { + enum : int32 { Halo = CHUNK_SIZE + 8, Dim = 2 * Halo + 1 }; + int32 BaseX = 0, BaseY = 0; + uint64 Key = 0; // strate + layout + seed + ParamsFingerprint + bool bValid = false; + FColumn Cols[Dim * Dim]; + bool Computed[Dim * Dim]; + }; + thread_local FColumnBox Box = {}; + thread_local FColumn DirectColumn = {}; - const uint32 HX = *reinterpret_cast(&WorldX); - const uint32 HY = *reinterpret_cast(&WorldY); - const uint32 Idx = ((HX * 0x9E3779B9u) ^ (HY * 0x85EBCA6Bu)) >> 20; // [0,4095] + // The production mesher and the exact-lattice classifier use integer XY. Fractional + // XY is still valid for the public density/equivalence probes: compute it directly so + // 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 (S.Key != ColumnKey || S.X != WorldX || S.Y != WorldY) + if (bIntegerXY) + { + 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); - 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.CeilSurf = CeilingStack.EvalHeight(WorldX, WorldY); @@ -754,12 +790,10 @@ namespace // 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 (bIntegerXY) { Box.Computed[CI] = true; } } - else - { - INC_DWORD_STAT(STAT_VoxelForgeColumnMemoHit); - } - return S.C; + return *MemoColumn; } /** Le champ structurel nu — l'overhang s'en sert pour emprunter la roche amont. diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index d9da7a4..1c9752b 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -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 // classerait un champ que le mesher ne produira pas. C'est le même drapeau, lu au // 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 = // deux piles, et une pile ne sait répondre que pour SA strate. int32 CaveTopCZ = 0, CaveBotCZ = 0; if (!StrateManager->GetStrateChunkZBounds(ChunkZ, CaveTopCZ, CaveBotCZ)) { + INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent); return EVoxelTileClass::Mixed; // hors layout } if (CaveBotChunkZ != INT32_MAX && CaveBotChunkZ != CaveBotCZ) { + INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent); return EVoxelTileClass::Mixed; } 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 // 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 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 // 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); - if (NumChunkCoords > 27) { return EVoxelTileClass::Mixed; } + if (NumChunkCoords > 27) + { + INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); + return EVoxelTileClass::Mixed; + } FSlabGenerationParams TileSlab; FMazeGenerationParams TileMaze; @@ -2907,12 +2921,17 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in const FIntVector CC(cx, cy, cz); if (StrateManager->GetGeneratorTypeForChunk(CC) != CaveType) { + INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); 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 // 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) { @@ -2921,28 +2940,44 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in { const FSlabGenerationParams Q = StrateManager->GetSlabParamsForChunk(CC); 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; } case ECaveGeneratorType::Maze: { const FMazeGenerationParams Q = StrateManager->GetMazeParamsForChunk(CC); 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; } case ECaveGeneratorType::VerticalShafts: { const FVerticalShaftParams Q = StrateManager->GetVerticalShaftParamsForChunk(CC); 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; } case ECaveGeneratorType::FloatingIslands: { const FFloatingIslandParams Q = StrateManager->GetFloatingIslandParamsForChunk(CC); 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; } case ECaveGeneratorType::Underwater: @@ -2950,10 +2985,15 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in { const FStrateGenerationParams Q = StrateManager->GetGenerationParams(CC); 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; } default: + INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams); 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`, // donc la pile ne décrit pas ce que le mesher verra. Aucun verdict. + INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack); return EVoxelTileClass::Mixed; } TileStack.PrepareChunk(OpCtx); @@ -2992,7 +3033,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in const FBox TileBox(FVector((float)MinX, (float)MinY, (float)MinZ), FVector((float)MaxX, (float)MaxY, (float)MaxZ)); 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; } else { bCanSolid = false; } @@ -3007,7 +3052,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in if (D.ChasmDensity > 0.0f) { bCanSolid = 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) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); diff --git a/Source/VoxelForge/Private/VoxelStats.cpp b/Source/VoxelForge/Private/VoxelStats.cpp index 0f4c01d..2a08970 100644 --- a/Source/VoxelForge/Private/VoxelStats.cpp +++ b/Source/VoxelForge/Private/VoxelStats.cpp @@ -10,5 +10,11 @@ DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir); DEFINE_STAT(STAT_VoxelForgeTilesMeshed); DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid); 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_VoxelForgeColumnMemoMiss); diff --git a/Source/VoxelForge/Public/VoxelStats.h b/Source/VoxelForge/Public/VoxelStats.h index bd92e5f..5c1720b 100644 --- a/Source/VoxelForge/Public/VoxelStats.h +++ b/Source/VoxelForge/Public/VoxelStats.h @@ -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 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("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 Misses"), STAT_VoxelForgeColumnMemoMiss, STATGROUP_VoxelForge, VOXELFORGE_API);