diff --git a/CODEMAP.md b/CODEMAP.md index 7e87873..d0e04bb 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, operator-stack verdicts, and cave-bail diagnosis. | +| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. The former ambiguous `Cave Bail Not Op Stack` is split into `Sole Slot`, `Boundary Tile`, `No Layout`, and late `Recheck` counters, so each increment names one guard/context. | ### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it) | Symbol | Line | Notes | @@ -268,7 +268,7 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h). | `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. | | `ComputeSurfaceTerrainZ` / `GetSurfaceDensity` | — | SurfaceWorld heightfield → terrain Z, then density; biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/sheets/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in `SurfaceDensityFromColumn`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. §8.14. | | `VF_BuildOpStackForChunk` (file-static) | — | **The archetype → stack mapping, written down once.** `GetDensityAt` and `ClassifyTile` both call it; params are passed in, never fetched here. A second copy would be the worst bug available in this file — a tile skipped on the verdict of a stack that is not the one producing its density is a hole. Returns false (⇒ caller falls back to the `switch`) for an unported archetype, missing params, or a **degenerate strate**, since five archetype functions early-out to air there and the stack deliberately has no such early-out. `Refs.Surface == nullptr` makes it refuse SurfaceWorld, which is how `ClassifyTile` keeps its own exact-lattice proof. | -| `ClassifyTile` | — | **T1.d trivial-tile reject** (worker, called by `LoadTile` before `GenerateMesh`): proves a tile AllSolid/AllAir on the mesher's exact lattice (gap chunks + SurfaceWorld columns via the SHARED `GSurfColCache`; seal bands; **cave archetypes via `FVoxelOpStack::ClassifyBox` when the strate opted in** — see §3.2d for the six guards, all failing to `Mixed`; guards: diff mods, passages, spine, disturbances, **F20 overhang** — a column point in `(TerrainZ, TerrainZ+OverhangMargin]` (margin = max `OverhangHeight`) is unprovable ⇒ Mixed, UPWARD only since the shelf union only ADDS rock above ground, so an overhang shelf never holes a trivially-skipped tile) → skip gen. Mixed = generate normally. §8.10. | +| `ClassifyTile` | — | **T1.d trivial-tile reject** (worker, called by `LoadTile` before `GenerateMesh`): proves a tile AllSolid/AllAir on the mesher's exact lattice (gap chunks + SurfaceWorld columns via the SHARED `GSurfColCache`; seal bands; **cave archetypes via `FVoxelOpStack::ClassifyBox` when the strate opted in** — see §3.2d for the six guards, all failing to `Mixed`; guards: diff mods, passages, spine, disturbances, **F20 overhang** — a column point in `(TerrainZ, TerrainZ+OverhangMargin]` (margin = max `OverhangHeight`) is unprovable ⇒ Mixed, UPWARD only since the shelf union only ADDS rock above ground, so an overhang shelf never holes a trivially-skipped tile) → skip gen. Mixed = generate normally. Its diagnostic-only not-op-stack bail attribution distinguishes a tile wholly inside the disabled slot, a boundary tile, and an unresolved layout; the classifier's conditions/returns are unchanged. §8.10. | | `SampleRelief` / `SampleMoisture` | — | Climate fields (pure XY, [0,1]). Relief = shared source of truth for the relief map M. §8.14. | | `SampleBiomeAt` | — | Warped-Voronoi + climate biome query (dominant + neighbour + weight). Reference used by the preview bake + `GetDominantBiomeAt`. §8.14. | | `ResolveBiomeSampleAt` / `RebuildBiomeGrid` | — | Hot-path biome resolve (FBiomeSample) via a box-validated per-chunk cell-grid cache. Bit-identical to `SampleBiomeAt`. §8.14, §8.10. | diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 1c9752b..39c97f8 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -2809,7 +2809,33 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in // même endroit, que `GetDensityAt`. if (!StrateManager->UsesOperatorStackForChunk(CC)) { - INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStack); + // Attribution DIAGNOSTIQUE uniquement : l'ancien compteur mélangeait une + // strate cave entièrement désactivée avec une tuile de frontière qui avait + // rencontré un slot désactivé avant la garde « slot différent » ci-dessous. + // On résout les bornes APRÈS l'échec du même prédicat ; elles ne changent ni + // la condition, ni le point de retour, ni le verdict. + // Diagnostic attribution only: the old counter mixed a wholly disabled cave + // slot with a boundary tile that met a disabled slot before the different-slot + // guard below. Resolve bounds only after the same predicate fails; classification + // control flow and return value stay unchanged. + int32 FailedTopCZ = 0, FailedBotCZ = 0; + if (!StrateManager->GetStrateChunkZBounds(ChunkZ, FailedTopCZ, FailedBotCZ)) + { + INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout); + } + else + { + const int32 TileMinCZ = FloorDivC(MinZ, CHUNK_SIZE); + const int32 TileMaxCZ = FloorDivC(MaxZ, CHUNK_SIZE); + if (TileMinCZ >= FailedBotCZ && TileMaxCZ <= FailedTopCZ) + { + INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot); + } + else + { + INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile); + } + } return EVoxelTileClass::Mixed; } @@ -2929,7 +2955,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in // déclenché la tentative : un seul chunk hors pile invaliderait le verdict. if (!StrateManager->UsesOperatorStackForChunk(CC)) { - INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStack); + // Le passage Z précédent a déjà accepté l'unique slot cave. Avec le layout actuel + // (prédicat indépendant de X/Y), ce recheck est redondant ; un hit nomme donc + // précisément cette garde tardive au lieu d'être agrégé aux opt-ins désactivés. + // The prior Z pass already accepted the sole cave slot. With the current X/Y- + // independent predicate this recheck is redundant, so attribute it separately. + INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck); return EVoxelTileClass::Mixed; } diff --git a/Source/VoxelForge/Private/VoxelStats.cpp b/Source/VoxelForge/Private/VoxelStats.cpp index 2a08970..1a37a17 100644 --- a/Source/VoxelForge/Private/VoxelStats.cpp +++ b/Source/VoxelForge/Private/VoxelStats.cpp @@ -10,7 +10,10 @@ DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir); DEFINE_STAT(STAT_VoxelForgeTilesMeshed); DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid); DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir); -DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStack); +DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot); +DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile); +DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout); +DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck); DEFINE_STAT(STAT_VoxelForgeCaveBailMixedContent); DEFINE_STAT(STAT_VoxelForgeCaveBailParams); DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict); diff --git a/Source/VoxelForge/Public/VoxelStats.h b/Source/VoxelForge/Public/VoxelStats.h index 5c1720b..171656e 100644 --- a/Source/VoxelForge/Public/VoxelStats.h +++ b/Source/VoxelForge/Public/VoxelStats.h @@ -14,7 +14,10 @@ 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 Not Op Stack Sole Slot"), STAT_VoxelForgeCaveBailNotOpStackSoleSlot, 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 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 Bail Params"), STAT_VoxelForgeCaveBailParams, STATGROUP_VoxelForge, VOXELFORGE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Stack Verdict"), STAT_VoxelForgeCaveBailStackVerdict, STATGROUP_VoxelForge, VOXELFORGE_API);