Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 451ebcb776 | |||
| 91585ea493 | |||
| 4d33321bfa | |||
| 2303442d8f | |||
| 871ca190af | |||
| a2c5e02713 | |||
| 05986bd875 | |||
| 4ae9e6e72f | |||
| b06c39c077 | |||
| 9740d117e0 | |||
| 45c61dd00e |
+77
-313
@@ -1,339 +1,103 @@
|
||||
# Handoff — VoxelForge operator stack, updated 2026-08-16 (four things queued on ONE build)
|
||||
# VoxelForge handoff — the operator stack is DONE. Read this, not the old jargon.
|
||||
|
||||
> Paste the block below into a fresh session. Everything it refers to is on disk and in git.
|
||||
>
|
||||
> **State:** 8 of 8 archetypes ported and green. Tile-skipping is measured **in the automation
|
||||
> harness** (11 of 40 tiles proved `AllSolid` at production defaults, 14641 voxels brute-forced,
|
||||
> 0 violations) and **still unobserved in the running game**. `AUDIT §C2` is **fully closed** (both
|
||||
> halves — verified 2026-08-16, don't reopen). `experimental` is pushed.
|
||||
>
|
||||
> ## ⛔ FOUR unbuilt things are stacked on `experimental`. Build once, read four numbers.
|
||||
>
|
||||
> | # | commit | what to read |
|
||||
> |---|---|---|
|
||||
> | 1 | `e002bd4` VerticalShafts connector capsules | `Box verdicts over 60 VerticalShafts tiles` — **0 has been the number for the project's whole life**; `violations` must stay 0 |
|
||||
> | 2 | `eb317d9` `stat VoxelForge`, 8 counters | baseline `TilesOpStackSolid` = 0 → tick one `TunnelNetwork` strate → **non-zero**. That is the production proof of T1.d, which has never existed |
|
||||
> | 3 | `7dbdf51` `ExtraReach` × `VF_PerlinAbsBound` | Maze/VerticalShafts may prove **FEWER** tiles. **That is correct, not a regression** |
|
||||
> | 4 | `eaa44bf` `Max3` radius envelope | **a NO-OP at shipped defaults is the correct result** — any moved number means the diff did more than intended |
|
||||
>
|
||||
> In all four: the eight equivalence tests must stay green, and `violations` must stay 0.
|
||||
>
|
||||
> **⚠️ 3 and 4 are CORRECTNESS fixes to box verdicts, found by auditing all 28 `EffectOverBox`
|
||||
> implementations.** Both were the same mistake: *a bound taken from the parameter that reads like
|
||||
> the maximum instead of the supremum of what `Eval` actually produces* — and both times a correct
|
||||
> instance of the same reasoning already existed elsewhere in the same file. See
|
||||
> `OPSTACK-PROGRESS.md` 2026-08-16 (e) and (f); the sound-and-checked ops are listed there so they
|
||||
> are not re-audited.
|
||||
> Paste this into a fresh session. Written 2026-08-16, deliberately in plain language: the previous
|
||||
> version of this file had become a private dialect that Jahni could not read, which is a failure of
|
||||
> the document, not of the reader.
|
||||
|
||||
---
|
||||
|
||||
You're picking up the VoxelForge UE5 voxel plugin on branch `experimental` (already checked out —
|
||||
do not create another). I'm Jahni. The design and the history are written down so you don't
|
||||
re-derive them.
|
||||
## 1. What the operator stack was, in one paragraph
|
||||
|
||||
## Read first, in this order
|
||||
Cave generation used to be one big `switch`: each cave type (tunnels, maze, shafts, floating
|
||||
islands, surface…) was its own hardcoded ~200–1000 line C++ function. The refactor replaced that with
|
||||
small composable pieces ("operators") that stack up to produce the same terrain. **The promise was
|
||||
that you could eventually invent new world types by combining pieces in the editor instead of asking
|
||||
for another thousand-line function.**
|
||||
|
||||
1. **`CLAUDE.md`** — project rules. **Rule #1 is absolute: never build, compile, or run the editor.**
|
||||
I build everything myself. When code is done, stop, say "ready to build", list the likely
|
||||
compile-error spots, and wait.
|
||||
2. **`OPSTACK-PROGRESS.md` — THE LAST ENTRY FIRST.** Append-only log; the resume point.
|
||||
3. **`OPSTACK-PLAN.md`** — the plan. **§2.6.1 is the acceptance bar** and supersedes §2.6.
|
||||
4. **`OPSTACK-DECOMPOSITION.md`** — per-archetype breakdown. **§0.2** (the amplitude bound) is now
|
||||
*implemented*, not pending; §2 TunnelNetwork and §8 Underwater are history, not instructions.
|
||||
5. **`AUDIT-2026-07.md`** — **§C2's SDF-cache half is FIXED (2026-07-28)**, its live-edit half
|
||||
(`OC_Chunk` / `BM_Chunk` / `FChunkBiomeCache`) is still open; §C10 is SOLVED, don't reopen;
|
||||
§C9's library half is the top open theoretical risk with 0 measured exposure.
|
||||
6. **`CODEMAP.md`** — navigation. Trust symbol names over line numbers.
|
||||
**That promise — "Phase 3", ops as data assets — was never built, and is NOT being built now.**
|
||||
|
||||
## How we work now — Codex writes, you orchestrate
|
||||
## 2. Status: done. Stop refactoring.
|
||||
|
||||
From 2026-07-29 this project runs **in tandem with Codex (Model Luna, xHigh)**. **Codex handles most
|
||||
of the coding; you orchestrate.** Concretely:
|
||||
- **8 of 8 archetypes ported**, running in production (`bUseOperatorStack` is ticked on the game's
|
||||
strate assets), and **bit-for-bit identical** to the old path. Verified by a 115 000-sample field
|
||||
digest plus eight per-archetype equivalence tests.
|
||||
- **14/14 automation tests green, 0 violations anywhere.**
|
||||
- **39 % of tiles are skipped in the running game** (`Tiles Meshed` 1.15 vs `Tiles Classified` 1.88).
|
||||
It was 0 % on the morning of 2026-08-16. Each skipped tile avoids ~43 000 density evaluations plus
|
||||
marching cubes.
|
||||
|
||||
- You read the code and decide *what* to do; you write **precise specs** Codex executes; you **review
|
||||
what comes back against the real code, not against its description**; you own the docs
|
||||
(`OPSTACK-PROGRESS.md`, `CODEMAP §3`, this file) and the measurements.
|
||||
- **Hand Codex the INVARIANT, not just the task.** This codebase's traps are invisible in a diff —
|
||||
density sign, `Identity` meaning `Sdf ≥ T` (below), cache keys needing params + `LayoutVersion`,
|
||||
inserting classes above the anonymous-namespace end marker. A spec that omits these gets code that
|
||||
compiles and deletes collision.
|
||||
- `CODEX-TASK-*.md` at the plugin root are the specs. Each carries a **Why**, the **exact site**, the
|
||||
**invariants**, an **acceptance** section, and **notes for the reviewer**. Write the next one the
|
||||
same way.
|
||||
- Unchanged: **never build** (Jahni does), and a plausible patch is not a verified one until a
|
||||
measurement says so.
|
||||
**Decision taken 2026-08-16, with Jahni:** the op stack is finished. **Do not start Phase 3. Do not
|
||||
start another refactor.** The old `switch` stays in place as the correctness oracle — deleting it
|
||||
buys nothing today. Next work should be things Jahni can *see*: see `fable-idea.md` (F7 set-pieces,
|
||||
F9 audio were queued before this started).
|
||||
|
||||
## Where things stand
|
||||
### The honest ledger, so nobody re-litigates it
|
||||
|
||||
All 8 archetypes have an operator-stack twin, per-strate opt-in, each equivalence-tested **bit for
|
||||
bit** against its original density function. The `switch` and the stack are two complete,
|
||||
interchangeable implementations.
|
||||
Three weeks, 120 commits, from 2026-07-27. Every `feat:` commit in that window is a *port* of
|
||||
something that already worked. **The world did not change by a single voxel — that was the
|
||||
acceptance criterion.** What Jahni actually got: the 39 % perf win, two genuine pre-existing bugs
|
||||
found (a use-after-free on every strate-asset edit while streaming, and an under-bounded room
|
||||
collection that could make two multiplayer peers generate different geometry), and a number of fixes
|
||||
to bugs the refactor itself introduced. That is a thin return for three weeks, and it is why the
|
||||
direction changed.
|
||||
|
||||
Everything sits behind `UVoxelStrateDefinition::bUseOperatorStack`; the ported list lives **only** in
|
||||
`UVoxelStrateManager::UsesOperatorStackForChunk` (all 8). `GetDensityAt` and `ClassifyTile` build the
|
||||
stack through the **same** factory, `VF_BuildOpStackForChunk` — a second copy would be a hole.
|
||||
## 3. The jargon, translated
|
||||
|
||||
> ### ⚠️⚠️ CORRECTED 2026-08-16 — **THE FLAG IS ON IN THE GAME'S DATA ASSETS.**
|
||||
>
|
||||
> This section used to say *"No strate asset has the box ticked — that is my call and I still
|
||||
> haven't made it."* **That is false and was believed for a whole session.** Jahni: *"the data assets
|
||||
> in game have the switch on."*
|
||||
>
|
||||
> **Everything downstream of that premise flips:**
|
||||
> - The operator stack is **the production density path**, not a dormant twin. The measured perf
|
||||
> regression is a regression players feel, not a lab result.
|
||||
> - Any unsoundness in an op's `EffectOverBox` is a **live** hole, not a latent one. Two were found
|
||||
> and fixed on 2026-08-16 (`7dbdf51`, `eaa44bf`) and **both of those commit messages say "nothing
|
||||
> in the running game was affected" — that sentence is WRONG, for this reason.** See
|
||||
> `OPSTACK-PROGRESS.md` 2026-08-16 (h) for the corrected severity.
|
||||
> - The T1.d prize is already being collected in-game; `CODEX-TASK-001`'s counters measure how much.
|
||||
>
|
||||
> **Never state the flag's state from memory again — it lives in `.uasset` data, which is not
|
||||
> greppable from here. Ask, or read it in the editor.**
|
||||
Almost all of it means one thing: **can we prove a chunk of world is entirely rock or entirely air
|
||||
without checking every point in it, so we can skip the expensive work?**
|
||||
|
||||
### ✅ T1.d — the tile-skipping prize — is real and measured **in the harness** (not yet in the game)
|
||||
|
||||
`FRoomGraphSource::EffectOverBox` answers **spatially**. The result, brute-forced voxel by voxel:
|
||||
|
||||
```
|
||||
[production defaults] 11 of 40 tiles proved AllSolid — 14641 voxels checked, 0 violations
|
||||
[dense fixture] 0 of 40 — correct, and structurally inevitable
|
||||
```
|
||||
|
||||
One function's verdict is inherited by `FSdfConvertOp`, the twelve detail modifiers (via
|
||||
`VF_NoCaveOverBox`) **and** `FWormFieldSource` — fourteen operators from one place. That is what the
|
||||
C1 wiring was built for.
|
||||
|
||||
### ⚠️⚠️ THE ONE INVARIANT THAT CAN DELETE COLLISION — read before touching any op
|
||||
|
||||
**`FRoomGraphSource::EffectOverBox` returning `Identity` now means `Sdf ≥ T`, NOT `Sdf == FLT_MAX`**,
|
||||
where `T = max(3·SDFBlendRadius, WormNetworkRange)`. That is sound only because all three consumers
|
||||
of the SDF channel were read one by one:
|
||||
|
||||
| consumer | threshold |
|
||||
| term | plain meaning |
|
||||
|---|---|
|
||||
| `FSdfConvertOp::Eval` | `Sdf >= Blend`, and the tunnel stack passes `MakeSdfCarve(P.SDFBlendRadius, …)` ⇒ **K** |
|
||||
| the twelve modifiers | `VF_NearCaveSurface` ⇒ **3K** |
|
||||
| `FWormFieldSource::Eval` | `CaveSDF >= WormNetworkRange` ⇒ **WormNetworkRange** |
|
||||
| **T1.d / tile skipping** | that idea. The single biggest perf item in the plan. |
|
||||
| **box verdict / `ClassifyBox`** | "is this whole box uniform?" → `AllSolid`, `AllAir`, or `Mixed` (don't know) |
|
||||
| **`Mixed`** | "can't prove it" — always safe, just means we do the work |
|
||||
| **`ClassifyTile`** | the function that decides, per tile, whether to skip meshing |
|
||||
| **operator / op stack** | one generation step (rock, carve, roughness…) and the list of them |
|
||||
| **equivalence test** | proof the new path produces byte-identical terrain to the old one |
|
||||
| **`violations`** | ⚠️ **the only number that means danger.** A tile wrongly proved uniform has *no geometry and no collision* — a player falls through the floor. Must always be 0. |
|
||||
|
||||
**Any new consumer of `InOut.Sdf` must have a threshold ≤ `T`, or be added to that `max`.** An op
|
||||
reading `Sdf < 100` would see false `Identity` verdicts and produce tiles with no geometry **and no
|
||||
collision**. The warning is written at the site you land on when you add one.
|
||||
## 4. What is verified, and what is not
|
||||
|
||||
(The `−K` slack covers *any* number of primitives because `SmoothMin`'s penalty is exactly zero once
|
||||
`|A−B| ≥ K`, so the running minimum saturates at `K` below the smallest term. Without that
|
||||
observation the slack would scale with the ~88 tunnels in a cache and the criterion would be dead.)
|
||||
**Verified:** everything through commit `871ca19` — tests green, digests unchanged, 39 % measured
|
||||
in game.
|
||||
|
||||
## First actions — one build to read, two tasks to hand Codex
|
||||
⚠️ **Built but NOT re-verified:** `4d33321` (Sol's boundary fold — lets a tile that straddles cave and
|
||||
open air still resolve) and `91585ea` (a test-only warning demotion). Jahni built these and says the
|
||||
game *looks* fine, but **the test suite has not been re-run and the counters have not been re-read
|
||||
since.** Before trusting them:
|
||||
|
||||
### (a) Hand Codex `CODEX-TASK-001-tile-skip-stats.md`, then `-002-` — 001 first, they chain
|
||||
1. run the `VoxelForge` automation filter — **`violations` must be 0 and all eight equivalences
|
||||
bit-identical**;
|
||||
2. `stat VoxelForge` in game — the accounting must close:
|
||||
`Tiles Meshed + Skipped All Air + Skipped All Solid = Tiles Classified`.
|
||||
|
||||
Everything in this refactor has been proved in an automation harness on 40 sampled tiles, and
|
||||
**nothing has ever been observed in the running game.** Task 001 adds a `stat VoxelForge` group with
|
||||
`TilesClassified / TilesSkippedAllSolid / TilesSkippedAllAir / TilesMeshed`, **plus
|
||||
`TilesOpStackSolid / TilesOpStackAir` at a second site**. Task 002 adds `ColumnMemoHit / Miss` to the
|
||||
same group; it needs 001's header to exist, and both should land in one build.
|
||||
If either fails, `git revert 4d33321` — the 39 % win does not depend on it.
|
||||
|
||||
Its deliverable is a **before/after that constitutes the production proof of T1.d**: after ticking
|
||||
`bUseOperatorStack` on one `TunnelNetwork` strate and flying the same route, **`TilesOpStackSolid`
|
||||
must go non-zero**. The spec carries the invariants — most importantly that `bTrivialEmpty` decides
|
||||
whether a tile has **collision**, and that `GenerateTileResult` runs on **worker threads** so a plain
|
||||
`static int32++` is a data race.
|
||||
## 5. Rules that still prevent real bugs
|
||||
|
||||
⚠️ **Corrected 2026-08-16 — the earlier version of this bar was unmeasurable.** It said
|
||||
`TilesSkippedAllSolid` would read 0 underground with no strate opted in. It will not: `ClassifyTile`
|
||||
also proves `AllSolid` on its **hand-written** path (a bedrock-gap chunk sets `bCanAir = false`,
|
||||
`VoxelGenerator.cpp` ~2835), which fires with nothing ticked at all. That is the same "~84 %" caveat
|
||||
below, which the old bar quoted and then contradicted. The op-stack-only counters are zero **by
|
||||
construction** — the cave branch returns `Mixed` at `UsesOperatorStackForChunk` — so they are the
|
||||
ones that prove anything.
|
||||
- **Never build.** Jahni builds; he has the editor open and it costs him real time. Say "ready to
|
||||
build" and list likely compile-error spots.
|
||||
- **`violations` 0 and the eight equivalences bit-identical** — the only non-negotiable results.
|
||||
- **A bound used to skip work must be PROVED, not observed.** Use `VF_PerlinAbsBound` (= 1.5);
|
||||
`FMath::Lerp(A,B,t)` spans `[min(A,B), max(A,B)]`, so a radius envelope is `max(Min,Max)`.
|
||||
- **Never change `ClassifyTile`'s conditions or return values casually.** Every `return` there fails
|
||||
safe to `Mixed`.
|
||||
- **Never state a `.uasset` value from memory** (like `bUseOperatorStack`). Ask, or read it in the
|
||||
editor. This sent a full day sideways.
|
||||
- `FindSlotIndexForChunkZ` is **protected**; `GetStrateChunkZBounds` is the public equivalent.
|
||||
- Push `experimental` freely. **Never push `main`.**
|
||||
|
||||
Interim answer if Jahni wants it before that lands: **Unreal Insights already shows this.** The trace
|
||||
scopes `VoxelForge_ClassifyTile` and `VoxelForge_GenerateMesh` exist at the site; a skipped tile is a
|
||||
`ClassifyTile` with no `GenerateMesh` after it. ⚠️ But ~84 % of tiles were *already* being rejected by
|
||||
the hand-written SurfaceWorld/bedrock paths long before this work, so surface skips will drown the
|
||||
cave ones — you must be **underground in an opted-in `TunnelNetwork` strate** for the number to mean
|
||||
anything.
|
||||
## 6. Two lessons that generalise beyond this plugin
|
||||
|
||||
### (b) Build `e002bd4` (VerticalShafts) and read ONE line
|
||||
- **A signal that always says the same thing measures nothing.** A counter that can fire for two
|
||||
reasons is not a measurement — splitting one such counter is what finally cracked T1.d after a day
|
||||
of wrong inference. A warning that fires every run and always means "this is fine" is noise that
|
||||
trains the reader to ignore warnings; one of those quietly worried Jahni for several sessions.
|
||||
- **Verify the premise, and verify it completely.** Multiple confident chains reversed on checking
|
||||
this month. Twice the failure was a *partial* read — grepping a symbol and reporting it as checked
|
||||
for something else. A grep that finds a declaration has not checked its access specifier.
|
||||
|
||||
Everything before it is built and green.
|
||||
## 7. The open question, which matters more than any of the above
|
||||
|
||||
> Build, run the `VoxelForge` filter, and read
|
||||
> **`Box verdicts over 60 VerticalShafts tiles`**.
|
||||
>
|
||||
> **0 was the number for the whole project's life.** Its `EffectOverBox` used to return `CarveOnly`
|
||||
> because a shaft merely *existed* within a `Spacing*1.6` halo — true almost everywhere at
|
||||
> `ShaftSpacing 55 / ShaftDensity 0.6`. It now rebuilds the connectors the way `GetCells` does and
|
||||
> tests the real capsules, with **Z exact** and XY conservative.
|
||||
>
|
||||
> - **Non-zero, and `violations` still 0** ⇒ it worked; record it and move on.
|
||||
> - **Still 0** ⇒ the warning in that test names what to check **first**: `ExtraReach` inflates both
|
||||
> remaining tests, so compare it against `ShaftMaxRadius` before touching either test. **Do not
|
||||
> re-derive from scratch** — that is exactly what cost three rounds on TunnelNetwork.
|
||||
|
||||
## Then, in order
|
||||
|
||||
1. **PERF — the biggest open item, and now AIMED (2026-08-16). Read this before touching it.**
|
||||
The op path is measurably slower; one cause was already found and fixed (the column memo
|
||||
discarded itself every chunk). Three things were worked out since, all still **unmeasured**:
|
||||
|
||||
- **The A/B needs no new code.** `VoxelForge_ClassifyTile` and `VoxelForge_GenerateMesh` already
|
||||
exist, and the world is deterministic, so two Insights traces — flag off, then on, same seed and
|
||||
route — are a clean before/after.
|
||||
- **But it is unreadable without task 001.** With the stack on, tiles get *skipped*, so
|
||||
`GenerateMesh` runs fewer times; a total conflates "cheaper per tile" with "fewer tiles" and
|
||||
those pull opposite ways. `TilesMeshed` is the denominator. **⇒ 001 is a PREREQUISITE here, not
|
||||
a parallel item.** Order: 001 → traces → attribution → fix.
|
||||
- **The suspects don't share an archetype**, so measure one ticked strate at a time.
|
||||
*SurfaceWorld* = the hashed column memo. *TunnelNetwork* = **19 virtual calls per voxel**
|
||||
(16 from `BuildTunnelNetworkStack` + 3 from `AppendStructuralPost`), plus the known 12× gate
|
||||
re-test (stage B5's deliberate trade).
|
||||
|
||||
`CODEX-TASK-002` tests the SurfaceWorld suspect and **fixes nothing on purpose** — the derivation
|
||||
says the direct-mapped 4096-slot table evicts ~25 % of columns *every Z plane* (the mesher
|
||||
pre-samples **Z-outermost**), for a derived ~9× on column work. **Derived, not measured.**
|
||||
**Measure before optimising** — the §C10 lesson, re-learned the hard way last session.
|
||||
2. **The warp squeeze — PARKED with its ceiling measured, my recommendation is leave it.** The
|
||||
`WARP SHARE` line says over half the remaining blocking is the query-box dilation, not geometry
|
||||
(production: rooms 0.9 → 0.4, tunnels 2.4 → 1.1 with the dilation zeroed). The only remaining
|
||||
route is proving `sup|Perlin3D|` down from the proved **1.5** toward its apparent ~1.0–1.1, worth
|
||||
~27 % of the dilation. Spot-checking a grid is **not** a proof and a wrong sup is a hole.
|
||||
**A negative result is already recorded so nobody repeats it:** bounding the warp *locally*
|
||||
(evaluate at the box centre, shift, dilate by the variation) is **worse** — a rigorous per-axis
|
||||
Lipschitz bound is `4·1.875 + 1 = 8.5` per unit cell, and `8.5 × 0.206` (the half-box in noise
|
||||
units) `= 1.75` exceeds the global range bound of 1.5.
|
||||
3. **`AUDIT §C9` library half** — `sinf`/`cosf` are not IEEE-754 specified, so MSVC's CRT and glibc's
|
||||
libm can differ. Currently **0 samples within 1e-6 of the isosurface**, i.e. no measured risk. Run
|
||||
`CrossPlatformDigest` on Linux, compare the SHAPE digest, pin it. The real fix if ever needed is a
|
||||
deterministic in-house sin/cos.
|
||||
4. ~~**`AUDIT §C2`'s remaining half**~~ — **✅ CLOSED, verified 2026-08-16. Do not re-open, and do
|
||||
not spec a fix for it — I nearly did.** `OC_Chunk`, `BM_Chunk` and `TC_BiomeCache` all carry a
|
||||
layout-version guard (`OC_Version` / `BM_Version` / `TC_SeenVersion`), `FChunkBiomeCache` has an
|
||||
explicit `Invalidate()` that all four `thread_local` instances call on a version change, and the
|
||||
only other two instances in the tree are **function-local**, so they cannot go stale. Recorded in
|
||||
`AUDIT-2026-07.md §C2`. The live-edit half was fixed at the same time as the determinism half;
|
||||
only this list was stale.
|
||||
5. **Phase 3 — ops as data assets.** A design conversation, not a transcription. Don't start it
|
||||
unprompted. What makes it possible is already in place: ops depend on capabilities
|
||||
(`IVoxelBiomeField`), never on `UVoxelGenerator`.
|
||||
|
||||
## Debts — status changed, read this before acting on the old text
|
||||
|
||||
1. **"Box bounds read STRATE params but a per-room op can raise them" — DORMANT, not urgent.**
|
||||
Checked rather than paid, and the check reversed the premise: when the source proves `Identity`
|
||||
the twelve modifiers are `Identity` **soundly** (their `bNearCaveSurface` gate never opens, so no
|
||||
room op can enable anything), and when it answers `Both` it supplies no `MaxCarveOverBox`, so the
|
||||
default `FLT_MAX` kills every hypothesis regardless of what the modifiers claim. **It goes live
|
||||
the day `FRoomGraphSource` gains a `MaxCarveOverBox`** — bounding the converter's `2·BaseDensity`
|
||||
would make the modifiers' own numbers matter for the first time. Written at the site.
|
||||
2. **`AUDIT §C2` — FIXED on the `switch` path.** `GetDensityWithParams` now takes **required**
|
||||
`ParamsFingerprint` + `LayoutVersion`. Required, not defaulted, so a caller that forgets fails to
|
||||
compile. The CRC is taken **once per chunk** where the params memo already lives (`CP_TunnelFP`) —
|
||||
a `MemCrc32` per voxel on the hottest path would have been a real regression. Note the audit's own
|
||||
suggested alternative ("add chunk Z to the key") is both insufficient (`Interleaved` makes `Alpha`
|
||||
depend on chunk **XY** too) and destructive (chunk XY is deliberately absent so `WorldX ± 1`
|
||||
gradient probes don't thrash the box — `ARCHITECTURE §8.10`).
|
||||
|
||||
## Hard rules that prevent real bugs
|
||||
|
||||
- **Density sign:** negative = solid at the mesher. Inside the op stack the convention is INTERNAL
|
||||
(**positive = solid**), negated once by the caller. The SDF channel uses standard SDF convention.
|
||||
- **`Identity` from the room source means `Sdf ≥ T`.** See the boxed invariant above. This is the
|
||||
single most dangerous thing in the current code.
|
||||
- **Never run both density paths in one world.** **Comparing them is legitimate** — §C10 is closed
|
||||
since `FPSemantics = Precise`, and all eight equivalence tests compare bit for bit. They are
|
||||
**port-correctness oracles**, not fidelity checks: §2.6.1 requires *same seed ⇒ same world on every
|
||||
peer*, not resemblance to the pre-refactor world.
|
||||
- **Every cache key includes `LayoutVersion` AND the params.** See §C2 and the overhang regression of
|
||||
2026-07-27, where omitting the params silently deleted the overhang and only 1 sample in 20 000
|
||||
crossed the isosurface.
|
||||
- **A bound in a box verdict must be PROVED, not observed.** `|Perlin3D| ≤ 1.5` is derived from
|
||||
`GradDot`'s two-distinct-axes form and the per-axis weighted bound of 0.5 — *not* from the header's
|
||||
"~[-1,1]". Over-estimating costs CPU; under-estimating deletes collision.
|
||||
**⚠️ USE `VF_PerlinAbsBound` — it is file-scope in `VoxelDensityOpStack.cpp` and it is the ONLY
|
||||
copy. Never write a bare `1.0` for a noise amplitude in a reach.** `VoxelNoise::FBM` **normalises**
|
||||
(`return Total / MaxValue`), so `sup|FBM| = sup|Perlin3D|` **exactly** — the octave sum neither
|
||||
amplifies nor attenuates it, and an `FBM`-driven reach needs the same 1.5. This rule was written
|
||||
*before* three `ExtraReach` formulas were found violating it (2026-08-16, fixed in `7dbdf51`):
|
||||
VerticalShafts and Maze were unsound at their shipped defaults, FloatingIslands sound only because
|
||||
its `SDFBlendRadius` happens to be large. **A rule stated in a doc is not a rule enforced in code** —
|
||||
when you add a reach, grep for `VF_PerlinAbsBound` and use it.
|
||||
- `ProcessQueue` stays `EQueueMode::Mpsc`; `Epoch` carries through every async path; don't "optimize"
|
||||
the `ARCHITECTURE §8.10` invariants.
|
||||
- Commit per coherent unit with a real message. **`experimental` is pushed and tracked
|
||||
(`origin/experimental`, since 2026-07-29) — keep it in sync. NEVER push `main`**, which stays the
|
||||
known-good fallback at the commit it has always been. ⚠️ A pushed commit here is **not** a
|
||||
"verified green" marker: the branch carries unbuilt work by design, and only `OPSTACK-PROGRESS.md`
|
||||
says what was actually built.
|
||||
- Update `CODEMAP §3`, `ARCHITECTURE §8`, tick `OPSTACK-PLAN`, append to `OPSTACK-PROGRESS.md`.
|
||||
- **When inserting a class into `VoxelDensityOpStack.cpp` / `VoxelHeightOpStack.cpp`, put it ABOVE
|
||||
the labelled end of the anonymous namespace.** Anchoring on the FACTORIES banner puts it outside,
|
||||
and the brace added with it closes nothing. Made that mistake twice; both files say so.
|
||||
- **Match the codebase's spelling of engine macros.** `KINDA_SMALL_NUMBER`, not
|
||||
`UE_KINDA_SMALL_NUMBER` — the plugin uses the unprefixed form everywhere.
|
||||
|
||||
## Method lessons this refactor actually paid for
|
||||
|
||||
Ordered by how much they cost.
|
||||
|
||||
- **⭐ Instrument what you ASSUMED, not just what you changed.** This is the expensive one, learned
|
||||
over four rounds in one session. The warp dilation — `CaveWarpStrength · VOXEL_NOISE_SCALE ·
|
||||
PerlinAbsBound`, a constant chosen in the first commit — inflated a 10-voxel tile into a 50-voxel
|
||||
query box, **125× the volume**. Four separate tightenings (the worm, the columns, the sampler, the
|
||||
tunnel disjunction) were each individually correct and each landed *around* that untouched term.
|
||||
The tunnel fix, predicted "an order of magnitude", delivered 25 % — **and the instrument said so,
|
||||
and I credited the tunnels.** *When a fix under-delivers against its predicted size, suspect the
|
||||
constant you never measured.*
|
||||
- **Instrument before hypothesising.** §C10 cost six builds and five refuted hypotheses. In this
|
||||
session the attribution line (`AllSolid killed by: …`) was written after *two* wrong guesses and
|
||||
immediately named a third operator nobody had looked at. **A diagnostic that lists candidate causes
|
||||
without measuring them is still a guess wearing rigour** — my "either the tiles straddle cave or
|
||||
the source isn't reaching Identity" warning offered two causes and both were wrong.
|
||||
- **Verify the premise before reasoning from it.** Six times now a confident chain rested on an
|
||||
unchecked assumption and the check reversed it. Latest three: `RoomSpacing` was **42** (the fixture
|
||||
overrides it) while I did three rounds of arithmetic with the header default of 80 — *the number
|
||||
was printing in the report I kept quoting*; "the plugin bets on `|Perlin3D| ≤ 0.8`" was wrong (the
|
||||
cache **rebuilds** when the warped query leaves the box, so that expansion is a perf heuristic);
|
||||
and the per-room-op debt "must be paid first" was wrong (it is dormant). **Include the premises you
|
||||
are confident enough about not to look up — especially a default, when a fixture exists whose whole
|
||||
job is overriding defaults.**
|
||||
- **A sampler must cover at least one period of what it samples.** The tunnel test drew tile XY from
|
||||
**±32 voxels** with `RoomSpacing 80` and a guaranteed origin room at (0,0) — it measured the spine
|
||||
hub and called it the world. The shaft test had the identical bug (±48 against `ShaftSpacing 55`).
|
||||
Both now print their own extent **in units of the pattern's period**.
|
||||
- **A test fixture tuned for coverage can be antagonistic to the thing you are measuring.**
|
||||
`EnableTunnelFeatures` densifies (`RoomSpacing` 80→42, `RoomDensity` 0.35→0.85) so the equivalence
|
||||
check isn't comparing solid rock to solid rock — and at that density the room cull radius *equals*
|
||||
the lattice spacing, so **no box can ever be proved**. `0 proved` there is the correct answer. The
|
||||
box verdict is therefore measured on **both** densities, and the dense run must stay at 0.
|
||||
- **Diagnostics report THIS run; history goes in the log.** The test output had accumulated hardcoded
|
||||
numbers from previous runs beside live ones ("32 of 34 tiles" printed while the live figure was 21
|
||||
of 28). Unreadable, and self-inflicted.
|
||||
- **Read the code, not the comment.** The cliff modifier's comment promises a sampled Z±1 gradient;
|
||||
the code samples nothing and uses a Z-stretched Perlin it *calls* `VertGrad`. Ported as written —
|
||||
and written down, so nobody "fixes" it from the comment.
|
||||
- **A perf change can be a correctness change.** The column-memo optimisation silently deleted the
|
||||
overhang; the tests caught it the same day. Invisible to inspection, and it produced plausible
|
||||
terrain.
|
||||
- **Coverage is a number, not a boolean.** Four related traps, each producing a green run that proved
|
||||
almost nothing:
|
||||
- *A test that prints nothing on success is indistinguishable from one that never ran.*
|
||||
- *A guard that only trips at zero notices absence, it does not measure coverage.* Use fractions.
|
||||
- *A success message that **asserts** coverage instead of reporting it reads as evidence while
|
||||
measuring nothing.*
|
||||
- *A check can be vacuous as well as a counter.* "Nothing leaked" is worthless unless something
|
||||
happened.
|
||||
- **Enabling a feature is not evidence it fired — ask the structure, not the output.** Setting
|
||||
`PitDensity` did nothing (wrong struct). **Prefer the check that can fail for exactly one reason**
|
||||
— and when a zero has several possible causes, give each one its own number.
|
||||
- **An oracle that shares the defect under test proves nothing.** The stale-cache check compares each
|
||||
stack against *itself evaluated alone*. (Since §C2 was fixed, the test call sites now pass a real
|
||||
params fingerprint, so the original no longer shares the defect either.)
|
||||
- **One definition, not two kept in sync.** `VF_BuildOpStackForChunk` exists because a tile skipped on
|
||||
the verdict of a stack that is not the one producing its density is a hole. The same reasoning is
|
||||
why `GetLastRoomBoxDiagnostic` **reads back** what the operator computed instead of letting the
|
||||
test re-derive the criterion, and why the two-density tile scan is one lambda called twice.
|
||||
- **Don't assert a number you want to improve.** Check 4 asserted `0 proved` — honest when written,
|
||||
and it would have forbidden the entire T1.d gain. What it asserts now is that **no proved tile is
|
||||
wrong** (brute force, every voxel); the proved count is *reported*.
|
||||
**What do you want the world to *do* that it doesn't?** Three weeks went into a pipeline instead of
|
||||
that question. Start there.
|
||||
|
||||
@@ -4214,3 +4214,387 @@ The dirty candidate diffs were preserved as named, recoverable stashes before cl
|
||||
|
||||
`../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.
|
||||
|
||||
## 2026-08-16 (o) — build break in the six-box LRU: `Box` used after its scope closed
|
||||
|
||||
`VoxelDensityOpStack.cpp(832): error C2065: 'Box' : identificateur non déclaré`
|
||||
|
||||
Sol's single-box → six-box refactor changed the acquisition to
|
||||
`FColumnBox& Box = Cache.Acquire(...)` **inside** `if (bIntegerXY)`, but the `Computed` flag is only
|
||||
set *after* the column is computed, further down and **outside** that block. The old single-box
|
||||
version had `Box` at function scope, so the write-back compiled; the reference did not.
|
||||
|
||||
Fixed by hoisting `FColumnBox* AcquiredBox = nullptr;` beside `MemoColumn` and writing back through
|
||||
it. The guard is now `if (AcquiredBox)` rather than `if (bIntegerXY)` — **non-null ⇔ integer path**,
|
||||
so the pointer proves its own safety instead of relying on two conditions staying in agreement.
|
||||
|
||||
Swept the file: the only other `Box.` uses (761–763) are inside the `if` and in scope. Not a logic
|
||||
error and not a caching change — a scope slip, invisible in review because the diff showed both
|
||||
halves separately. Still unbuilt beyond this compile fix.
|
||||
|
||||
## 2026-08-16 (p) — the opt-in diagnostic was failing the test suite; and it answered T1.d
|
||||
|
||||
### The six-box LRU worked
|
||||
|
||||
Jahni: *"column memo miss is quite a fraction of what it used to be."* First qualitative
|
||||
confirmation that the LRU port was the right call — the single-box version wiped 6561 cells on every
|
||||
recenter. **Still owed a same-seed/same-route number before it counts as measured.**
|
||||
|
||||
### The diagnostic broke the tests, for doing its job
|
||||
|
||||
Sol's opt-in warning logs at `Warning`. UE's automation framework counts a `Warning` as a failure,
|
||||
and the `Determinism.*` tests **deliberately build a non-opted-in world** — that world is their
|
||||
comparison oracle, the baseline the `OpStack.*` numbers are measured against. So the diagnostic was
|
||||
failing the suite by correctly reporting an intentional configuration.
|
||||
|
||||
Fixed: verbosity is now `Log` when `GIsAutomationTesting`, `Warning` otherwise. The message is
|
||||
written **once** and only the verbosity branches (`FString::Printf` then log `%s`) — four copies of a
|
||||
long format string across two sites would have been the "one definition, not two kept in sync"
|
||||
failure this project already has rules about. `CoreGlobals.h` added explicitly for
|
||||
`GIsAutomationTesting` rather than relying on transitivity, since there is no shared PCH.
|
||||
|
||||
**Answer to "can I set the tests to use the op stack?" — no, and it must stay that way.** The
|
||||
`Determinism.*` fixture being non-opted-in is not an oversight; it is the oracle. `OpStack.*` runs the
|
||||
opted-in variant. Flipping the fixture would delete the comparison that gives every T1.d number its
|
||||
meaning.
|
||||
|
||||
### ⚠️ AND THE DIAGNOSTIC ANSWERED T1.d — pending one check in the GAME log
|
||||
|
||||
```
|
||||
7/7 cave layout slots have Use Operator Stack disabled
|
||||
cave slot=0..3, 5..7 bUseOperatorStack=false
|
||||
```
|
||||
|
||||
Slot 4 is absent, which is Sol's SurfaceWorld narrowing working as intended.
|
||||
|
||||
Those slots are the **test fixture** (`name=''` ⇒ transient objects). **The line that matters is the
|
||||
same log from the real game.** If it also reports slots disabled, then the belief recorded on
|
||||
2026-08-16 (h) — *"the data assets in game have the switch on"* — is **wrong**, and T1.d has never
|
||||
been enabled in production at all. That single log line settles what a day of counter-reading could
|
||||
not, and it costs nothing to read.
|
||||
|
||||
⇒ **Next action for Jahni: open the game (not the tests) and search the log for
|
||||
`Operator-stack opt-in`.** Whatever it says is the ground truth, and it supersedes both my earlier
|
||||
inference from `Cave Bail Not Op Stack` and the recorded belief about the assets.
|
||||
|
||||
## 2026-08-16 (q) — suite green again, and the FIELD digest proves nothing moved
|
||||
|
||||
14/14 pass, no `StrateManager` warnings in the output — the verbosity fix worked and the diagnostic
|
||||
no longer fails the suite it was meant to inform.
|
||||
|
||||
### The result that matters: every number is IDENTICAL to the pre-change run
|
||||
|
||||
```
|
||||
SHAPE digest 0xAC0294F1C6DD354C FIELD digest 0xF62F3D355B1C0BDB (both unchanged)
|
||||
ClassifyTile Mixed 472 / AllSolid 70 / AllAir 58 (unchanged)
|
||||
OpStack ClassifyTile Mixed 541 / AllSolid 46 / AllAir 13 (unchanged)
|
||||
Shafts 30/60 · Maze 27/60 · Slab 45,43,32/60 · Islands 9 AllAir (unchanged)
|
||||
Tunnel [production] 11 proved / 14641 voxels / 0 violations (unchanged)
|
||||
all eight equivalences bit-identical · 0 violations everywhere
|
||||
```
|
||||
|
||||
**The `FIELD` digest is the strongest evidence available here** — a bit-for-bit hash over 115,000
|
||||
samples of the whole field. It is unchanged, so the six-box LRU, the `DensityCacheOwnerId` key, the
|
||||
bail re-attribution and the scope fix **did not move a single voxel between them.** Four changes to
|
||||
caching, keying and diagnostics, and the world is byte-for-byte the same. That is precisely the
|
||||
contract each of them claimed, now tested rather than asserted.
|
||||
|
||||
⇒ Caching changes are validated. **Their PERFORMANCE benefit is still unmeasured** — Jahni's
|
||||
"quite a fraction of what it used to be" on the column memo is a real observation but not a
|
||||
same-seed/same-route number.
|
||||
|
||||
### The one thing still open, and it costs a log search
|
||||
|
||||
**`Operator-stack opt-in` in the GAME's log.** The fixture reports 7/7 cave slots disabled, which is
|
||||
expected there. Whether the *real* world says the same is the ground truth for T1.d, and it
|
||||
supersedes both my inference from `Cave Bail Not Op Stack` and the belief recorded in (h). Nothing
|
||||
else should be built or reasoned about T1.d until that line is read.
|
||||
|
||||
## 2026-08-16 (r) — ⛔ T1.d's BLOCKER FOUND: out-of-layout chunks are misrouted into the cave branch
|
||||
|
||||
In-game `stat VoxelForge`, flying at full speed, with the split bail counters:
|
||||
|
||||
```
|
||||
Tiles Classified 1.90 Tiles Meshed 1.90 ← nothing skipped
|
||||
Cave Bail Not Op Stack No Layout 1.58 (83% of classified)
|
||||
Cave Bail Stack Verdict 0.27
|
||||
Cave Bail Params 0.05
|
||||
Cave Bail Not Op Stack SoleSlot / BoundaryTile / MixedContent ← NEVER FIRED
|
||||
```
|
||||
|
||||
**Jahni was right and I was wrong to doubt the assets.** `SoleSlot` never fires, so every strate he
|
||||
flies is genuinely opted in. `BoundaryTile` never fires either, so the neighbour theory is dead too.
|
||||
Sol's four-way split earned its keep on its first flight: the old lumped counter said "not op stack"
|
||||
and would have sent us at the assets again.
|
||||
|
||||
### The mechanism, read out of the code
|
||||
|
||||
Two functions disagree about what a chunk outside the strate stack *is*:
|
||||
|
||||
```cpp
|
||||
// GetGeneratorTypeForChunk — outside all strates (above or below) →
|
||||
return ECaveGeneratorType::TunnelNetwork; // "the fallback density path produces solid rock anyway"
|
||||
|
||||
// IsGapChunk — above the top strate or below the bottom →
|
||||
return false; // "open air, NOT a gap"
|
||||
```
|
||||
|
||||
So for any chunk **above the top strate** (`TopChunkZ = 0`, i.e. all of the open air above the
|
||||
world) `ClassifyTile` sees: not a gap, not SurfaceWorld ⇒ **cave archetype**. It enters the cave
|
||||
branch, calls `UsesOperatorStackForChunk`, `FindSlotIndexForChunkZ` returns −1, and it bails.
|
||||
|
||||
**Every tile touching open air is being classified as a TunnelNetwork cave tile whose strate has the
|
||||
flag off.** That is 83 % of tiles on his route, and it is why `Tiles Classified == Tiles Meshed`.
|
||||
|
||||
⚠️ **Note the two comments contradict each other** — one calls out-of-layout "solid rock via the
|
||||
fallback", the other calls above-stack "open air". **Do not guess which is true**; the correct verdict
|
||||
for those tiles depends on it, and guessing here writes a hole. Read `GetDensityAt`'s no-slot path
|
||||
before choosing.
|
||||
|
||||
### Why this is the whole T1.d story
|
||||
|
||||
The cave branch was never reached for the tiles that dominate a normal flight. T1.d has not been
|
||||
failing — it has been **unreachable**, and for a reason that has nothing to do with the operator
|
||||
stack, the box verdicts, or the asset flags. All of that machinery is correct and tested; it was
|
||||
sitting behind a routing mistake in the classifier's archetype lookup.
|
||||
|
||||
**Next step (not taken — out of budget, and it needs the density question answered first):** make
|
||||
out-of-layout chunks their own category in `ClassifyTile` rather than borrowing `TunnelNetwork`.
|
||||
If open air above the stack is provably uniform, those tiles become an `AllAir`/`AllSolid` verdict
|
||||
and T1.d finally fires on the majority of a surface flight.
|
||||
|
||||
## 2026-08-16 (s) — T1.d BLOCKER FIXED: out-of-layout is constant AIR, not a cave archetype
|
||||
|
||||
The contradictory comments are settled by reading `GetGenerationParams`:
|
||||
|
||||
```cpp
|
||||
// If outside all strates, return negative density → guaranteed air.
|
||||
if (SlotIdx < 0) {
|
||||
Empty.BaseDensity = -1.0f; Empty.WormStrength = 0.0f; Empty.RoomDensity = 0.0f;
|
||||
```
|
||||
|
||||
⇒ **`GetGeneratorTypeForChunk`'s comment ("the fallback density path will produce solid rock
|
||||
anyway") is FALSE.** Out-of-layout is a **constant air field** — no rooms, no worms, nothing to
|
||||
carve or fill it. `IsGapChunk`'s "open air, NOT a gap" was the correct one.
|
||||
|
||||
### The fix
|
||||
|
||||
`ClassifyTile` gains a fourth Z category (`MemoCat = 3`) for chunks with no layout slot:
|
||||
|
||||
- sets `bAnyNonCave = true` ⇒ **never enters the cave branch**, which is where all 83 % were dying;
|
||||
- sets `bCanSolid = false` ⇒ the AllSolid hypothesis dies, **AllAir survives**;
|
||||
- bails to `Mixed` if a disturbance could *add* rock there (`BridgeDensity`/`RidgeDensity` > 0) —
|
||||
chasms only carve, so they cannot threaten an air verdict.
|
||||
|
||||
A tile entirely above the stack now resolves: no cave block, no column scan (`NumSlots == 0`),
|
||||
`bCanSolid = false`, `bCanAir = true` ⇒ **`AllAir`, skipped.** That is the majority of a surface
|
||||
flight, and it is a class of tile T1.d has never once been able to prove.
|
||||
|
||||
### What to read after the build
|
||||
|
||||
- **`Cave Bail Not Op Stack No Layout` should collapse to ~0** — those tiles no longer reach the
|
||||
cave branch at all.
|
||||
- **`Tiles Skipped All Air` should rise sharply**, and `Tiles Meshed` should fall below
|
||||
`Tiles Classified` for the first time in the game.
|
||||
- ⚠️ **`violations` must stay 0** in every test, and the eight equivalences must stay bit-identical:
|
||||
this changes only the *classifier*, never a density value.
|
||||
|
||||
Compile-risk spots: `FindSlotIndexForChunkZ` is public (`VoxelStrateManager.h:351`, verified);
|
||||
`FStrateDisturbanceParams` is already used later in the same function; the new `else if` sits before
|
||||
the surface `else`, so `Slots[MemoSlotIdx]` is never dereferenced for the new category.
|
||||
|
||||
## 2026-08-16 (t) — build break: `FindSlotIndexForChunkZ` is protected, not public
|
||||
|
||||
`VoxelGenerator.cpp(2796): C2248 — cannot access protected member`.
|
||||
|
||||
I wrote "`FindSlotIndexForChunkZ` is public (`VoxelStrateManager.h:351`, verified)" in the previous
|
||||
entry. **It is `protected`.** The grep I called verification printed the declaration line and not the
|
||||
access specifier above it — I checked that the symbol existed and reported it as checked for
|
||||
something else. Exactly the "assert a negative from a partial read" mistake made earlier today with
|
||||
the test fixture, in the same shape.
|
||||
|
||||
**Fixed without widening the API:** `GetStrateChunkZBounds` is `public:` (line 177), returns false
|
||||
for precisely the no-slot case, and this same function already calls it three times for the same
|
||||
question. A small `VF_ChunkZHasSlot` lambda wraps it so the `else if` reads as the predicate it is.
|
||||
No accessor promoted, no new surface.
|
||||
|
||||
## 2026-08-16 (u) — ✅✅ T1.d FIRES IN PRODUCTION. 39% of tiles skipped, measured in game.
|
||||
|
||||
```
|
||||
BEFORE (a2c5e02^) AFTER
|
||||
Tiles Classified 1.90 1.88
|
||||
Tiles Meshed 1.90 1.15
|
||||
Tiles Skipped All Air (absent) 0.73
|
||||
Cave Bail Not Op Stack NoLayout 1.58 (GONE)
|
||||
Cave Bail Mixed Content (absent) 0.77
|
||||
```
|
||||
|
||||
`1.15 + 0.73 = 1.88` — the accounting closes exactly.
|
||||
|
||||
**39 % of classified tiles are now skipped, against 0 % this morning.** Each skipped tile avoids
|
||||
~43 000 density evaluations plus marching cubes. This is the first time in the project's history
|
||||
that tile-skipping has been observed in the running game rather than in an automation harness.
|
||||
|
||||
`Cave Bail Not Op Stack No Layout` is gone: those tiles now either resolve `AllAir` or attribute to
|
||||
`Cave Bail Mixed Content` (0.77) — genuinely straddling open air and something else, a correct
|
||||
conservative `Mixed` rather than a misrouting.
|
||||
|
||||
### What is NOT yet firing, stated plainly
|
||||
|
||||
`Tiles Skipped All Solid`, `TilesOpStackSolid` and `TilesOpStackAir` are still absent. **The cave
|
||||
branch still produces no verdicts in game** — its attempts now land in `Cave Bail Mixed Content`
|
||||
(0.77) and `Cave Bail Stack Verdict` (0.32). So the win measured here is the **open-air** class,
|
||||
proved by the classifier's own Z categories; the operator stack's `ClassifyBox` has still never
|
||||
returned a verdict in production. That remains the open item, and the counters now name where to
|
||||
look: tiles straddling content boundaries, and boxes the stack legitimately cannot prove.
|
||||
|
||||
### How this was found, worth keeping
|
||||
|
||||
Every earlier attempt reasoned from a number that could mean four things. It broke open when Sol's
|
||||
four-way split of `Cave Bail Not Op Stack` showed `SoleSlot` and `BoundaryTile` at **zero** — which
|
||||
proved the assets were correctly ticked (Jahni said so twice; I doubted it twice) and pointed at
|
||||
`NoLayout`, a cause nobody had considered. *A counter that can fire for two reasons is not a
|
||||
measurement.*
|
||||
|
||||
## 2026-08-17 (v) — mixed-content split + guarded category fold LANDED, awaiting Jahni's build
|
||||
|
||||
**Nothing in this entry was built, compiled, run in the editor, automation-tested, or measured.**
|
||||
The only validation was static source/diff inspection and `git diff --check`. All rates below are
|
||||
instructions for the next flight, not claimed results.
|
||||
|
||||
### The old 0.77 bucket is split before changing its guard
|
||||
|
||||
`Cave Bail Mixed Content` had three call sites and four meanings. It is replaced by:
|
||||
|
||||
- `Cave Mix Out Of Layout` — a tile contains one cave slot plus constant-air outside the layout;
|
||||
- `Cave Mix Gap` — one cave slot plus solid inter-strate bedrock;
|
||||
- `Cave Mix Surface World` — one cave slot plus a SurfaceWorld slot;
|
||||
- `Cave Bail Two Cave Slots` — a second cave slot was found; this remains an immediate `Mixed` bail.
|
||||
|
||||
The first three are deliberately **presence counters**, incremented once per present category per
|
||||
tile before the later stack guards. They are not exclusive for an unusually tall tile, so they are
|
||||
not expected to sum to the old 0.77; each answers one unambiguous yes/no question. This placement is
|
||||
why the diagnostic survives the fold: a tile can increment `Cave Mix Out Of Layout` and later be
|
||||
proved `AllAir`. `Two Cave Slots` is still a bail counter because that condition was not relaxed.
|
||||
|
||||
The split was applied first with the original `return Mixed` intact. Only after that
|
||||
behavior-preserving split was inspected was the return deliberately replaced by the fold below.
|
||||
|
||||
### Decision: relax one-category, NOT one-cave-slot
|
||||
|
||||
Folding is safe, but only in the narrow form suggested by the code:
|
||||
|
||||
- Gap already contributes a proved `solid` hypothesis, out-of-layout a proved `air` hypothesis,
|
||||
and SurfaceWorld classifies its exact mesher lattice through `ComputeSurfaceColumn`.
|
||||
- The one cave slot records the minimum/maximum **sampled cave Z**. `ClassifyBox` receives that cave
|
||||
Z sub-box with the tile's full XY extent; asking it about the previous full tile box would model
|
||||
gap/surface/out-of-layout with cave params and is not valid.
|
||||
- Opt-in, generator type, and bit-identical params are still checked for **every chunk intersecting
|
||||
that cave sub-box in XYZ**. They were narrowed in Z only to exclude categories represented by
|
||||
their own hypotheses; none was removed or weakened.
|
||||
- The stack verdict folds into the existing `bCanSolid` / `bCanAir`, then SurfaceWorld's exact
|
||||
column scan still runs. `Tiles Operator Stack Solid/Air` now increments only for the final
|
||||
non-`Mixed` tile verdict, after all categories have agreed.
|
||||
|
||||
The independent **one-cave-slot requirement cannot be relaxed by this fold**. A second cave slot can
|
||||
carry a different archetype, opt-in, params, and stack; proving it would require building and
|
||||
guarding a second stack, not stretching the first. `Cave Bail Two Cave Slots` therefore still
|
||||
returns `Mixed`. Category folding is compatible with the generator-type and bit-identical-param
|
||||
guards precisely because those guards remain exhaustive over the sole cave sub-box.
|
||||
|
||||
`Cave Bail Fold Conflict` was added for a proved cave verdict that contradicts an already proved
|
||||
category (for example cave=solid plus out-of-layout=air). That is distinct from `Cave Bail Stack
|
||||
Verdict` (the stack itself could not prove its box) and `Cave Bail Disturbance` (a post-stack feature
|
||||
killed the remaining hypothesis).
|
||||
|
||||
### Exact next-build / next-flight readout
|
||||
|
||||
Read these counters together on the same underground route:
|
||||
|
||||
1. `Cave Mix Out Of Layout`, `Cave Mix Gap`, `Cave Mix Surface World`, and
|
||||
`Cave Bail Two Cave Slots` — whichever is largest names the dominant old mixed-content cause.
|
||||
The first three are presence counts and may overlap; `Cave Bail Mixed Content` should be absent.
|
||||
2. `Tiles Operator Stack Air` and `Tiles Operator Stack Solid` — either becoming non-zero proves a
|
||||
cave stack participated in a **final** skip verdict. For the measured cave+open-air population,
|
||||
the expected success signal is primarily `Tiles Operator Stack Air`.
|
||||
3. `Tiles Skipped All Air`, `Tiles Skipped All Solid`, `Tiles Meshed`, and `Tiles Classified` — the
|
||||
accounting must still close: `Meshed + SkippedAllAir + SkippedAllSolid = Classified`. A fold win
|
||||
raises a skipped counter and lowers `Tiles Meshed`; the pre-fold baseline was 1.15 meshed,
|
||||
0.73 skipped-air, 1.88 classified.
|
||||
4. `Cave Bail Fold Conflict`, `Cave Bail Stack Verdict`, `Cave Bail Params`,
|
||||
`Cave Bail Disturbance`, and `Cave Bail Two Cave Slots` — these explain mixed tiles that remain.
|
||||
In particular, a high `Fold Conflict` means the dominant mix is genuinely heterogeneous rather
|
||||
than blocked merely by the former one-category policy.
|
||||
|
||||
After Jahni runs the suite, every brute-force `violations` count must remain zero and all eight
|
||||
operator-stack equivalences must remain bit-identical. Those are correctness requirements, not
|
||||
results from this unbuilt change.
|
||||
|
||||
### Likely compile-error / review watch spots
|
||||
|
||||
- declaration/definition/use spelling for the five new UE stats, especially
|
||||
`STAT_VoxelForgeCaveBailFoldConflict` and `STAT_VoxelForgeCaveBailTwoCaveSlots`;
|
||||
- the `MAX_int32` / `MIN_int32` cave-extrema initializers and `FMath::Min/Max(int32)` assignments to
|
||||
`CaveMinZ` / `CaveMaxZ`;
|
||||
- construction of `FBox CaveBox` from the cave-only integer Z extrema;
|
||||
- control flow after removing the cave branch's final return: SurfaceWorld columns must run before
|
||||
the shared final verdict, and `bAnyCave` must imply initialized cave extrema (established in the
|
||||
same `MemoCat == 2` iteration);
|
||||
- no protected layout accessor was introduced: the code still uses public
|
||||
`GetStrateChunkZBounds`, never `FindSlotIndexForChunkZ`.
|
||||
|
||||
## 2026-08-16 (v) — the permanent "warning" was noise, and it cost Jahni several sessions of worry
|
||||
|
||||
Jahni: *"[this] have been in a 'warning' stage, is this fine ? is everything fine ?"*
|
||||
|
||||
**Everything was fine.** The only `Warning:` in that whole run is the tunnel test's
|
||||
`[dense fixture] No tile was proved`, whose own text says *"On the DENSE FIXTURE this is the EXPECTED
|
||||
and correct result, not a defect."* The test reports Success; density is bit-identical; every
|
||||
coverage number is non-zero; `violations` is 0; and the `[production defaults]` run directly below
|
||||
reports **11 proved / 14641 voxels / 0 violations**.
|
||||
|
||||
**But a warning that fires on every run and always means "this is fine" is not a warning — it is
|
||||
noise that trains the reader to ignore warnings.** It sat there for several sessions and made him
|
||||
uneasy about a green suite. That is a real cost, and it came from writing diagnostics for a reader
|
||||
who already had the context.
|
||||
|
||||
Fixed: `RunTileScan` takes `bZeroProvedIsExpected`. On the **dense fixture** 0 proved is the only
|
||||
arithmetically possible answer (cull spheres cover that world ~3.6×) ⇒ `AddInfo`. On **production
|
||||
defaults** 0 proved would be a genuine regression from today's 11 ⇒ still `AddWarning`. Same message,
|
||||
same information, but the severity now carries meaning.
|
||||
|
||||
⇒ **General rule for this project's diagnostics:** if a line cannot distinguish a good run from a bad
|
||||
one, it must not be logged at a severity that implies something is wrong. The same defect as the
|
||||
lumped bail counter, one layer out: *a signal that always says the same thing measures nothing.*
|
||||
|
||||
## 2026-08-16 (w) — CLOSED. The operator stack is declared done; direction changes to visible work.
|
||||
|
||||
Jahni built `4d33321` + `91585ea` and reports the game looks fine. He asked for the handoff to be
|
||||
updated and the thread closed.
|
||||
|
||||
**`OPSTACK-HANDOFF.md` fully rewritten in plain language.** The previous version had become a private
|
||||
dialect — his words: *"you've started using very unique terminology, i have NO idea what you've been
|
||||
doing."* That is a failure of the document, not of the reader. The new one leads with what the op
|
||||
stack was for, states plainly that Phase 3 was never built and is not being built, carries the honest
|
||||
three-week ledger, and includes a translation table for the jargon (`T1.d`, `box verdict`, `Mixed`,
|
||||
`violations`).
|
||||
|
||||
### Final state
|
||||
|
||||
- 8/8 archetypes live and bit-identical (115 000-sample field digest unchanged, eight equivalences).
|
||||
- 14/14 tests green through `871ca19`, `violations` 0 everywhere.
|
||||
- **39 % of tiles skipped in the running game**, from 0 % that morning.
|
||||
|
||||
⚠️ **`4d33321` (boundary fold) and `91585ea` (warning demotion) are built but NOT re-verified** — the
|
||||
suite has not been re-run and the counters not re-read since. Recorded in the handoff with the exact
|
||||
two checks and the instruction to `git revert 4d33321` if either fails; the 39 % does not depend on
|
||||
it.
|
||||
|
||||
### Direction, agreed
|
||||
|
||||
**No Phase 3. No further refactoring.** The old `switch` stays as the oracle. Next work is whatever
|
||||
Jahni can see — `fable-idea.md`, F7 set-pieces, F9 audio. The handoff closes on the question that
|
||||
should have been asked in July: *what do you want the world to do that it doesn't?*
|
||||
|
||||
This log stays append-only and stays the record of what was actually built. It ends here for the
|
||||
operator stack.
|
||||
|
||||
@@ -1107,7 +1107,8 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// seule ne dit rien de la production. Copier-coller le balayage aurait donné deux critères qui
|
||||
// divergent ; c'est un paramètre, pas un doublon.
|
||||
auto RunTileScan = [&](const FVoxelOpStack& S, const FStrateGenerationParams& TP,
|
||||
const FVoxelOpContext& TCtx, const TCHAR* Label)
|
||||
const FVoxelOpContext& TCtx, const TCHAR* Label,
|
||||
bool bZeroProvedIsExpected)
|
||||
{
|
||||
int32 NumProved = 0, NumMixed = 0, NumSolid = 0, NumAir = 0;
|
||||
int32 NumBruteSamples = 0, NumViolations = 0;
|
||||
@@ -1290,7 +1291,17 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
|
||||
if (NumProved == 0)
|
||||
{
|
||||
AddWarning(FString::Printf(
|
||||
// ⚠️ UN AVERTISSEMENT QUI SE DÉCLENCHE À CHAQUE RUN ET VEUT DIRE « tout va bien »
|
||||
// N'EST PAS UN AVERTISSEMENT — c'est du bruit qui apprend à ignorer les vrais.
|
||||
// Sur la fixture dense, 0 prouvé est la SEULE réponse arithmétiquement possible
|
||||
// (les sphères de cull couvrent ce monde 3,6x) : c'est une info. En production
|
||||
// défauts, 0 prouvé serait une VRAIE régression (11 aujourd'hui) : ça reste un
|
||||
// avertissement.
|
||||
// A warning that fires every run and always means "this is fine" is noise that
|
||||
// trains the reader to ignore warnings. Zero proved is the only possible answer on
|
||||
// the dense fixture (info); on production defaults it would be a real regression
|
||||
// from 11 (warning).
|
||||
const FString ZeroMsg = FString::Printf(
|
||||
TEXT("[%s] No tile was proved, so the brute force verified nothing -- it has no ")
|
||||
TEXT("verdict to contradict. Do NOT re-derive the cause: read the two lines above, ")
|
||||
TEXT("which name the operator and then the primitive class. ⚠️ On the DENSE ")
|
||||
@@ -1299,7 +1310,10 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
TEXT("occupancy, so cull spheres cover that world ~3.6x over and no box can be ")
|
||||
TEXT("outside all of them. It is the 'production defaults' run that answers ")
|
||||
TEXT("whether real worlds have skippable rock."),
|
||||
Label));
|
||||
Label);
|
||||
|
||||
if (bZeroProvedIsExpected) { AddInfo(ZeroMsg); }
|
||||
else { AddWarning(ZeroMsg); }
|
||||
}
|
||||
|
||||
TestEqual(FString::Printf(
|
||||
@@ -1336,7 +1350,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// something, and that same densification makes tile-proving structurally impossible (room cull
|
||||
// radius ~= the lattice spacing, at 85% occupancy). Both are measured and both are brute-forced;
|
||||
// the dense one reporting ~0 is the correct answer, not a failure.
|
||||
RunTileScan(Stack, P, Ctx, TEXT("dense fixture"));
|
||||
RunTileScan(Stack, P, Ctx, TEXT("dense fixture"), /*bZeroProvedIsExpected*/ true);
|
||||
|
||||
{
|
||||
FStrateGenerationParams SparseP = P;
|
||||
@@ -1352,7 +1366,7 @@ bool FVoxelForgeOpStackTunnelTest::RunTest(const FString& Parameters)
|
||||
// et c'est exactement ce que le contrôle 3 vérifie : l'empreinte de params est dans la clé,
|
||||
// donc l'une ne peut pas se servir les salles de l'autre. Le jour où ce contrôle tombe,
|
||||
// cette ligne-ci devient fausse en même temps — elles se surveillent mutuellement.
|
||||
RunTileScan(SparseStack, SparseP, Ctx, TEXT("production defaults"));
|
||||
RunTileScan(SparseStack, SparseP, Ctx, TEXT("production defaults"), /*bZeroProvedIsExpected*/ false);
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
|
||||
@@ -740,6 +740,11 @@ namespace
|
||||
const bool bIntegerXY = WorldX == FMath::FloorToFloat(WorldX)
|
||||
&& WorldY == FMath::FloorToFloat(WorldY);
|
||||
FColumn* MemoColumn = &DirectColumn;
|
||||
// ⚠️ La boîte acquise doit survivre au `if` : le drapeau `Computed` n'est posé qu'APRÈS
|
||||
// le calcul, plus bas, hors de cette portée. Non nul ⇔ chemin XY entier.
|
||||
// The acquired box must outlive the `if`: the `Computed` flag is only set AFTER the
|
||||
// column is computed, further down and outside this scope. Non-null <=> integer path.
|
||||
FColumnBox* AcquiredBox = nullptr;
|
||||
int32 CI = 0;
|
||||
bool bNeedsCompute = true;
|
||||
|
||||
@@ -751,6 +756,7 @@ namespace
|
||||
// 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);
|
||||
AcquiredBox = &Box;
|
||||
|
||||
CI = (IY - Box.BaseY) * FColumnBox::Dim + (IX - Box.BaseX);
|
||||
MemoColumn = &Box.Cols[CI];
|
||||
@@ -829,7 +835,7 @@ namespace
|
||||
if (Slope > KINDA_SMALL_NUMBER) { C.DirX = GX / Slope; C.DirY = GY / Slope; }
|
||||
}
|
||||
|
||||
if (bIntegerXY) { Box.Computed[CI] = true; }
|
||||
if (AcquiredBox) { AcquiredBox->Computed[CI] = true; }
|
||||
}
|
||||
return *MemoColumn;
|
||||
}
|
||||
|
||||
@@ -2725,8 +2725,8 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (CX * CX + CY * CY <= Reach * Reach) bCanSolid = false;
|
||||
}
|
||||
|
||||
// ── Catégorisation par Z de treillis. v1 : gap bedrock = solide ; SurfaceWorld = test
|
||||
// colonne ; tout le reste (intérieurs de caves, hors layout) = Mixed immédiat. ──
|
||||
// ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ;
|
||||
// SurfaceWorld = test colonne ; un slot cave opt-in = verdict de pile sur sa sous-boîte. ──
|
||||
struct FSurfSlot
|
||||
{
|
||||
int32 BotChunkZ = INT32_MAX; // identité du slot (borne basse de la strate, en chunks)
|
||||
@@ -2753,17 +2753,31 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
int32 NumSlots = 0;
|
||||
|
||||
// ── T1.d GÉNÉRIQUE : la pile d'opérateurs classe les archétypes de CAVE ──
|
||||
// Ces trois-là suivent le slot de cave que la tuile touche. Le verdict de la pile porte sur la
|
||||
// BOÎTE ENTIÈRE, pas sur un z, donc il ne peut être calculé qu'après la boucle — et il n'est
|
||||
// valable que si la tuile ne touche QUE ce slot-là (voir la garde `bAnyNonCave`).
|
||||
// Ces valeurs suivent l'UNIQUE slot de cave que la tuile touche. La pile classera seulement la
|
||||
// sous-boîte Z de ses échantillons ; les catégories gap/surface/hors-layout plient séparément
|
||||
// leurs hypothèses dans `bCanSolid` / `bCanAir`.
|
||||
// These values track the ONE cave slot touched by the tile. The stack classifies only its
|
||||
// sampled Z sub-box; gap/surface/out-of-layout fold their hypotheses separately.
|
||||
int32 CaveBotChunkZ = INT32_MAX; // identité du slot de cave (borne basse, en chunks)
|
||||
int32 CaveRepChunkZ = 0;
|
||||
bool bAnyCave = false;
|
||||
bool bAnyNonCave = false; // gap ou SurfaceWorld dans la même tuile ⇒ on abandonne
|
||||
int32 CaveMinZ = MAX_int32;
|
||||
int32 CaveMaxZ = MIN_int32;
|
||||
bool bAnyCave = false;
|
||||
bool bAnyGap = false;
|
||||
bool bAnySurface = false;
|
||||
bool bAnyOutOfLayout = false;
|
||||
|
||||
int32 MemoChunkZ = INT32_MAX;
|
||||
int32 MemoCat = -1; // 0 = gap, 1 = surface, 2 = cave (pile d'opérateurs)
|
||||
int32 MemoCat = -1; // 0 = gap, 1 = surface, 2 = cave (pile), 3 = hors layout
|
||||
int32 MemoSlotIdx = -1;
|
||||
|
||||
// « Ce chunk Z appartient-il à une strate ? » sous forme publique : `FindSlotIndexForChunkZ`
|
||||
// est `protected`, `GetStrateChunkZBounds` rend false pour exactement le même cas.
|
||||
auto VF_ChunkZHasSlot = [&](int32 Z) -> bool
|
||||
{
|
||||
int32 UnusedTopCZ = 0, UnusedBotCZ = 0;
|
||||
return StrateManager->GetStrateChunkZBounds(Z, UnusedTopCZ, UnusedBotCZ);
|
||||
};
|
||||
for (int32 g = -1; g <= GridDim; ++g)
|
||||
{
|
||||
const int32 Zi = OriginVoxels.Z + g * Step;
|
||||
@@ -2775,7 +2789,43 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (StrateManager->IsGapChunk(CC))
|
||||
{
|
||||
MemoCat = 0;
|
||||
bAnyNonCave = true;
|
||||
bAnyGap = true;
|
||||
}
|
||||
//=================================================================
|
||||
// ⛔ HORS LAYOUT = AIR CONSTANT. C'ÉTAIT LE BLOCAGE DE T1.d.
|
||||
//=================================================================
|
||||
// `GetGeneratorTypeForChunk` rend `TunnelNetwork` pour tout chunk hors de la pile de
|
||||
// strates (« le chemin de repli produit de la roche de toute façon » — CE COMMENTAIRE
|
||||
// EST FAUX) et `IsGapChunk` rend false au-dessus du sommet (« open air, NOT a gap »).
|
||||
// Résultat : chaque tuile touchant l'air libre au-dessus du monde entrait dans la
|
||||
// BRANCHE DE CAVE, n'y trouvait aucun slot, et abandonnait — mesuré en jeu à 83 % des
|
||||
// tuiles classées (`Cave Bail Not Op Stack No Layout` = 1.58 / 1.90).
|
||||
//
|
||||
// La vérité est dans `GetGenerationParams` : hors layout il rend `BaseDensity = -1`,
|
||||
// `RoomDensity = 0`, `WormStrength = 0` — un champ CONSTANT, donc de l'air, sans salle
|
||||
// ni ver pour le percer. Une telle tuile est prouvable sans échantillonner.
|
||||
//
|
||||
// Out-of-layout is a CONSTANT AIR field, not a cave archetype. Every tile touching the
|
||||
// open air above the world was being routed into the cave branch and bailing there.
|
||||
// `GetStrateChunkZBounds` (PUBLIC) rend false exactement quand `FindSlotIndexForChunkZ`
|
||||
// rend -1 — ce dernier est `protected`, et cette fonction l'utilise déjà deux fois pour
|
||||
// la même question. Pas de nouvelle surface d'API pour un prédicat qui existe.
|
||||
// GetStrateChunkZBounds is the public form of "has a layout slot"; the index accessor
|
||||
// is protected and this function already uses the bounds call twice for the same test.
|
||||
else if (!VF_ChunkZHasSlot(ChunkZ))
|
||||
{
|
||||
MemoCat = 3;
|
||||
bAnyOutOfLayout = true;
|
||||
|
||||
// Les disturbances sont appliquées APRÈS la densité d'archétype et peuvent AJOUTER
|
||||
// de la roche (ponts, arêtes). Même prudence que les branches gap et cave : si
|
||||
// l'une peut agir ici, on ne prouve rien. Les chasms ne font que creuser ⇒ ils ne
|
||||
// menacent pas un verdict d'air.
|
||||
const FStrateDisturbanceParams DOut = StrateManager->GetDisturbanceParamsForChunk(CC);
|
||||
if (DOut.BridgeDensity > 0.0f || DOut.RidgeDensity > 0.0f)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
}
|
||||
else if (StrateManager->GetGeneratorTypeForChunk(CC) == ECaveGeneratorType::SurfaceWorld)
|
||||
{
|
||||
@@ -2817,7 +2867,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) bCanAir = false;
|
||||
}
|
||||
MemoCat = 1;
|
||||
bAnyNonCave = true;
|
||||
bAnySurface = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2865,12 +2915,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
int32 CaveTopCZ = 0, CaveBotCZ = 0;
|
||||
if (!StrateManager->GetStrateChunkZBounds(ChunkZ, CaveTopCZ, CaveBotCZ))
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent);
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
|
||||
return EVoxelTileClass::Mixed; // hors layout
|
||||
}
|
||||
if (CaveBotChunkZ != INT32_MAX && CaveBotChunkZ != CaveBotCZ)
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent);
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots);
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
CaveBotChunkZ = CaveBotCZ;
|
||||
@@ -2882,12 +2932,23 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
|
||||
if (MemoCat == 2)
|
||||
{
|
||||
// Rien par z : la pile répond pour la boîte entière, après la boucle.
|
||||
// La pile répond après la boucle, sur la sous-boîte Z contenant exactement les
|
||||
// échantillons cave (XY reste la boîte complète du treillis).
|
||||
CaveMinZ = FMath::Min(CaveMinZ, Zi);
|
||||
CaveMaxZ = FMath::Max(CaveMaxZ, Zi);
|
||||
}
|
||||
else if (MemoCat == 0)
|
||||
{
|
||||
bCanAir = false; // bedrock du gap = solide (le carve des passages est déjà gardé)
|
||||
}
|
||||
else if (MemoCat == 3)
|
||||
{
|
||||
// Hors layout = air constant (BaseDensity = -1, aucune salle, aucun ver). L'hypothèse
|
||||
// « tout solide » meurt ; « tout air » survit. Les passages et la spine ne font que
|
||||
// creuser — ils sont déjà gardés plus haut et ne peuvent pas rendre ce z solide.
|
||||
// Out of layout = constant air: AllSolid dies, AllAir survives.
|
||||
bCanSolid = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
FSurfSlot& S = Slots[MemoSlotIdx];
|
||||
@@ -2918,23 +2979,27 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
// DENSITÉ de cette tuile — même fabrique, mêmes params, même drapeau.
|
||||
if (bAnyCave)
|
||||
{
|
||||
// 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.
|
||||
if (bAnyNonCave)
|
||||
// Diagnostic de PRÉSENCE avant les gardes : le signal reste visible même si le pliage rend
|
||||
// finalement AllSolid/AllAir. Ces compteurs ne sont pas exclusifs entre eux sur une tuile
|
||||
// très haute ; chacun répond exactement à « cette catégorie était-elle aussi présente ? ».
|
||||
// Presence diagnostics run before the guards, so a successful fold cannot hide the mix.
|
||||
// They are not mutually exclusive for a very tall tile; each answers one exact question.
|
||||
if (bAnyOutOfLayout || bAnyGap || bAnySurface)
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailMixedContent);
|
||||
return EVoxelTileClass::Mixed;
|
||||
if (bAnyOutOfLayout) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixOutOfLayout); }
|
||||
if (bAnyGap) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixGap); }
|
||||
if (bAnySurface) { INC_DWORD_STAT(STAT_VoxelForgeCaveMixSurfaceWorld); }
|
||||
}
|
||||
|
||||
const FIntVector RepCC(0, 0, CaveRepChunkZ);
|
||||
const ECaveGeneratorType CaveType = StrateManager->GetGeneratorTypeForChunk(RepCC);
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// ⚠️ LA GARDE QUI COMPTE : LES PARAMS DOIVENT ÊTRE LES MÊMES SUR TOUTE LA TUILE
|
||||
// ⚠️ LA GARDE QUI COMPTE : LES PARAMS DOIVENT ÊTRE LES MÊMES SUR TOUTE LA SOUS-BOÎTE CAVE
|
||||
//---------------------------------------------------------------------
|
||||
// `GetGenerationParams` et ses homologues BLENDENT les params dans les bandes de transition :
|
||||
// `Alpha` dépend du chunk Z pour `Gradient`, et du chunk XY EN PLUS pour `Interleaved`. Deux
|
||||
// chunks d'une même tuile peuvent donc porter des params différents — c'est le constat de
|
||||
// chunks d'une même sous-boîte peuvent donc porter des params différents — c'est le constat de
|
||||
// `AUDIT §C2`, confirmé par lecture le 2026-07-28 — et UNE pile ne peut pas représenter DEUX
|
||||
// champs. On construit donc les params pour CHAQUE coordonnée de chunk que la boîte touche et
|
||||
// on exige qu'ils soient identiques bit à bit.
|
||||
@@ -2943,7 +3008,12 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
// `Mixed` de trop. On se trompe du côté du CPU, jamais du côté du trou.
|
||||
const int32 CX0 = FloorDivC(MinX, CHUNK_SIZE), CX1 = FloorDivC(MaxX, CHUNK_SIZE);
|
||||
const int32 CY0 = FloorDivC(MinY, CHUNK_SIZE), CY1 = FloorDivC(MaxY, CHUNK_SIZE);
|
||||
const int32 CZ0 = FloorDivC(MinZ, CHUNK_SIZE), CZ1 = FloorDivC(MaxZ, CHUNK_SIZE);
|
||||
// IMPORTANT : les gardes restent complètes, mais seulement sur les chunks où le mesher
|
||||
// échantillonne réellement CETTE strate cave. Inclure gap/surface/hors-layout ici ferait
|
||||
// échouer la garde d'archétype avant de pouvoir plier leurs hypothèses indépendantes.
|
||||
// The guards stay exhaustive over the cave samples. Non-cave chunks are intentionally not
|
||||
// represented by this stack; their hypotheses were folded separately in the Z pass.
|
||||
const int32 CZ0 = FloorDivC(CaveMinZ, CHUNK_SIZE), CZ1 = FloorDivC(CaveMaxZ, CHUNK_SIZE);
|
||||
|
||||
// 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é.
|
||||
@@ -3046,7 +3116,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
}
|
||||
default:
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailParams);
|
||||
return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas arriver ici (bAnyNonCave)
|
||||
return EVoxelTileClass::Mixed; // SurfaceWorld ne peut pas être le type du slot cave
|
||||
}
|
||||
|
||||
bFirst = false;
|
||||
@@ -3082,9 +3152,9 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
}
|
||||
TileStack.PrepareChunk(OpCtx);
|
||||
|
||||
const FBox TileBox(FVector((float)MinX, (float)MinY, (float)MinZ),
|
||||
FVector((float)MaxX, (float)MaxY, (float)MaxZ));
|
||||
const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(TileBox, OpCtx);
|
||||
const FBox CaveBox(FVector((float)MinX, (float)MinY, (float)CaveMinZ),
|
||||
FVector((float)MaxX, (float)MaxY, (float)CaveMaxZ));
|
||||
const EVoxelTileClass StackVerdict = TileStack.ClassifyBox(CaveBox, OpCtx);
|
||||
if (StackVerdict == EVoxelTileClass::Mixed)
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailStackVerdict);
|
||||
@@ -3094,6 +3164,17 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (StackVerdict == EVoxelTileClass::AllSolid) { bCanAir = false; }
|
||||
else { bCanSolid = false; }
|
||||
|
||||
// Le verdict cave se plie avec gap=solide, hors-layout=air, seals surface=solide. Si les
|
||||
// deux hypothèses sont mortes ici, les catégories se contredisent : ce n'est PAS un échec
|
||||
// de borne de la pile ni une disturbance.
|
||||
// Fold the cave verdict with gap=solid, out-of-layout=air, and solid surface seals. If both
|
||||
// hypotheses die here, the categories conflict; this is not a stack-bound/disturbance bail.
|
||||
if (!bCanSolid && !bCanAir)
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailFoldConflict);
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// ⚠️ LES DISTURBANCES NE SONT PAS DANS LA PILE (`OPSTACK-DECOMPOSITION §10.2`) :
|
||||
// `GetDensityAt` les applique APRÈS, sur la densité déjà négatée. Un verdict qui les
|
||||
@@ -3104,20 +3185,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (D.ChasmDensity > 0.0f) { bCanSolid = false; }
|
||||
if (D.BridgeDensity > 0.0f || D.RidgeDensity > 0.0f) { bCanAir = false; }
|
||||
|
||||
if (bCanSolid == bCanAir)
|
||||
if (!bCanSolid && !bCanAir)
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeCaveBailDisturbance);
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
if (bCanSolid)
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid);
|
||||
}
|
||||
else
|
||||
{
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir);
|
||||
}
|
||||
return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
|
||||
}
|
||||
|
||||
// ── Balayage des colonnes XY sur le treillis exact du mesher (marge incluse). Une colonne
|
||||
@@ -3176,6 +3248,15 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
// Ici exactement UNE hypothèse doit survivre (chaque point testé en tue une ; les tuiles
|
||||
// sans point intérieur ont tué AllAir via gap/seal). Égalité = prudence → Mixed.
|
||||
if (bCanSolid == bCanAir) return EVoxelTileClass::Mixed;
|
||||
if (bAnyCave)
|
||||
{
|
||||
// Compte seulement les verdicts FINAUX qui sautent réellement une tuile. Une pile peut avoir
|
||||
// prouvé sa sous-boîte cave puis perdre l'hypothèse sur une colonne SurfaceWorld adjacente.
|
||||
// Count only final verdicts that actually skip a tile; a later surface column may still
|
||||
// invalidate the hypothesis proved for the cave sub-box.
|
||||
if (bCanSolid) { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackSolid); }
|
||||
else { INC_DWORD_STAT(STAT_VoxelForgeTilesOpStackAir); }
|
||||
}
|
||||
return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,13 @@ DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailMixedContent);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveMixGap);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailTwoCaveSlots);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailParams);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailStackVerdict);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailFoldConflict);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailDisturbance);
|
||||
DEFINE_STAT(STAT_VoxelForgeCaveBailNoStack);
|
||||
DEFINE_STAT(STAT_VoxelForgeColumnMemoHit);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Runtime strate layout generation and queries.
|
||||
|
||||
#include "VoxelStrateManager.h"
|
||||
#include "CoreGlobals.h" // GIsAutomationTesting — the opt-in diagnostic stays quiet under tests
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
|
||||
#include "VoxelCaveMorphology.h" // For VoxelSDF and VoxelHash
|
||||
@@ -149,11 +150,23 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ WARNING EN ÉDITEUR/JEU, JAMAIS EN TEST. Les tests `Determinism.*` construisent
|
||||
// DÉLIBÉRÉMENT un monde non opt-in — c'est leur oracle de comparaison — et le framework
|
||||
// d'automatisation compte un Warning comme un échec. Un diagnostic ne doit pas casser la suite
|
||||
// qu'il est censé éclairer. Le message reste écrit UNE fois : seule la verbosité change.
|
||||
// Warning in editor/game where it is actionable, never in tests: the Determinism.* tests build
|
||||
// a non-opted-in world ON PURPOSE as their comparison oracle, and the automation framework
|
||||
// treats a Warning as a failure. One message, two verbosities.
|
||||
const bool bQuietDiagnostic = GIsAutomationTesting;
|
||||
|
||||
if (NumOperatorStackDisabledCaves > 0)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning,
|
||||
const FString Summary = FString::Printf(
|
||||
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);
|
||||
|
||||
if (bQuietDiagnostic) { UE_LOG(LogTemp, Verbose, TEXT("%s"), *Summary); }
|
||||
else { UE_LOG(LogTemp, Warning, TEXT("%s"), *Summary); }
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -171,12 +184,15 @@ void UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
|
||||
continue;
|
||||
}
|
||||
|
||||
UE_LOG(LogTemp, Warning,
|
||||
const FString Line = FString::Printf(
|
||||
TEXT("[StrateManager] cave slot=%d name='%s' Z chunks=[%d to %d] bUseOperatorStack=false"),
|
||||
Slot.StrateIndex,
|
||||
*Slot.Definition->StrateName.ToString(),
|
||||
Slot.TopChunkZ,
|
||||
Slot.BottomChunkZ);
|
||||
|
||||
if (bQuietDiagnostic) { UE_LOG(LogTemp, Verbose, TEXT("%s"), *Line); }
|
||||
else { UE_LOG(LogTemp, Warning, TEXT("%s"), *Line); }
|
||||
}
|
||||
|
||||
CachedSeed = WorldSeed;
|
||||
|
||||
@@ -313,12 +313,12 @@ public:
|
||||
* (OriginVoxels, Step, CellsPerAxis) = les MÊMES arguments que GenerateMesh ; le verdict
|
||||
* porte sur le treillis exact que le mesher échantillonnerait (marge ±1 incluse).
|
||||
*
|
||||
* v1 : ne prouve que les chunks GAP (bedrock) et les strates SurfaceWorld — colonnes
|
||||
* terrain/plafond évaluées par le MÊME ComputeSurfaceColumn que le chemin densité (donc
|
||||
* bit-identiques), bandes de seal solides, gardes spine/passages/disturbances/diff.
|
||||
* Tout autre archétype (intérieur de caves) ⇒ Mixed. Worker-safe (lecture seule +
|
||||
* caches thread_local partagés avec GetDensityAt — un verdict Mixed laisse les colonnes
|
||||
* chaudes pour la génération qui suit).
|
||||
* GAP (bedrock), hors-layout (air constant) et SurfaceWorld plient leurs hypothèses par Z ;
|
||||
* SurfaceWorld évalue ses colonnes terrain/plafond avec le MÊME ComputeSurfaceColumn que le
|
||||
* chemin densité. Un unique slot cave opt-in peut ajouter le verdict conservatif de sa pile,
|
||||
* sous gardes d'archétype et de params bit-identiques. Spine/passages/disturbances/diff restent
|
||||
* des gardes conservatrices. Worker-safe (lecture seule + caches thread_local partagés avec
|
||||
* GetDensityAt — un verdict Mixed laisse les colonnes chaudes pour la génération qui suit).
|
||||
*/
|
||||
EVoxelTileClass ClassifyTile(const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis) const;
|
||||
|
||||
|
||||
@@ -18,9 +18,13 @@ DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Sole Slot"), STAT
|
||||
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 Mix Out Of Layout"), STAT_VoxelForgeCaveMixOutOfLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Gap"), STAT_VoxelForgeCaveMixGap, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Mix Surface World"), STAT_VoxelForgeCaveMixSurfaceWorld, STATGROUP_VoxelForge, VOXELFORGE_API);
|
||||
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Two Cave Slots"), STAT_VoxelForgeCaveBailTwoCaveSlots, 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 Fold Conflict"), STAT_VoxelForgeCaveBailFoldConflict, 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);
|
||||
|
||||
Reference in New Issue
Block a user