diff --git a/CODEMAP.md b/CODEMAP.md index 4b821cc..7e87873 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -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 `\|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`. | @@ -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. | diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index f943889..cf2f58f 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -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; diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index ecbb7af..8b3d365 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -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;