Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e7ea7038a | |||
| f90c4e56c3 | |||
| 1e02c6314f | |||
| 8295f6e76b |
+3
-1
@@ -306,7 +306,9 @@ driven by `EditorBrush*` props.
|
|||||||
- **SDF cache** (`GetDensityWithParams`): search-BOX validity, not chunk-key — gradient ±1
|
- **SDF cache** (`GetDensityWithParams`): search-BOX validity, not chunk-key — gradient ±1
|
||||||
sampling must not thrash the (expensive) rebuild.
|
sampling must not thrash the (expensive) rebuild.
|
||||||
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
|
- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached
|
||||||
thread-locally per chunk; don't move the fetch/blend back to per-voxel.
|
thread-locally by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; the process-unique owner ID
|
||||||
|
prevents cross-world reuse while adding only one `uint64` compare per voxel. Don't remove the owner
|
||||||
|
or layout key, and don't move the fetch/blend back to per-voxel.
|
||||||
- **Biome cache** (`ResolveBiomeSampleAt`/`FChunkBiomeCache`, §8.14): validity is a world-XY BOX +
|
- **Biome cache** (`ResolveBiomeSampleAt`/`FChunkBiomeCache`, §8.14): validity is a world-XY BOX +
|
||||||
ChunkZ + Seed, NOT a chunk key — same reason as the SDF cache. The cell classification is
|
ChunkZ + Seed, NOT a chunk key — same reason as the SDF cache. The cell classification is
|
||||||
noise-heavy; a chunk-key would thrash it on gradient-normal / +X/+Y boundary samples. Keep
|
noise-heavy; a chunk-key would thrash it on gradient-normal / +X/+Y boundary samples. Keep
|
||||||
|
|||||||
+9
-7
@@ -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, 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)
|
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
|
||||||
| Symbol | Line | Notes |
|
| Symbol | Line | Notes |
|
||||||
@@ -152,7 +152,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
|||||||
| `VoxelDensityOps::MakeSlabVoidSource` | 1 | Floor surface + ceiling surface → void field. **XY-pure** since §3.1, which is what gives it an **exact `ClassifyBox` with no sampling**: FBM's `[-1,1]` contract bounds both surfaces into known Z bands. Serves FlatPlain **and** CrystalChamber. |
|
| `VoxelDensityOps::MakeSlabVoidSource` | 1 | Floor surface + ceiling surface → void field. **XY-pure** since §3.1, which is what gives it an **exact `ClassifyBox` with no sampling**: FBM's `[-1,1]` contract bounds both surfaces into known Z bands. Serves FlatPlain **and** CrystalChamber. |
|
||||||
| `VoxelDensityOps::MakeGridColumnMod` | 3 | Infinite-height cylinders on a world grid, 3×3 cell memo. Adds solid only ⇒ `FillOnly` when a column reaches the box, `Identity` otherwise — and that `Identity` is what lets the source's `AllAir` verdict survive. |
|
| `VoxelDensityOps::MakeGridColumnMod` | 3 | Infinite-height cylinders on a world grid, 3×3 cell memo. Adds solid only ⇒ `FillOnly` when a column reaches the box, `Identity` otherwise — and that `Identity` is what lets the source's `AllAir` verdict survive. |
|
||||||
| `VoxelDensityOps::BuildSlabStack` | — | 5 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults, exactly as `GetSlabDensity` already had it. 8 archetypes → 7. |
|
| `VoxelDensityOps::BuildSlabStack` | — | 5 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults, exactly as `GetSlabDensity` already had it. 8 archetypes → 7. |
|
||||||
| `FSurfaceColumnSource` (internal) | 1 | The bridge between the two spaces: consumes the ground + sky-cap **height** stacks and produces density. `IsXYPure()` **false** — the heights are XY-pure, a distance to them never is. Owns the per-column memo, keyed by `PrepareChunk` on `(StrateBottomWorldZ, LayoutVersion, Seed)` so it is **shared down the whole vertical strate stack**, exactly like `GSurfColCache`. |
|
| `FSurfaceColumnSource` (internal) | 1 | The bridge between the two spaces: consumes the ground + sky-cap **height** stacks and produces density. `IsXYPure()` **false** — the heights are XY-pure, a distance to them never is. Owns a **six-box spatial LRU** of direct-indexed per-column cells, keyed by `PrepareChunk` on `(StrateBottomWorldZ, LayoutVersion, Seed, ParamsFingerprint)` so it is **shared down the whole vertical strate stack**, exactly like `GSurfColCache`. Six 81×81 boxes preserve hot columns across interleaved regions at roughly 0.79 MiB TLS before padding (more memory, fewer whole-cache recenter/recompute misses). Fractional XY remains direct-compute. |
|
||||||
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 3 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
|
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 3 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
|
||||||
| `VoxelDensityOps::BuildVerticalShaftStack` | — | 8 ops, and **three are Maze's reused unchanged** (`ConstantRock`, `SdfRoughness`, `SdfCarve`) with different tuning (freq 0.1 vs 0.12, window `rough+4` vs `R+rough+2`). The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. |
|
| `VoxelDensityOps::BuildVerticalShaftStack` | — | 8 ops, and **three are Maze's reused unchanged** (`ConstantRock`, `SdfRoughness`, `SdfCarve`) with different tuning (freq 0.1 vs 0.12, window `rough+4` vs `R+rough+2`). The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. |
|
||||||
| `FRoomGraphSource` (internal) | 1 | TunnelNetwork's SDF spine. **CALLS `BuildChunkCache`/`EvaluateSDFCached` — does not transcribe them**: that is where §8.4's two-region discipline lives and a copy would fork it. Owns the cave warp (scope = this op alone; pits/chimneys read *unwarped* coords, which is why no FRAME op was needed). Its cache key adds a **params CRC + LayoutVersion**; the original lacked them until AUDIT §C2 was fixed (2026-07-28) and now carries them too. **`EffectOverBox` ANSWERS SPATIALLY** since 2026-07-28 — this is the T1.d switch (measured: **6 of 40 tiles proved AllSolid at production defaults, 7986 voxels brute-forced, 0 violations**). It builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), then applies a **disjunction** per primitive: it doesn't matter if it **fails its cull** *or* if **its own SDF stays ≥ `T+K`** over the box. Cull wins for rooms (`Rmax+3K` < `Rmax+T+K`); the threshold wins hugely for tunnels, whose cull is a capsule *bounding sphere* (~107 radius for a 200-long tube of radius 7). ⚠️ **`Identity` therefore means `Sdf ≥ T`, not `Sdf == FLT_MAX`**, with `T = max(3·SDFBlendRadius, WormNetworkRange)` — **any new consumer of the `Sdf` channel must have a threshold ≤ T or be added to that max**, or it gets tiles with no geometry and no collision. The `−K` slack covers any number of primitives because `SmoothMin`'s penalty is exactly 0 once `\|A−B\| ≥ K`. Pits/chimneys use the cull only; columns are **not** tested (their sole consumer gates on `Sdf`, so the test was redundant). Verdict memoised per box; warp dilation uses a **provable** `\|Perlin3D\| ≤ 2`. |
|
| `FRoomGraphSource` (internal) | 1 | TunnelNetwork's SDF spine. **CALLS `BuildChunkCache`/`EvaluateSDFCached` — does not transcribe them**: that is where §8.4's two-region discipline lives and a copy would fork it. Owns the cave warp (scope = this op alone; pits/chimneys read *unwarped* coords, which is why no FRAME op was needed). Its cache key adds a **params CRC + LayoutVersion**; the original lacked them until AUDIT §C2 was fixed (2026-07-28) and now carries them too. **`EffectOverBox` ANSWERS SPATIALLY** since 2026-07-28 — this is the T1.d switch (measured: **6 of 40 tiles proved AllSolid at production defaults, 7986 voxels brute-forced, 0 violations**). It builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), then applies a **disjunction** per primitive: it doesn't matter if it **fails its cull** *or* if **its own SDF stays ≥ `T+K`** over the box. Cull wins for rooms (`Rmax+3K` < `Rmax+T+K`); the threshold wins hugely for tunnels, whose cull is a capsule *bounding sphere* (~107 radius for a 200-long tube of radius 7). ⚠️ **`Identity` therefore means `Sdf ≥ T`, not `Sdf == FLT_MAX`**, with `T = max(3·SDFBlendRadius, WormNetworkRange)` — **any new consumer of the `Sdf` channel must have a threshold ≤ T or be added to that max**, or it gets tiles with no geometry and no collision. The `−K` slack covers any number of primitives because `SmoothMin`'s penalty is exactly 0 once `\|A−B\| ≥ K`. Pits/chimneys use the cull only; columns are **not** tested (their sole consumer gates on `Sdf`, so the test was redundant). Verdict memoised per box; warp dilation uses a **provable** `\|Perlin3D\| ≤ 2`. |
|
||||||
@@ -251,24 +251,26 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
|||||||
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
|
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
|
||||||
|
|
||||||
### 3.6 Density generator — `Public/VoxelGenerator.h` + `Private/VoxelGenerator.cpp`
|
### 3.6 Density generator — `Public/VoxelGenerator.h` + `Private/VoxelGenerator.cpp`
|
||||||
`UVoxelGenerator : UObject` — lightweight; holds `Seed`, and injected services
|
`UVoxelGenerator : UObject` — lightweight; holds `Seed`, a process-unique
|
||||||
`StrateManager` + `DiffLayer` (both nullable). This is **where terrain shape lives.**
|
`DensityCacheOwnerId`, and injected services `StrateManager` + `DiffLayer` (both nullable).
|
||||||
|
This is **where terrain shape lives.**
|
||||||
|
|
||||||
| Symbol | .cpp line | Role |
|
| Symbol | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
|
| `UVoxelGenerator` / `DensityCacheOwnerId` | — | Constructor allocates a process-unique integer identity (relaxed atomic, once per object). `GetDensityAt` includes it in the `CP_*` thread-local key, preventing a worker from serving another generator/world's params, biome context, `CP_UseOpStack`, or stack when `(ChunkCoord, LayoutVersion)` happens to match. Hot-path cost: one `uint64` compare per voxel. Scope is deliberately only the proved `CP_*` path. |
|
||||||
| `FractalNoise3D` (static) | 25 | fBM (layered Perlin). |
|
| `FractalNoise3D` (static) | 25 | fBM (layered Perlin). |
|
||||||
| `RidgedNoise3D` (static) | 55 | Ridged multifractal — craggy. |
|
| `RidgedNoise3D` (static) | 55 | Ridged multifractal — craggy. |
|
||||||
| `CellularNoise3D` (static) | 101 | Worley/cellular — grotto/scallop. |
|
| `CellularNoise3D` (static) | 101 | Worley/cellular — grotto/scallop. |
|
||||||
| `ApplyBoundarySeal` (static) | 170 | Solidifies strate top/bottom shells. |
|
| `ApplyBoundarySeal` (static) | 170 | Solidifies strate top/bottom shells. |
|
||||||
| `ApplyPassageCarving` (static) | 197 | Punches passages/elevator through the seal. |
|
| `ApplyPassageCarving` (static) | 197 | Punches passages/elevator through the seal. |
|
||||||
| `InitializeSettings` | 211 | Copies seed from settings. |
|
| `InitializeSettings` | 211 | Copies seed from settings. |
|
||||||
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, dispatches, adds diff offset. |
|
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, dispatches, adds diff offset. Its `CP_*` per-chunk state is keyed by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; every key component is an integer compare and a different generator/world cannot inherit the previous owner's cached params or op stack. |
|
||||||
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. |
|
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. |
|
||||||
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
|
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
|
||||||
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `ResolveBiomeSampleAt` / `RebuildBiomeGrid` | — | Hot-path biome resolve (FBiomeSample) via a box-validated per-chunk cell-grid cache. Bit-identical to `SampleBiomeAt`. §8.14, §8.10. |
|
||||||
@@ -338,7 +340,7 @@ Maps depth→strate at runtime; owns passages.
|
|||||||
- `FStrateSlot` (h:84): definition + chunk-Z range + index.
|
- `FStrateSlot` (h:84): definition + chunk-Z range + index.
|
||||||
| Method | .cpp line | Role |
|
| Method | .cpp line | Role |
|
||||||
|--------|-----------|------|
|
|--------|-----------|------|
|
||||||
| `Initialize` | 10 | Builds the stacked layout from settings+seed (fixed slots + shuffled pool), then `GeneratePassages`. |
|
| `Initialize` | 10 | Builds the stacked layout from settings+seed (fixed slots + shuffled pool), logs every **cave** slot whose operator-stack opt-in is disabled, then `GeneratePassages`. SurfaceWorld is deliberately excluded from that diagnostic because its exact-lattice T1.d path does not depend on the flag. |
|
||||||
| `GeneratePassages` | 146 | Deterministic passages between consecutive strates (per-type control points). |
|
| `GeneratePassages` | 146 | Deterministic passages between consecutive strates (per-type control points). |
|
||||||
| `EvaluateModifierSDF` | 357 | SDF of passages at a point (for carving). Per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`. §8.10. |
|
| `EvaluateModifierSDF` | 357 | SDF of passages at a point (for carving). Per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`. §8.10. |
|
||||||
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
| `AnyPassageNearBox` | — | Conservative sphere-vs-AABB test of every passage's bound against a voxel box (+carve blend pad). Per TILE (ClassifyTile guard), never per voxel. |
|
||||||
|
|||||||
@@ -4134,3 +4134,83 @@ or reword that first.
|
|||||||
|
|
||||||
**Neither approach solves VF-03's owner identity, boundary params conservatism, or the memo's
|
**Neither approach solves VF-03's owner identity, boundary params conservatism, or the memo's
|
||||||
unproven benefit.** Stated plainly by Sol rather than glossed.
|
unproven benefit.** Stated plainly by Sol rather than glossed.
|
||||||
|
|
||||||
|
## 2026-08-17 (o) — Approach A + unambiguous bails + CP owner key LANDED on `experimental`
|
||||||
|
|
||||||
|
Jahni explicitly authorised writes, commits, and a push to the real `experimental` tree. The three
|
||||||
|
code changes below were landed there as separate commits. **Nothing in this entry was built,
|
||||||
|
compiled, run in the editor, automation-tested, or measured.** All checks were static source/diff
|
||||||
|
inspection plus `git diff --check`. The tree is ready for Jahni's build, not claimed green.
|
||||||
|
|
||||||
|
### `8295f6e` — six-box surface-column LRU + cave-only opt-in diagnostic
|
||||||
|
|
||||||
|
- Landed Approach A's six 81×81 direct-indexed boxes in
|
||||||
|
`FSurfaceColumnSource::GetColumn`. An acquisition miss recentres and clears one LRU victim; the
|
||||||
|
other five working sets stay warm. Exact `uint64 ColumnKey`, exact XY coverage, per-cell computed
|
||||||
|
flags, and fractional-XY direct computation are retained.
|
||||||
|
- Deliberately kept the recommended six boxes despite the estimated **~0.79 MiB TLS per worker**
|
||||||
|
versus ~0.13 MiB for one box. There is no same-route measurement supporting an arbitrary smaller
|
||||||
|
count; silently choosing one would trade unknown miss behaviour for memory without evidence.
|
||||||
|
- Applied the report's caveat before landing: `UVoxelStrateManager::Initialize` counts and lists
|
||||||
|
disabled **cave** slots only. `SurfaceWorld` is explicitly excluded because its exact-lattice
|
||||||
|
T1.d proof does not depend on `bUseOperatorStack`. The flag remains a real per-asset A/B switch;
|
||||||
|
no forced cutover from Approach B was taken.
|
||||||
|
- Likely compile-error/watch spots for this commit: MSVC/UE function-local TLS for the large nested
|
||||||
|
`FColumnCache`; aggregate initialization of six `FColumnBox` values; range-for/member lookup in
|
||||||
|
the local `Acquire`; `FMemory::Memzero` on the selected `Computed` array; the new `UE_LOG` format
|
||||||
|
arguments and direct `Slot.Definition->GeneratorType` access.
|
||||||
|
|
||||||
|
### `1e02c63` — the old `Cave Bail Not Op Stack` no longer means two things
|
||||||
|
|
||||||
|
- Replaced the ambiguous counter with four named sites: `Sole Slot` means the complete sampled tile
|
||||||
|
Z range is proved inside the disabled slot; `Boundary Tile` means that range crosses its bounds;
|
||||||
|
`No Layout` means the failed chunk has no resolvable slot; `Recheck` means the later exhaustive
|
||||||
|
XYZ guard failed after the Z pass had already accepted the cave slot.
|
||||||
|
- This is instrumentation only. The existing `UsesOperatorStackForChunk` predicate is still tested
|
||||||
|
at the same point, and every original `ClassifyTile` return point and return value is unchanged.
|
||||||
|
Slot bounds are queried only after that predicate has already failed, solely to choose a counter.
|
||||||
|
- Likely compile-error/watch spots for this commit: declaration/definition spelling for all four UE
|
||||||
|
stats; the long stat display names; `FloorDivC` visibility inside the diagnostic branch; and the
|
||||||
|
`GetStrateChunkZBounds(ChunkZ, Top, Bottom)` argument order. A source diff confirms no terrain
|
||||||
|
predicate or `EVoxelTileClass` return changed, but only a build can validate the stat macros.
|
||||||
|
|
||||||
|
### `f90c4e5` — VF-03's proved `CP_*` cache now has an owner identity
|
||||||
|
|
||||||
|
- Each `UVoxelGenerator` receives a monotonic process-unique `DensityCacheOwnerId` from a relaxed
|
||||||
|
atomic at construction. `GetDensityAt` now keys its function-static `thread_local CP_*` state by
|
||||||
|
`(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`. A world/generator change therefore refetches
|
||||||
|
params, generator type, disturbances, biome context, `CP_UseOpStack`, and the built op stack, and
|
||||||
|
invalidates `CP_BiomeCache` even when chunk/version values happen to match.
|
||||||
|
- Hot-path cost is one additional `uint64` comparison per voxel; atomic work happens only once per
|
||||||
|
generator construction. A monotonic ID was chosen instead of a raw owner pointer so a later
|
||||||
|
UObject address reuse cannot resurrect stale TLS state.
|
||||||
|
- Scope was deliberately restricted to the proved `CP_*` path. No claim was made and no key was
|
||||||
|
added to `OC_*`, `BM_*`, passage, biome, diff, or op-local caches. The test fixture's process-unique
|
||||||
|
layout-version bumps remain as conservative isolation for those unaudited caches, with its stale
|
||||||
|
CP-specific explanation corrected.
|
||||||
|
- Likely compile-error/watch spots for this commit: UHT/generated-constructor compatibility with the
|
||||||
|
explicit `UVoxelGenerator()` declaration; MSVC/UE support for `<atomic>`, `std::atomic<uint64>`,
|
||||||
|
`fetch_add`, and `std::memory_order_relaxed`; and initialization/access of the new private,
|
||||||
|
non-UPROPERTY `DensityCacheOwnerId` from the `.cpp` constructor and const hot path.
|
||||||
|
|
||||||
|
### Deliberately still open
|
||||||
|
|
||||||
|
- Read the actual `.uasset` opt-in state in the editor and fly the same underground route. The new
|
||||||
|
`Sole Slot` versus `Boundary Tile` counters make that result interpretable; source cannot answer
|
||||||
|
asset state.
|
||||||
|
- Measure the six-box memo on the same seed/route/settings/warm-up and record both miss rate per tile
|
||||||
|
and process memory at the real worker count. The verified structural improvement is **not** yet a
|
||||||
|
measured performance win.
|
||||||
|
- Audit owner identity for the other TLS caches separately. This change does not promote VF-03's
|
||||||
|
unproved breadth into fact.
|
||||||
|
- Boundary slot/params conservatism and every `ClassifyTile` safety guard remain unchanged.
|
||||||
|
|
||||||
|
### Candidate worktree cleanup
|
||||||
|
|
||||||
|
The dirty candidate diffs were preserved as named, recoverable stashes before cleanup:
|
||||||
|
|
||||||
|
- `c5e067c3` (currently `stash@{1}`) — `archive VF approach A before worktree cleanup 2026-08-17`
|
||||||
|
- `3be0b558` (currently `stash@{0}`) — `archive VF approach B before worktree cleanup 2026-08-17`
|
||||||
|
|
||||||
|
`../VF-approach-A` and `../VF-approach-B` were then removed from Git's worktree list and their
|
||||||
|
directories removed. Stashing was a cleanup-safety deviation only; it did not alter the landed code.
|
||||||
|
|||||||
@@ -129,22 +129,20 @@ namespace VoxelForgeTest
|
|||||||
// elle n'était jusqu'ici masquée que par un accident.
|
// elle n'était jusqu'ici masquée que par un accident.
|
||||||
//
|
//
|
||||||
// `PassagesVersion` est PAR INSTANCE et part de 0, donc deux `FTestWorld` successifs
|
// `PassagesVersion` est PAR INSTANCE et part de 0, donc deux `FTestWorld` successifs
|
||||||
// rendaient tous les deux **1**. Or les caches par chunk de `GetDensityAt` sont clés sur
|
// rendaient tous les deux **1**. Historiquement, les caches `CP_*` de `GetDensityAt`
|
||||||
// `(ChunkCoord, LayoutVersion)` : deux mondes différents, même version, même chunk ⇒ le
|
// n'avaient que `(ChunkCoord, LayoutVersion)` et le second monde pouvait hériter les
|
||||||
// second se voit servir les params — ET le drapeau `CP_UseOpStack` — du premier.
|
// params — ET `CP_UseOpStack` — du premier. `DensityCacheOwnerId` ferme maintenant CE
|
||||||
// Personne ne l'a vu parce que `bUseOperatorStack` valait false partout : les deux
|
// chemin prouvé. Les bumps restent ici comme isolation conservatrice des autres caches
|
||||||
// mondes étaient d'accord par défaut. Le premier monde qui coche la case fait tomber
|
// TLS que cette correction n'a volontairement pas audités ni modifiés.
|
||||||
// cette coïncidence, dans les DEUX sens (il contamine, et il est contaminé).
|
|
||||||
//
|
//
|
||||||
// Un compteur de processus donne à chaque monde une version distincte, donc tout cache
|
// Un compteur de processus donne à chaque monde une version distincte, donc tout cache
|
||||||
// survivant d'un test à l'autre est forcément invalidé. `Initialize` est déterministe
|
// survivant d'un test à l'autre est forcément invalidé. `Initialize` est déterministe
|
||||||
// (le pool est mélangé par le seed, les fixed strates sont épinglées), donc le rappeler
|
// (le pool est mélangé par le seed, les fixed strates sont épinglées), donc le rappeler
|
||||||
// ne change pas le layout — seulement le compteur.
|
// ne change pas le layout — seulement le compteur.
|
||||||
//
|
//
|
||||||
// Each test world gets a process-unique LayoutVersion. Two worlds both reporting 1 made
|
// Each test world still gets a process-unique LayoutVersion. DensityCacheOwnerId now
|
||||||
// GetDensityAt's per-chunk caches serve the previous world's params — and its
|
// prevents the proved CP_* cross-world reuse directly; the version bumps remain as
|
||||||
// CP_UseOpStack flag — for the same chunk coord. Invisible while every world agreed that
|
// conservative isolation for other TLS caches not audited or changed by that fix.
|
||||||
// the flag was false.
|
|
||||||
static int32 GWorldSerial = 0;
|
static int32 GWorldSerial = 0;
|
||||||
const int32 Bumps = ++GWorldSerial;
|
const int32 Bumps = ++GWorldSerial;
|
||||||
for (int32 b = 0; b < Bumps; ++b)
|
for (int32 b = 0; b < Bumps; ++b)
|
||||||
|
|||||||
@@ -661,30 +661,77 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** La colonne complète, exactement les cinq sorties de `ComputeSurfaceColumn`.
|
/** La colonne complète, exactement les cinq sorties de `ComputeSurfaceColumn`.
|
||||||
* Le mémo est une boîte à index direct, comme `FSurfaceColumnBox` : la pile évalue tous
|
* Le mémo est un LRU spatial de six boîtes à index direct, comme `GSurfColCache` : la
|
||||||
* les Z d'une colonne au même XY, donc l'overhang lit la MÊME colonne que la source, par
|
* pile évalue tous les Z d'une colonne au même XY, donc l'overhang lit la MÊME colonne
|
||||||
* construction plutôt que par convention. */
|
* que la source, par construction plutôt que par convention.
|
||||||
|
*
|
||||||
|
* The memo is a six-box spatial LRU with direct XY indexing, matching `GSurfColCache`.
|
||||||
|
* Six boxes retain interleaved strate regions at the cost of roughly 0.79 MiB of TLS for
|
||||||
|
* the five-float column payload plus one computed flag per cell, before compiler padding. */
|
||||||
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
|
||||||
{
|
{
|
||||||
// Même schéma éprouvé que `GSurfColCache` : index direct dans une boîte XY, puis un
|
// Même schéma éprouvé que `GSurfColCache` : six boîtes à index direct dans XY, chacune
|
||||||
// drapeau `Computed` par cellule. Une tuile MC pleine résolution demande
|
// avec un drapeau `Computed` par cellule et une clé uint64 exacte. Une tuile MC pleine
|
||||||
// (CHUNK_SIZE + 3)² = 35×35 = 1225 colonnes (anneau de marge inclus) ; cette boîte
|
// résolution demande 35×35 = 1225 colonnes (anneau de marge inclus) ; une boîte de
|
||||||
// de Dim×Dim, recentrée sur le premier échantillon, les garde toutes sans collision.
|
// Dim×Dim, recentrée sur le premier échantillon, les garde toutes sans éviction.
|
||||||
// Same proven scheme as `GSurfColCache`: direct XY indexing plus one `Computed` flag per
|
// Same proven scheme as `GSurfColCache`: six direct-indexed XY boxes, each with one
|
||||||
// cell. A full-resolution MC tile needs 35×35 = 1225 columns including its margin ring;
|
// `Computed` flag per cell and an exact uint64 key. A full-resolution MC tile needs
|
||||||
// the box is sized so one tile fits without eviction.
|
// 35×35 = 1225 columns including its margin ring; one Dim×Dim box holds that tile.
|
||||||
struct FColumnBox
|
struct FColumnBox
|
||||||
{
|
{
|
||||||
enum : int32 { Halo = CHUNK_SIZE + 8, Dim = 2 * Halo + 1 };
|
enum : int32 { Halo = CHUNK_SIZE + 8, Dim = 2 * Halo + 1 };
|
||||||
int32 BaseX = 0, BaseY = 0;
|
int32 BaseX = 0, BaseY = 0;
|
||||||
uint64 Key = 0; // strate + layout + seed + ParamsFingerprint
|
uint64 Key = 0; // strate + layout + seed + ParamsFingerprint
|
||||||
|
uint32 LastUse = 0; // LRU stamp
|
||||||
bool bValid = false;
|
bool bValid = false;
|
||||||
FColumn Cols[Dim * Dim];
|
FColumn Cols[Dim * Dim];
|
||||||
bool Computed[Dim * Dim];
|
bool Computed[Dim * Dim];
|
||||||
};
|
};
|
||||||
thread_local FColumnBox Box = {};
|
|
||||||
|
struct FColumnCache
|
||||||
|
{
|
||||||
|
enum : int32 { NumBoxes = 6 };
|
||||||
|
FColumnBox Boxes[NumBoxes];
|
||||||
|
uint32 Clock = 0;
|
||||||
|
|
||||||
|
// Hit exact : clé complète + couverture XY complète. En cas de miss, seul le
|
||||||
|
// victim LRU est recentré et invalidé ; les cinq autres boîtes restent chaudes.
|
||||||
|
// Exact hit: full key + full XY coverage. On a miss, only the LRU victim is
|
||||||
|
// recentered and invalidated; the other five boxes remain warm.
|
||||||
|
FColumnBox& Acquire(int32 IX, int32 IY, uint64 InColumnKey)
|
||||||
|
{
|
||||||
|
++Clock;
|
||||||
|
for (FColumnBox& B : Boxes)
|
||||||
|
{
|
||||||
|
if (B.bValid && B.Key == InColumnKey
|
||||||
|
&& IX >= B.BaseX && IX < B.BaseX + FColumnBox::Dim
|
||||||
|
&& IY >= B.BaseY && IY < B.BaseY + FColumnBox::Dim)
|
||||||
|
{
|
||||||
|
B.LastUse = Clock;
|
||||||
|
return B;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miss d'acquisition : évincer/recentrer une seule boîte, jamais tout le cache.
|
||||||
|
// Acquisition miss: evict/recenter one box only, never the whole cache.
|
||||||
|
FColumnBox* Victim = &Boxes[0];
|
||||||
|
for (FColumnBox& B : Boxes)
|
||||||
|
{
|
||||||
|
if (B.LastUse < Victim->LastUse) Victim = &B;
|
||||||
|
}
|
||||||
|
Victim->BaseX = IX - FColumnBox::Halo;
|
||||||
|
Victim->BaseY = IY - FColumnBox::Halo;
|
||||||
|
Victim->Key = InColumnKey;
|
||||||
|
Victim->LastUse = Clock;
|
||||||
|
Victim->bValid = true;
|
||||||
|
FMemory::Memzero(Victim->Computed, sizeof(Victim->Computed));
|
||||||
|
return *Victim;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_local FColumnCache Cache = {};
|
||||||
thread_local FColumn DirectColumn = {};
|
thread_local FColumn DirectColumn = {};
|
||||||
|
|
||||||
// The production mesher and the exact-lattice classifier use integer XY. Fractional
|
// The production mesher and the exact-lattice classifier use integer XY. Fractional
|
||||||
@@ -701,18 +748,9 @@ namespace
|
|||||||
const int32 IX = (int32)WorldX;
|
const int32 IX = (int32)WorldX;
|
||||||
const int32 IY = (int32)WorldY;
|
const int32 IY = (int32)WorldY;
|
||||||
|
|
||||||
// Bounds are checked exactly before deriving CI; the index itself is the XY key.
|
// Acquire vérifie la clé uint64 complète et les bornes exactes avant de dériver CI.
|
||||||
// Les bornes sont vérifiées exactement avant CI : l'index EST la clé XY.
|
// Acquire checks the exact uint64 key and exact bounds before deriving CI.
|
||||||
if (!Box.bValid || Box.Key != ColumnKey
|
FColumnBox& Box = Cache.Acquire(IX, IY, 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);
|
CI = (IY - Box.BaseY) * FColumnBox::Dim + (IX - Box.BaseX);
|
||||||
MemoColumn = &Box.Cols[CI];
|
MemoColumn = &Box.Cols[CI];
|
||||||
@@ -830,11 +868,13 @@ namespace
|
|||||||
* c'est-à-dire à chaque chunk. Résultat : une strate haute de 4 chunks recalculait ses
|
* c'est-à-dire à chaque chunk. Résultat : une strate haute de 4 chunks recalculait ses
|
||||||
* colonnes **4 fois**, resamples du cliff compris. Le chemin d'origine ne fait pas ça —
|
* colonnes **4 fois**, resamples du cliff compris. Le chemin d'origine ne fait pas ça —
|
||||||
* `GSurfColCache` est clé sur `(boîte XY, StrateKey, Seed, LayoutVersion)` **SANS ChunkZ**,
|
* `GSurfColCache` est clé sur `(boîte XY, StrateKey, Seed, LayoutVersion)` **SANS ChunkZ**,
|
||||||
* délibérément, « shared down the whole vertical strate stack ».
|
* délibérément, « shared down the whole vertical strate stack ». Cette pile reprend la
|
||||||
|
* même identité de strate/layout/seed, en ajoutant l'empreinte obligatoire des params pour
|
||||||
|
* protéger ses sorties propres ; son mémo est maintenant un LRU spatial de six boîtes.
|
||||||
*
|
*
|
||||||
* Donc la clé devient la même identité : ce qui rend deux colonnes interchangeables, c'est
|
* Donc la clé garde l'identité partagée : ce qui rend deux colonnes interchangeables, c'est
|
||||||
* la STRATE et la version de layout, pas le chunk. Le mémo étant `thread_local`, il SURVIT
|
* la STRATE, le seed, la version de layout et les params, pas le chunk. Le mémo étant
|
||||||
* à la reconstruction de la pile — seule la clé l'invalidait.
|
* `thread_local`, il SURVIT à la reconstruction de la pile — seule la clé l'invalidait.
|
||||||
*
|
*
|
||||||
* POURQUOI C'EST SÛR : les hauteurs sont XY-pures par construction (c'est tout l'objet de
|
* POURQUOI C'EST SÛR : les hauteurs sont XY-pures par construction (c'est tout l'objet de
|
||||||
* `VoxelHeightOp.h`, où le type n'a pas de Z), et le champ de biomes est documenté
|
* `VoxelHeightOp.h`, où le type n'a pas de Z), et le champ de biomes est documenté
|
||||||
@@ -843,8 +883,9 @@ namespace
|
|||||||
*
|
*
|
||||||
* The memo was keyed on InstanceId, which changes every chunk, so a 4-chunk strate recomputed
|
* The memo was keyed on InstanceId, which changes every chunk, so a 4-chunk strate recomputed
|
||||||
* every column 4x. GSurfColCache deliberately omits ChunkZ and shares down the whole vertical
|
* every column 4x. GSurfColCache deliberately omits ChunkZ and shares down the whole vertical
|
||||||
* stack; this now keys on the same identity. Safe because heights are XY-pure by type and the
|
* stack; this now shares the same strate/layout/seed identity and adds the required params
|
||||||
* biome field is documented Z-independent.
|
* fingerprint for its own outputs. The six-box LRU keeps independent XY regions alive. Safe
|
||||||
|
* because heights are XY-pure by type and the biome field is documented Z-independent.
|
||||||
*/
|
*/
|
||||||
void PrepareChunk(const FVoxelOpContext& Ctx) override
|
void PrepareChunk(const FVoxelOpContext& Ctx) override
|
||||||
{
|
{
|
||||||
@@ -881,14 +922,14 @@ namespace
|
|||||||
|
|
||||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||||
{
|
{
|
||||||
// ⚠️ PAS de mémo par colonne ICI, délibérément. Le cache T1.a existe déjà UN NIVEAU
|
// Le mémo par colonne vit ici, dans six boîtes thread_local partagées par les instances
|
||||||
// AU-DESSUS (`GSurfColCache` dans `GetDensityAt`), clé sur (boîte XY, StrateKey, Seed).
|
// mais séparées par la clé, et lues par les Eval de cette source et FOverhangShelfMod.
|
||||||
// En rajouter un ici demanderait une seconde clé de cache à tenir juste — et une clé de
|
// Il est séparé de `GSurfColCache` : la pile possède ses propres sorties et sa clé
|
||||||
// cache fausse dans un op partagé sur toute la pile verticale est précisément le mode de
|
// complète (strate + layout + seed + empreinte des params), donc réutiliser le cache
|
||||||
// défaillance qu'`AUDIT §6.3` décrit. Le branchement (étape 2b) réutilise le cache
|
// du générateur serait incorrect.
|
||||||
// existant plutôt que d'en inventer un second.
|
// The per-column memo lives here in six thread-local boxes shared across instances but
|
||||||
// No per-column memo here on purpose: T1.a already exists one level up, and a second
|
// separated by the key, and read by this source's Eval calls and FOverhangShelfMod.
|
||||||
// cache key is a second thing to get wrong.
|
// It is separate from `GSurfColCache`: the stack owns its own outputs and full key.
|
||||||
const FColumn& C = GetColumn(WorldX, WorldY);
|
const FColumn& C = GetColumn(WorldX, WorldY);
|
||||||
|
|
||||||
float Density = C.TerrainZ - WorldZ;
|
float Density = C.TerrainZ - WorldZ;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
#include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it
|
||||||
#include "VoxelStats.h"
|
#include "VoxelStats.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
// L'ADAPTATEUR DE CHAMP DE BIOMES / THE BIOME FIELD ADAPTER
|
// L'ADAPTATEUR DE CHAMP DE BIOMES / THE BIOME FIELD ADAPTER
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -445,6 +447,13 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z,
|
|||||||
// never fetched here — both callers already have them.
|
// never fetched here — both callers already have them.
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
// Une identité monotone évite qu'un worker réutilise les CP_* d'un monde détruit même si
|
||||||
|
// l'allocateur UObject recycle plus tard la même adresse. Relaxed suffit : on ne publie aucune
|
||||||
|
// donnée, on alloue seulement une valeur distincte par instance.
|
||||||
|
// A monotonic identity prevents stale CP_* reuse even if UObject allocation later recycles an
|
||||||
|
// address. Relaxed ordering is sufficient: this allocates uniqueness, it publishes no data.
|
||||||
|
std::atomic<uint64> GNextDensityCacheOwnerId { 0 };
|
||||||
|
|
||||||
struct FVoxelStackParamRefs
|
struct FVoxelStackParamRefs
|
||||||
{
|
{
|
||||||
const FSlabGenerationParams* Slab = nullptr;
|
const FSlabGenerationParams* Slab = nullptr;
|
||||||
@@ -542,6 +551,11 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UVoxelGenerator::UVoxelGenerator()
|
||||||
|
: DensityCacheOwnerId(GNextDensityCacheOwnerId.fetch_add(1, std::memory_order_relaxed) + 1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
||||||
{
|
{
|
||||||
// Seul le seed est copié ici. Tout le reste (params de cave, transitions,
|
// Seul le seed est copié ici. Tout le reste (params de cave, transitions,
|
||||||
@@ -579,7 +593,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// The generator type, the (boundary-blended) param struct, and the disturbance
|
// The generator type, the (boundary-blended) param struct, and the disturbance
|
||||||
// params are identical for the whole chunk, yet resolving them re-runs a strate
|
// params are identical for the whole chunk, yet resolving them re-runs a strate
|
||||||
// lookup + copies large structs (and a ~60-field Lerp for blended cave chunks).
|
// lookup + copies large structs (and a ~60-field Lerp for blended cave chunks).
|
||||||
// Cache them thread-locally, keyed by chunk coord — refetch only on chunk change.
|
// Cache them thread-locally, keyed by owner + chunk coord + layout version — refetch only
|
||||||
|
// when one of those integer identities changes.
|
||||||
|
thread_local uint64 CP_OwnerId = 0;
|
||||||
thread_local FIntVector CP_Chunk(INT32_MAX, INT32_MAX, INT32_MAX);
|
thread_local FIntVector CP_Chunk(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
thread_local ECaveGeneratorType CP_GenType = ECaveGeneratorType::TunnelNetwork;
|
thread_local ECaveGeneratorType CP_GenType = ECaveGeneratorType::TunnelNetwork;
|
||||||
thread_local FStrateGenerationParams CP_Tunnel;
|
thread_local FStrateGenerationParams CP_Tunnel;
|
||||||
@@ -613,18 +629,23 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
|||||||
// "I tweaked the strate asset, regenerated, and one patch kept the old shape."
|
// "I tweaked the strate asset, regenerated, and one patch kept the old shape."
|
||||||
thread_local uint32 CP_Version = 0xFFFFFFFFu;
|
thread_local uint32 CP_Version = 0xFFFFFFFFu;
|
||||||
// OPSTACK Phase 1 — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les
|
// OPSTACK Phase 1 — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les
|
||||||
// params (donc même clé chunk+version, aucune logique d'invalidation en plus). Vide tant que
|
// params (donc même clé owner+chunk+version, aucune logique d'invalidation en plus). Vide tant que
|
||||||
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
// la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté.
|
||||||
thread_local FVoxelOpStack CP_OpStack;
|
thread_local FVoxelOpStack CP_OpStack;
|
||||||
thread_local bool CP_UseOpStack = false;
|
thread_local bool CP_UseOpStack = false;
|
||||||
|
|
||||||
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
||||||
if (ChunkCoord != CP_Chunk || LayoutVersion != CP_Version)
|
const bool bOwnerChanged = DensityCacheOwnerId != CP_OwnerId;
|
||||||
|
if (bOwnerChanged || ChunkCoord != CP_Chunk || LayoutVersion != CP_Version)
|
||||||
{
|
{
|
||||||
// La grille de biome est validée par une BOÎTE XY, qui ne dit rien du FBiomeContext
|
// La grille de biome est validée par une BOÎTE XY, qui ne dit rien du FBiomeContext
|
||||||
// ayant servi à classer ses cellules : sur un changement de version elle est périmée
|
// ayant servi à classer ses cellules : sur un changement de version elle est périmée
|
||||||
// même si la boîte couvre encore la requête.
|
// même si la boîte couvre encore la requête. Même invalidation quand le propriétaire
|
||||||
if (LayoutVersion != CP_Version) { CP_BiomeCache.Invalidate(); }
|
// change : deux mondes peuvent partager version et coordonnées, jamais leur contexte.
|
||||||
|
// The biome grid's XY box says nothing about its context. Owner changes invalidate it
|
||||||
|
// too: two worlds may share version and coordinates, never cached params/context.
|
||||||
|
if (bOwnerChanged || LayoutVersion != CP_Version) { CP_BiomeCache.Invalidate(); }
|
||||||
|
CP_OwnerId = DensityCacheOwnerId;
|
||||||
CP_Version = LayoutVersion;
|
CP_Version = LayoutVersion;
|
||||||
CP_Chunk = ChunkCoord;
|
CP_Chunk = ChunkCoord;
|
||||||
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
||||||
@@ -2809,7 +2830,33 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
|||||||
// même endroit, que `GetDensityAt`.
|
// même endroit, que `GetDensityAt`.
|
||||||
if (!StrateManager->UsesOperatorStackForChunk(CC))
|
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;
|
return EVoxelTileClass::Mixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2929,7 +2976,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
|||||||
// 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))
|
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;
|
return EVoxelTileClass::Mixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ 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_VoxelForgeCaveBailNotOpStackSoleSlot);
|
||||||
|
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile);
|
||||||
|
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout);
|
||||||
|
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck);
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveBailMixedContent);
|
DEFINE_STAT(STAT_VoxelForgeCaveBailMixedContent);
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveBailParams);
|
DEFINE_STAT(STAT_VoxelForgeCaveBailParams);
|
||||||
DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict);
|
DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict);
|
||||||
|
|||||||
@@ -129,6 +129,56 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
|||||||
Slot.HeightInChunks);
|
Slot.HeightInChunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diagnostic de configuration, une seule fois par construction de layout. SurfaceWorld est
|
||||||
|
// volontairement exclu : son chemin T1.d exact-lattice ne dépend pas de ce drapeau.
|
||||||
|
// Configuration diagnostic once per layout build. SurfaceWorld is deliberately excluded:
|
||||||
|
// its exact-lattice T1.d path does not depend on this flag.
|
||||||
|
int32 NumCaveSlots = 0;
|
||||||
|
int32 NumOperatorStackDisabledCaves = 0;
|
||||||
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
|
{
|
||||||
|
if (!Slot.Definition || Slot.Definition->GeneratorType == ECaveGeneratorType::SurfaceWorld)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++NumCaveSlots;
|
||||||
|
if (!Slot.Definition->bUseOperatorStack)
|
||||||
|
{
|
||||||
|
++NumOperatorStackDisabledCaves;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NumOperatorStackDisabledCaves > 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[StrateManager] Operator-stack opt-in: %d/%d cave layout slots have Use Operator Stack disabled. These slots cannot use operator-stack ClassifyBox/T1.d; enable the asset setting on the listed definitions if that is intended."),
|
||||||
|
NumOperatorStackDisabledCaves, NumCaveSlots);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Log,
|
||||||
|
TEXT("[StrateManager] Operator-stack opt-in: all %d cave layout slots have Use Operator Stack enabled."),
|
||||||
|
NumCaveSlots);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FStrateSlot& Slot : StrateLayout)
|
||||||
|
{
|
||||||
|
if (!Slot.Definition
|
||||||
|
|| Slot.Definition->GeneratorType == ECaveGeneratorType::SurfaceWorld
|
||||||
|
|| Slot.Definition->bUseOperatorStack)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("[StrateManager] cave slot=%d name='%s' Z chunks=[%d to %d] bUseOperatorStack=false"),
|
||||||
|
Slot.StrateIndex,
|
||||||
|
*Slot.Definition->StrateName.ToString(),
|
||||||
|
Slot.TopChunkZ,
|
||||||
|
Slot.BottomChunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
CachedSeed = WorldSeed;
|
CachedSeed = WorldSeed;
|
||||||
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
|
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
|
||||||
OriginSpineRadius = Settings->OriginSpineRadius;
|
OriginSpineRadius = Settings->OriginSpineRadius;
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ class VOXELFORGE_API UVoxelGenerator : public UObject
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
UVoxelGenerator();
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// SEED (source unique: Settings->Seed)
|
// SEED (source unique: Settings->Seed)
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
@@ -321,6 +323,10 @@ public:
|
|||||||
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
|
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
/** Identité process-unique du propriétaire des caches `CP_*` thread_local.
|
||||||
|
* Process-unique owner identity for the `CP_*` thread-local cache key. */
|
||||||
|
uint64 DensityCacheOwnerId = 0;
|
||||||
|
|
||||||
/** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */
|
/** Pick the biome (index into Ctx.Biomes) for a Voronoi site, by its climate. */
|
||||||
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;
|
int32 ClassifyBiomeAtSite(float SiteX, float SiteY, const FBiomeContext& Ctx, uint32 SiteHash) const;
|
||||||
|
|
||||||
|
|||||||
@@ -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 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 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 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 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user