From eb317d9933f6439c82eefe675732807a0c872c16 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 16 Aug 2026 16:01:26 +0200 Subject: [PATCH] feat(stats): stat VoxelForge -- make tile skipping and the column memo observable The plugin had ZERO stat counters, so every claim this refactor makes was harness-only: "skipped correctly" and "skipped nothing" render identically. CODEX-TASK-001 and -002, executed by Codex (gpt-5.6-luna xhigh), reviewed against the source. Eight DWORD counters in a new stat group: GenerateTileResult -- TilesClassified / SkippedAllSolid / SkippedAllAir / Meshed ClassifyTile bAnyCave exit -- TilesOpStackSolid / TilesOpStackAir FSurfaceColumnSource::GetColumn -- ColumnMemoHit / ColumnMemoMiss The op-stack counters are separate from the lumped skip counters on purpose: ClassifyTile also proves AllSolid on its hand-written bedrock-gap path with no strate opted in, so the lumped number cannot show a before/after. The op-stack pair is zero BY CONSTRUCTION until a strate ticks the flag. GetColumn is instrumented and deliberately NOT fixed -- the instrument and the fix in one build would make each other unreadable. Verified against UE_5.7 Stats.h rather than assumed (first stats use in this plugin, no in-repo precedent): all four macro arities, and INC_DWORD_STAT -> FThreadStats::AddMessage, so the worker-thread safety both sites need holds by mechanism. Not built -- Jahni builds. Co-Authored-By: Claude Opus 5 --- CODEMAP.md | 1 + OPSTACK-PROGRESS.md | 67 +++++++++++++++++++ .../Private/VoxelDensityOpStack.cpp | 6 ++ Source/VoxelForge/Private/VoxelGenerator.cpp | 9 +++ Source/VoxelForge/Private/VoxelStats.cpp | 14 ++++ Source/VoxelForge/Private/VoxelWorld.cpp | 17 ++++- Source/VoxelForge/Public/VoxelStats.h | 18 +++++ 7 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 Source/VoxelForge/Private/VoxelStats.cpp create mode 100644 Source/VoxelForge/Public/VoxelStats.h diff --git a/CODEMAP.md b/CODEMAP.md index 8d4cdd9..6b5743a 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -79,6 +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. | ### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it) | Symbol | Line | Notes | diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 3bd08f5..718dccc 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -3430,3 +3430,70 @@ nothing** — deliberately, so the instrument and the fix cannot land in the sam other unreadable. A negative result is specified as a real result. `CODEX-TASK-002-column-memo-thrash.md`. **Nothing was built or run.** + +## 2026-08-16 (c) — Codex delivered 001 + 002. Reviewed against the code. READY TO BUILD. + +First tandem round actually executed by me rather than handed over: Codex (`gpt-5.6-luna`, xhigh) +ran both specs, I reviewed the diffs against the source rather than against its reports. + +### What landed + +`stat VoxelForge` — the plugin's **first stat group ever** (`grep INC_DWORD_STAT` used to return +nothing). New `Public/VoxelStats.h` + `Private/VoxelStats.cpp`, eight `DWORD_COUNTER`s: + +| counter | site | +|---|---| +| `TilesClassified` / `TilesSkippedAllSolid` / `TilesSkippedAllAir` / `TilesMeshed` | `AVoxelWorld::GenerateTileResult` | +| `TilesOpStackSolid` / `TilesOpStackAir` | `UVoxelGenerator::ClassifyTile`, the `bAnyCave` exit | +| `ColumnMemoHit` / `ColumnMemoMiss` | `FSurfaceColumnSource::GetColumn` | + +### The review — what was checked, not what was reported + +- **Site A's verdict refactor is contract-identical.** `bTrivialEmpty = (Verdict != Mixed)` where + `Verdict` is the stored return. The gate's five clauses are untouched. That bool decides whether a + tile has **collision**, so this was checked character by character rather than read. +- **Site B increments AFTER the `bCanSolid == bCanAir` bail-out**, and the pair follows `bCanSolid` + the same way the returned enum does — the swap that would compile, look plausible, and prove the + wrong thing did not happen. +- **`GetColumn` is instrumented and NOT fixed**, which was the whole point of specifying 002 that + way. Table size, hash and the full key comparison are byte-identical; miss inside the `if`, hit in + the `else`, neither derived by subtracting from the other. +- **Every `INC_DWORD_STAT` body is braced.** The macro expands to a braced block, so an unbraced + `if` would have left a stray `;` and broken the following `else`. + +### The macro arities were VERIFIED, not assumed + +This is the plugin's first use of the stats system, so there was no in-repo precedent to match and +the usual "likely compile spot" would have been a guess. Read out of +`UE_5.7/Engine/Source/Runtime/Core/Public/Stats/Stats.h` instead: + +- `DECLARE_STATS_GROUP(GroupDesc, GroupId, GroupCat)` — 3 args ✔ +- `DECLARE_DWORD_COUNTER_STAT_EXTERN(CounterName, StatId, GroupId, API)` — 4 args ✔ +- `DEFINE_STAT(Stat)` ✔ · `INC_DWORD_STAT(Stat)` ✔ +- `INC_DWORD_STAT` → **`FThreadStats::AddMessage`**, i.e. per-thread stat packets ⇒ **the + worker-thread invariant holds by mechanism**, not by hope. Both sites are on workers. +- `Stats/Stats.h` includes `CoreGlobals.h` + `CoreTypes.h` itself ⇒ `VoxelStats.h` is self- + sufficient and IWYU-clean as a first include (it is, in `VoxelStats.cpp`). +- `VOXELFORGE_API` needs no include — UBT defines it on the command line. +- `VoxelForge.Build.cs` does not enumerate sources, so no build-script change is needed. + +### READY TO BUILD — and what to read afterwards + +Remaining compile risk is low and concentrated in `VoxelStats.h`/`.cpp` (identifier mismatch between +the eight declarations and the eight definitions — checked by eye, they match). + +Three readings, and **the order matters**: + +1. **Baseline, nothing ticked, underground:** `TilesOpStackSolid` / `TilesOpStackAir` must be **0**. + They are zero by construction (the cave branch returns `Mixed` at `UsesOperatorStackForChunk`), + but observe it anyway or the next step proves nothing. `TilesSkippedAllSolid` **will** be non-zero + here — that is the pre-existing bedrock/surface skipping, not a bug. +2. **Tick `bUseOperatorStack` on ONE `TunnelNetwork` strate, same route:** `TilesOpStackSolid` goes + **non-zero**. ← the production proof of T1.d, which has never existed. +3. **A `SurfaceWorld` strate ticked:** report `ColumnMemoHit` **and** `ColumnMemoMiss`, both numbers. + Miss ≈ once per distinct column ⇒ my ~9× thrash derivation is **wrong**, the table is fine, and + the perf cost is the 19-virtual-calls suspect instead. Miss stuck at 20–30 % of lookups ⇒ + confirmed, and the fix gets its own task with this run as its before. + +Also still unbuilt and riding along: `e002bd4` (VerticalShafts connector capsules) — +`Box verdicts over 60 VerticalShafts tiles`, **0 is the number to beat**, `violations` must stay 0. diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index cff8252..e606181 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -29,6 +29,7 @@ #include "VoxelTerrainOpDefinition.h" // ApplyTo — l'override d'op PAR SALLE (étape C1) #include "VoxelStrateManager.h" // EvaluateModifierSDF / AnyPassageNearBox #include "VoxelTypes.h" // SmoothStep01, VOXEL_NOISE_SCALE +#include "VoxelStats.h" #include // l'id d'instance non recyclé du mémo de colonne @@ -641,6 +642,7 @@ namespace FSlot& S = Slots[Idx]; if (S.Key != ColumnKey || S.X != WorldX || S.Y != WorldY) { + INC_DWORD_STAT(STAT_VoxelForgeColumnMemoMiss); S.Key = ColumnKey; S.X = WorldX; S.Y = WorldY; FColumn& C = S.C; @@ -700,6 +702,10 @@ namespace if (Slope > KINDA_SMALL_NUMBER) { C.DirX = GX / Slope; C.DirY = GY / Slope; } } } + else + { + INC_DWORD_STAT(STAT_VoxelForgeColumnMemoHit); + } return S.C; } diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index ba42966..d9da7a4 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -16,6 +16,7 @@ #include "VoxelDensityPrimitives.h" // spine / seal / passage — shared with the operator stack #include "VoxelDensityOpStack.h" // OPSTACK Phase 1: the opt-in per-strate operator stack #include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it +#include "VoxelStats.h" //============================================================================= // L'ADAPTATEUR DE CHAMP DE BIOMES / THE BIOME FIELD ADAPTER @@ -3007,6 +3008,14 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) { bCanAir = false; } if (bCanSolid == bCanAir) { return EVoxelTileClass::Mixed; } + if (bCanSolid) + { + INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); + } + else + { + INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); + } return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; } diff --git a/Source/VoxelForge/Private/VoxelStats.cpp b/Source/VoxelForge/Private/VoxelStats.cpp new file mode 100644 index 0000000..0f4c01d --- /dev/null +++ b/Source/VoxelForge/Private/VoxelStats.cpp @@ -0,0 +1,14 @@ +// VoxelStats.cpp +// Definitions for the VoxelForge runtime statistics. +// Définitions des statistiques runtime de VoxelForge. + +#include "VoxelStats.h" + +DEFINE_STAT(STAT_VoxelForgeTilesClassified); +DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllSolid); +DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir); +DEFINE_STAT(STAT_VoxelForgeTilesMeshed); +DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid); +DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir); +DEFINE_STAT(STAT_VoxelForgeColumnMemoHit); +DEFINE_STAT(STAT_VoxelForgeColumnMemoMiss); diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index c2f22c1..5259c21 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -11,6 +11,7 @@ #include "VoxelTerrainOpDefinition.h" #include "VoxelContentManager.h" #include "VoxelDensityVolume.h" +#include "VoxelStats.h" // IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc // APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController était // complet par transitivité seulement : on l'inclut explicitement, c'est exactement la fragilité @@ -1502,11 +1503,24 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector if (!bSheetTile && !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile); - bTrivialEmpty = (Generator->ClassifyTile(OriginVoxels, Step, Cells) != EVoxelTileClass::Mixed); + INC_DWORD_STAT(STAT_VoxelForgeTilesClassified); + const EVoxelTileClass Verdict = Generator->ClassifyTile(OriginVoxels, Step, Cells); + if (Verdict == EVoxelTileClass::AllSolid) + { + INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllSolid); + } + else if (Verdict == EVoxelTileClass::AllAir) + { + INC_DWORD_STAT(STAT_VoxelForgeTilesSkippedAllAir); + } + bTrivialEmpty = (Verdict != EVoxelTileClass::Mixed); } // F18 — feuille : deux heightfields sol/cap échantillonnés par colonne (pas de marching // cubes, pas de classifieur — la classe de surface est vraie par construction). + // `TilesMeshed` peut dépasser `TilesClassified` : les tuiles qui ratent cette porte sont + // maillées sans classification. / `TilesMeshed` may exceed `TilesClassified`: tiles that + // fail this gate are meshed without classification. FVoxelMeshData MeshData; if (!bTrivialEmpty) { @@ -1517,6 +1531,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector : Mesher->GenerateMesh(OriginVoxels, Step, Cells, bWantCapture ? &Result.CaptureGrid : nullptr, BandVoxLo, BandVoxHi); + INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed); } // T1.f — build the RMC geometry buffers HERE (worker), not on the game thread. Empty/all-air diff --git a/Source/VoxelForge/Public/VoxelStats.h b/Source/VoxelForge/Public/VoxelStats.h new file mode 100644 index 0000000..bd92e5f --- /dev/null +++ b/Source/VoxelForge/Public/VoxelStats.h @@ -0,0 +1,18 @@ +// VoxelStats.h +// Per-frame runtime counters for tile classification and meshing. +// Compteurs runtime par frame pour la classification et le meshing des tuiles. + +#pragma once + +#include "Stats/Stats.h" + +DECLARE_STATS_GROUP(TEXT("VoxelForge"), STATGROUP_VoxelForge, STATCAT_Advanced); + +DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Classified"), STAT_VoxelForgeTilesClassified, STATGROUP_VoxelForge, VOXELFORGE_API); +DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Solid"), STAT_VoxelForgeTilesSkippedAllSolid, STATGROUP_VoxelForge, VOXELFORGE_API); +DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Air"), STAT_VoxelForgeTilesSkippedAllAir, 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 Air"), STAT_VoxelForgeTilesOpStackAir, 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);