perf(opstack): retain six surface column regions

The single direct-indexed box discarded all 6,561 computed columns whenever spatial or column identity moved. Port the reference six-box LRU so interleaved regions keep five warm working sets, accepting the documented ~0.79 MiB TLS cost per worker to preserve the recommended and measurable A/B path. Also report disabled cave opt-ins at layout initialization; SurfaceWorld is excluded because its exact-lattice tile proof does not use that flag.
This commit is contained in:
2026-08-17 00:37:56 +02:00
parent 588f9e0294
commit 8295f6e76b
3 changed files with 130 additions and 39 deletions
+2 -2
View File
@@ -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::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. |
| `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::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 `\|AB\| ≥ 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`. |
@@ -338,7 +338,7 @@ Maps depth→strate at runtime; owns passages.
- `FStrateSlot` (h:84): definition + chunk-Z range + index.
| 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). |
| `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. |
@@ -661,30 +661,77 @@ namespace
}
/** 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
* 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. */
* Le mémo est un LRU spatial de six boîtes à index direct, comme `GSurfColCache` : la
* pile évalue tous les Z d'une colonne au même XY, donc l'overhang lit la MÊME colonne
* que la source, par construction plutôt que par convention.
*
* 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; };
const FColumn& GetColumn(float WorldX, float WorldY) const
{
// Même schéma éprouvé que `GSurfColCache` : index direct dans une boîte XY, puis un
// drapeau `Computed` par cellule. Une tuile MC pleine résolution demande
// (CHUNK_SIZE + 3)² = 35×35 = 1225 colonnes (anneau de marge inclus) ; cette boîte
// de Dim×Dim, recentrée sur le premier échantillon, les garde toutes sans collision.
// Same proven scheme as `GSurfColCache`: direct XY indexing plus one `Computed` flag per
// cell. A full-resolution MC tile needs 35×35 = 1225 columns including its margin ring;
// the box is sized so one tile fits without eviction.
// Même schéma éprouvé que `GSurfColCache` : six boîtes à index direct dans XY, chacune
// avec un drapeau `Computed` par cellule et une clé uint64 exacte. Une tuile MC pleine
// résolution demande 35×35 = 1225 colonnes (anneau de marge inclus) ; une boîte de
// Dim×Dim, recentrée sur le premier échantillon, les garde toutes sans éviction.
// Same proven scheme as `GSurfColCache`: six direct-indexed XY boxes, each with one
// `Computed` flag per cell and an exact uint64 key. A full-resolution MC tile needs
// 35×35 = 1225 columns including its margin ring; one Dim×Dim box holds that tile.
struct FColumnBox
{
enum : int32 { Halo = CHUNK_SIZE + 8, Dim = 2 * Halo + 1 };
int32 BaseX = 0, BaseY = 0;
uint64 Key = 0; // strate + layout + seed + ParamsFingerprint
uint32 LastUse = 0; // LRU stamp
bool bValid = false;
FColumn Cols[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 = {};
// The production mesher and the exact-lattice classifier use integer XY. Fractional
@@ -701,18 +748,9 @@ namespace
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));
}
// Acquire vérifie la clé uint64 complète et les bornes exactes avant de dériver CI.
// Acquire checks the exact uint64 key and exact bounds before deriving CI.
FColumnBox& Box = Cache.Acquire(IX, IY, ColumnKey);
CI = (IY - Box.BaseY) * FColumnBox::Dim + (IX - Box.BaseX);
MemoColumn = &Box.Cols[CI];
@@ -830,11 +868,13 @@ namespace
* 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 —
* `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
* la STRATE et la version de layout, pas le chunk. Le mémo étant `thread_local`, il SURVIT
* à la reconstruction de la pile — seule la clé l'invalidait.
* Donc la clé garde l'identité partagée : ce qui rend deux colonnes interchangeables, c'est
* la STRATE, le seed, la version de layout et les params, pas le chunk. Le mémo étant
* `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
* `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
* 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
* biome field is documented Z-independent.
* stack; this now shares the same strate/layout/seed identity and adds the required params
* 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
{
@@ -881,14 +922,14 @@ namespace
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
// AU-DESSUS (`GSurfColCache` dans `GetDensityAt`), clé sur (boîte XY, StrateKey, Seed).
// En rajouter un ici demanderait une seconde clé de cache à tenir juste — et une clé de
// cache fausse dans un op partagé sur toute la pile verticale est précisément le mode de
// défaillance qu'`AUDIT §6.3` décrit. Le branchement (étape 2b) réutilise le cache
// existant plutôt que d'en inventer un second.
// No per-column memo here on purpose: T1.a already exists one level up, and a second
// cache key is a second thing to get wrong.
// Le mémo par colonne vit ici, dans six boîtes thread_local partagées par les instances
// mais séparées par la clé, et lues par les Eval de cette source et FOverhangShelfMod.
// Il est séparé de `GSurfColCache` : la pile possède ses propres sorties et sa clé
// complète (strate + layout + seed + empreinte des params), donc réutiliser le cache
// du générateur serait incorrect.
// The per-column memo lives here in six thread-local boxes shared across instances but
// separated by the key, and read by this source's Eval calls and FOverhangShelfMod.
// It is separate from `GSurfColCache`: the stack owns its own outputs and full key.
const FColumn& C = GetColumn(WorldX, WorldY);
float Density = C.TerrainZ - WorldZ;
@@ -129,6 +129,56 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
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;
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
OriginSpineRadius = Settings->OriginSpineRadius;