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