Merge pull request #14 from Fr0zka/cleanup/2026-09

Cleanup/2026 09
This commit is contained in:
Fr0zka authored and GitHub committed 2026-09-28 18:12:41 +02:00
commit 0b8fbc5591
105 files changed
+1101 -4712

No files matched your search

+6 -1
View File
@@ -12,8 +12,13 @@
# The guarded single-entry test harness is source, not generated output.
!/Tools/
!/Tools/**
/Tools/**/__pycache__/
# Closed / historical documents.
!/Docs/
!/Docs/**
!VoxelForge.uplugin
# Keep every design / doc markdown at any depth (AUDIT P1 — these were untracked)
# Keep every design / doc markdown at any depth
!*.md
+72 -85
View File
@@ -36,7 +36,7 @@ radius, **0.300** against the Maze corridor radius, **0.469/0.625** against shaf
**0.234** against the minimum island radius (**0.484** including its 6-voxel blend), and
**0.625/0.938** against slab column radius for floor/ceiling roughness. SurfaceWorld is a
heightfield, so its **0.047** ratio is against its 80-voxel relief scale, not a fictitious radial
feature. FlatPlain and CrystalChamber intentionally share `GetSlabDensity` and the same slab
feature. FlatPlain and CrystalChamber intentionally share `BuildSlabStack` and the same slab
parameter block; they cannot acquire separate scale geometry without a generator change.
This scale contract is informed by [Epic's Unreal level-blockout guidance](https://dev.epicgames.com/documentation/en-us/unreal-engine/designer-01-project-setup-and-level-blockout-in-unreal-engine), which recommends a player-sized reference and gives 2-3 m hall / 3-4 m height starting guidance. The
@@ -62,22 +62,23 @@ are not landing or box-verdict violations.
### 8.1 Archetypes (`ECaveGeneratorType`, VoxelStrateTypes.h)
Each archetype has its own param `USTRUCT` (on `UVoxelStrateDefinition`, EditCondition-gated
by `GeneratorType`) and its own density function in `VoxelGenerator.cpp`, dispatched by the
`switch` in `GetDensityAt`.
by `GeneratorType`) and its own **operator stack** builder in `VoxelDensityOpStack.cpp`, selected per
chunk by `VF_BuildOpStackForChunk` in `GetDensityAt` (the same factory `ClassifyTile` uses). The stack
is the only density path; a degenerate (zero-height) strate has none and is open air.
| Archetype | Params struct | Density fn | Idea |
| Archetype | Params struct | Stack builder | Idea |
|-----------|---------------|------------|------|
| TunnelNetwork | `FStrateGenerationParams` | `GetDensityWithParams` | rooms+tunnels (original) |
| FlatPlain / CrystalChamber | `FSlabGenerationParams` | `GetSlabDensity` | floor/ceiling void (original) |
| Maze | `FMazeGenerationParams` | `GetMazeDensity` | origin-directed spanning-tree corridors on a 3D lattice, with capped loop edges and a thread-local per-cell capsule cache |
| SurfaceWorld | `FSurfaceGenerationParams` | `GetSurfaceDensity` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, **F20 heightfield terrain ops** (`Surface|Ops` — all default off ⇒ byte-identical): **Cliff** (slope-gated steepening — where the analytic structural slope > `CliffSlopeThreshold`, push the height away from the local mean by `CliffSharpness` ⇒ gentle slopes become sheer walls/canyon faces that hug real steep ground, gentle areas untouched; 4 structural resamples only when enabled), **Terrace** (relief-gated plateau quantize + `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed) — pure per-column height remaps in the single oracle `ComputeSurfaceTerrainZ` (`SampleSurfaceStructuralZ` = pre-op raw height, re-sampled at an XY offset for Cliff's slope), biome-selected + border-blended for free via each biome's `SurfaceParams` + the height output-lerp; plus **phase-2 Overhang** (the first VOLUMETRIC op — real jutting shelves like a cliff lip): in `SurfaceDensityFromColumn`, for AIR voxels in the window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height and unioned in. Low in the window the shift is ~0 (borrows nearby low rock ⇒ stays air over the void); high up it reaches the far cliff (solid) ⇒ a shelf attached to the cliff, tapering out over the void with air underneath. Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill dir `(DirX,DirY)` are resolved once in `ComputeSurfaceColumn` — the gradient sampled at the REACH scale (`OverhangReach`) so a point out over the void can "see" the cliff to know which way is uphill — and cached on `FSurfaceColumn`. It's genuine 3D (per-voxel structural re-eval), so it's gated hard to steep overhang columns; the union only ADDS rock (never removes), capped at `TerrainZ+OverhangHeight`, so `ClassifyTile` forces Mixed only in `(TerrainZ, TerrainZ+OverhangMargin]` (upward-only; margin = max `OverhangHeight`) — a shelf never holes a trivially-skipped tile, and only the thin cliff-edge band of air tiles is woken (NOT far-field like phase-3 spikes/holes). Overhang undersides classify as ground rock by the F17 rule (down-facing but below `TerrainZ`). Known v1 limit: applies at all LODs (Step-agnostic) — may alias far; gate to fine tiles later. Beaches at water line, high sky-cap ceiling. The cap is shapeable terrain in its own right (`ComputeSurfaceCeiling`, `Surface|Sky` params: `CeilingUndulation` broad inverted hills/valleys, `CeilingRidgeStrength` hanging ridgelines, `CeilingRoughness`+freq fine bumps, `CeilingWarp*`) — defaults (strengths 0, freq 0.04) = old flat-ish cap. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) **Sky-cap vs ground is a PER-TRIANGLE surface class (F17), not a per-tile verdict**: the mesher classifies each unique vertex on the **worker** — only down-facing verts (`N.Z<-0.1`) pay a memoized `GetSurfaceHeightAt` column query; nearer `CeilSurf` ⇒ sky-cap, nearer `TerrainZ` ⇒ terrain overhang stays ground (a future SurfaceWorld cave roof — down-facing but below `TerrainZ` — also lands ground by the same rule; non-surface strates always ground). Triangles take the majority class of their 3 verts and are packed as **two contiguous index runs** (ground then cap, `FVoxelMeshData::NumCeilingTriangles`; skirts inherit their source triangle's run) → RMC **polygroups 0/1** → one **section per non-empty group** (`ApplyMeshToTile`, material slot = group index): slot 0 = strate `OverrideMaterial`/default (min-corner chunk), slot 1 = `CeilingMaterial` resolved at the tile's **TOP chunk** (mid as gap fallback, then min; fallback: ground material) — a coarse tile is 2^level chunks tall, so its min corner can sit in a lower strate/gap while the cap belongs to the strate above (this was the "far cap = ground material" residue), so the shadowless overhead rock is tinted separately instead of reading flat/bright. Shadow is **per section** via `FRealtimeMeshSectionConfig::bCastsShadow` (NOT the component `SetCastShadow` — RMC's proxy ignores the component flag; this is also why level≥2 far tiles only stopped casting once the section flag was wired): ground casts at level≤1, the cap section never casts, so the rock ceiling never shadows the terrain below it. History: v1 was a game-thread centre height-oracle (misclassified coarse far tiles → terrain material on the cap underside); v2 a whole-tile worker normal VOTE — which painted **mixed coarse tiles** (one far tile spanning terrain AND cap) entirely with the winner's material, and put sky material under terrain overhangs. The per-triangle class fixes both and is the identity channel caves/F8 will reuse. |
| VerticalShafts | `FVerticalShaftParams` | `GetVerticalShaftDensity` | full-height shafts + horizontal connectors + partial ledges |
| FloatingIslands | `FFloatingIslandParams` | `GetFloatingIslandDensity` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void |
| Underwater | `FStrateGenerationParams` + water | (reuses `GetDensityWithParams`) | tunnel rock + high water table |
| TunnelNetwork | `FStrateGenerationParams` | `BuildTunnelNetworkStack` (fused evaluator: `GetDensityWithParams`) | rooms+tunnels (original) |
| FlatPlain / CrystalChamber | `FSlabGenerationParams` | `BuildSlabStack` | floor/ceiling void (original) |
| Maze | `FMazeGenerationParams` | `BuildMazeStack` | origin-directed spanning-tree corridors on a 3D lattice, with capped loop edges and a thread-local per-cell capsule cache |
| SurfaceWorld | `FSurfaceGenerationParams` | `BuildSurfaceStack` | heightfield terrain: domain-warped continents+ridged mtns+detail, a low-freq **relief map** (`M`) that scales mountains/elevation for plains↔highland variety, **F20 heightfield terrain ops** (`Surface|Ops` — all default off ⇒ byte-identical): **Cliff** (slope-gated steepening — where the analytic structural slope > `CliffSlopeThreshold`, push the height away from the local mean by `CliffSharpness` ⇒ gentle slopes become sheer walls/canyon faces that hug real steep ground, gentle areas untouched; 4 structural resamples only when enabled), **Terrace** (relief-gated plateau quantize + `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed) — pure per-column height remaps in the single oracle `ComputeSurfaceTerrainZ` (`SampleSurfaceStructuralZ` = pre-op raw height, re-sampled at an XY offset for Cliff's slope), biome-selected + border-blended for free via each biome's `SurfaceParams` + the height output-lerp; plus **phase-2 Overhang** (the first VOLUMETRIC op — real jutting shelves like a cliff lip): in `FOverhangShelfMod`, for AIR voxels in the window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height and unioned in. Low in the window the shift is ~0 (borrows nearby low rock ⇒ stays air over the void); high up it reaches the far cliff (solid) ⇒ a shelf attached to the cliff, tapering out over the void with air underneath. Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill dir `(DirX,DirY)` are resolved once in `ComputeSurfaceColumn` — the gradient sampled at the REACH scale (`OverhangReach`) so a point out over the void can "see" the cliff to know which way is uphill — and cached on `FSurfaceColumn`. It's genuine 3D (per-voxel structural re-eval), so it's gated hard to steep overhang columns; the union only ADDS rock (never removes), capped at `TerrainZ+OverhangHeight`, so `ClassifyTile` forces Mixed only in `(TerrainZ, TerrainZ+OverhangMargin]` (upward-only; margin = max `OverhangHeight`) — a shelf never holes a trivially-skipped tile, and only the thin cliff-edge band of air tiles is woken (NOT far-field like phase-3 spikes/holes). Overhang undersides classify as ground rock by the F17 rule (down-facing but below `TerrainZ`). Known v1 limit: applies at all LODs (Step-agnostic) — may alias far; gate to fine tiles later. Beaches at water line, high sky-cap ceiling. The cap is shapeable terrain in its own right (`ComputeSurfaceCeiling`, `Surface|Sky` params: `CeilingUndulation` broad inverted hills/valleys, `CeilingRidgeStrength` hanging ridgelines, `CeilingRoughness`+freq fine bumps, `CeilingWarp*`) — defaults (strengths 0, freq 0.04) = old flat-ish cap. (`Surface|Macro` params = the cheap precursor to biomes; `ReliefStrength=0` ⇒ old uniform terrain.) **Sky-cap vs ground is a PER-TRIANGLE surface class (F17), not a per-tile verdict**: the mesher classifies each unique vertex on the **worker** — only down-facing verts (`N.Z<-0.1`) pay a memoized `GetSurfaceHeightAt` column query; nearer `CeilSurf` ⇒ sky-cap, nearer `TerrainZ` ⇒ terrain overhang stays ground (a future SurfaceWorld cave roof — down-facing but below `TerrainZ` — also lands ground by the same rule; non-surface strates always ground). Triangles take the majority class of their 3 verts and are packed as **two contiguous index runs** (ground then cap, `FVoxelMeshData::NumCeilingTriangles`; skirts inherit their source triangle's run) → RMC **polygroups 0/1** → one **section per non-empty group** (`ApplyMeshToTile`, material slot = group index): slot 0 = strate `OverrideMaterial`/default (min-corner chunk), slot 1 = `CeilingMaterial` resolved at the tile's **TOP chunk** (mid as gap fallback, then min; fallback: ground material) — a coarse tile is 2^level chunks tall, so its min corner can sit in a lower strate/gap while the cap belongs to the strate above (this was the "far cap = ground material" residue), so the shadowless overhead rock is tinted separately instead of reading flat/bright. Shadow is **per section** via `FRealtimeMeshSectionConfig::bCastsShadow` (NOT the component `SetCastShadow` — RMC's proxy ignores the component flag; this is also why level≥2 far tiles only stopped casting once the section flag was wired): ground casts at level≤1, the cap section never casts, so the rock ceiling never shadows the terrain below it. History: v1 was a game-thread centre height-oracle (misclassified coarse far tiles → terrain material on the cap underside); v2 a whole-tile worker normal VOTE — which painted **mixed coarse tiles** (one far tile spanning terrain AND cap) entirely with the winner's material, and put sky material under terrain overhangs. The per-triangle class fixes both and is the identity channel caves/F8 will reuse. |
| VerticalShafts | `FVerticalShaftParams` | `BuildVerticalShaftStack` | full-height shafts + horizontal connectors + partial ledges |
| FloatingIslands | `FFloatingIslandParams` | `BuildFloatingIslandStack` | asymmetric islands: flat land top + underside tapering to a point, lobed (domain-warped) outline, in an open void |
| Underwater | `FStrateGenerationParams` + water | (reuses `BuildTunnelNetworkStack`) | tunnel rock + high water table |
All density fns share the convention: internal **positive=solid**, apply origin spine →
vertical boundary seal → inter-strate passage tube + landing/floor → XY edge seal, then `return -Density` (MC:
negative=solid). The final MC-facing edge pass in `GetDensityAt` is repeated after disturbances so
All stacks share the convention: internal **positive=solid**, and `AppendStructuralPost` ends every
stack with origin spine → vertical boundary seal → inter-strate passage tube + landing/floor → XY
edge seal; `EvalMC` negates once (MC: negative=solid). The final MC-facing edge pass in `GetDensityAt` is repeated after disturbances so
the global rim cannot be reopened by a post-process; the diff layer remains the explicit player
override.
StrateManager provides params per chunk via `GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk`
@@ -127,12 +128,9 @@ threshold** before the law gate. No corridor or threshold tuning was added. The
gate is therefore retained as a design blocker; see `COMPOSER-NOTES.md §3.2b` and the validation log
in §10.
⚠️ **The `switch` above is no longer the only density path.** All 8 archetypes also exist as
**operator stacks**, selected per strate by
`bUseOperatorStack` and evaluated instead of the `switch`; each is bit-identical to the function in
its row. The design lives in `OPSTACK-PLAN.md` / `OPSTACK-DECOMPOSITION.md`, the symbol index in
`CODEMAP §3.2d` — not repeated here. What matters for *this* document: the archetype table describes
what the world IS, and both paths compute it.
The operator-stack design lives in `Docs/archive/OPSTACK-PLAN.md` /
`Docs/archive/OPSTACK-DECOMPOSITION.md`, the symbol index in `CODEMAP §3.2d` — not repeated here.
What matters for *this* document: the archetype table describes what the world IS.
### 8.2 (0,0) spine & hybrid connections
- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) builds a finite, rounded landing room with
@@ -230,8 +228,8 @@ they are never needed for the connectivity proof.
The evaluation window is exactly the lower nodes in `{-1,0}³`, an 8-node / 2×2×2 local halo. The
edge predicate checks the adjacent `+1` endpoint's parent locally, so it does not need a wider
collect region and cannot disagree at a chunk boundary. Both `GetMazeDensity` and
`MakeLatticeCorridorSource` rebuild those decisions in their thread-local per-cell cache; the
collect region and cannot disagree at a chunk boundary. `MakeLatticeCorridorSource` rebuilds those
decisions in its thread-local per-cell cache; the
voxel loop evaluates only cached capsule SDFs. This is deliberately independent of
`WorldRadiusVoxels` (which remains `0`) and the still-gated lateral-region system.
@@ -291,9 +289,11 @@ when close (separate HISMs → crossing the near boundary never touches the far
(per grid, sharing ONE `MaxConcurrentDecorationTasks` budget — each throttles against the other's in-flight count):
snapshot the update's decoration palette (built once on the GAME thread — biome, see below) then fire an async `UE::Tasks` march
(`BuildCellSpawns`, `BackgroundNormal`, capped at `MaxConcurrentDecorationTasks` in flight via `InFlightCells`).
**(3)** `ProcessDecoResults`: drain finished tasks' results (`Mpsc` queue → `ReadyResults`), epoch-guarded
(`DecoEpoch`, bumped on clear/strate-change so stale in-flight results are discarded) + range-checked, and
**apply (spawn) budgeted** (`MaxDecorationCellsPerFrame` — the only game-thread cost, SpawnActor/AddInstance).
**(3)** `ProcessDecoResults`: drain finished cell marches (`DecoResults`, `Mpsc`) and fold each into its
grid's region build (`MergeCellResult`; a result whose build is gone or whose `BuildId` no longer matches —
region cleared + re-marched — is discarded), then **apply completed regions budgeted**
(`MaxDecorationCellsPerFrame` regions/frame via `ApplyRegion` — the only game-thread cost; a region that left
range while it marched is dropped unbuilt).
`BuildCellSpawns` (worker) finds each column's surface point(s) and rolls the entries there (shared
`PlaceAtCrossing`). Candidate columns are **snapped to INTEGER voxel XY** (integer jitter) so the generator's
surface-column cache (T1.a, §8.10) applies — FRACTIONAL XY bypasses it and recomputes the noise-heavy
@@ -321,7 +321,7 @@ result's `Entries` snapshot. `DecorationMaxCrossingsPerColumn` caps cave columns
task count before UObject teardown (tasks read the Generator); `BeginDestroy` is the backstop. **Determinism:**
pure hash of (cell, column, crossing, entry, seed) + the density surface snap. **Decorations exist ONLY in
the player's current strate** (march is strate-bounded) → a strate change wipes + rebuilds them, and there
is **no cross-strate light bleed to cull** (the old `SetActiveStrate` light-culling pass is SUBSUMED — gone).
is **no cross-strate light bleed to cull**.
**Render paths:** `ActorClass` → real actors (lights/logic, pricey game-thread spawn); `InstancedMesh` → HISM
(no tick/actor/collision, emissive glows far), per-cell-per-entry. **Per-entry HISM tuning for dense groundcover**
(`FStrateDecoration`, only the InstancedMesh path): `CullDistance` (cm; 0 = no cull — the lever that makes dense
@@ -332,8 +332,8 @@ grass affordable: placed thickly, drawn only near → GPU cost bounded by area-w
`MinSlopeAngle` (lower companion to Max — band a prop onto a tilt range, e.g. 30..70 = slopes only),
`bWallExcludeOverhangs` (wall-only-upright: drop normals with N.Z < 0 so downward overhangs don't take wall props).
**Shared vocabulary (2026-07-06):** these gates + spawn/transform/render fields now live on `FPlacementProfile`
(embedded as `Profile` on `FStrateDecoration`, `FStrateLandmark`, and the coming `FStrateSetPiece`), so all three
scatter primitives are authored identically. Rotation unified to `RotationOffset` (fixed) + `RandomRotation`
(embedded as `Profile` on `FStrateDecoration` and `FStrateLandmark`), so both scatter primitives are
authored identically. Rotation unified to `RotationOffset` (fixed) + `RandomRotation`
(per-axis hash-random) — decoration's ctor defaults `RandomRotation.Yaw = 360` (full random heading, replacing the
old `bRandomYaw`/`MinYaw`/`MaxYaw`; banded yaw = offset + a smaller random range). Distribution is unchanged; the
exact per-instance yaw values reshuffle once (different hash mix).
@@ -345,7 +345,7 @@ SampleMoisture / SampleBiomeAt) on demand, so it's deterministic + worker-safe (
strate's already-resolved `FBiomeContext`, so no re-resolve). Opt-in per entry (empty list = zero cost);
wired into both the deco worker (`BuildCellSpawns`) and landmark placement (`SpawnLandmarkInstance`). This
is the shared core of the coming quest `FindFeature` locator (same predicate, run as an outward search) and
the anchor gate for `FStrateSetPiece`. The BP bridge `AVoxelWorld::GetVoxelSurfaceHeightAt` exposes the
the landmark anchor gate. The BP bridge `AVoxelWorld::GetVoxelSurfaceHeightAt` exposes the
trace-free deterministic ground/ceiling height so authored ruin/set-piece Blueprints self-arrange on the
real surface before it meshes. Next types (water-edge band, relief-peak local-max, slope) are additive.
**F7 COMPANIONS (relational decoration, `FDecoCompanion`):** each `FStrateDecoration` may list `Companions`
@@ -397,12 +397,12 @@ Static-mobility HISM re-caches its proxy — fine for player-paced digging.) Not
grass on a freshly-exposed ledge / regrowth after fill-back — both self-correct on the next natural re-stream. **Freeze note:** a huge set-piece mesh hitches on register (game-thread proxy/distance-field build —
NOT async-fixable; the spawn is game-thread by engine rule; the asset is a hard ref so already resident) →
mitigate ASSET-side (Nanite on the mesh, bake distance fields), optionally budget spawns across frames.
`ApplyDecoResult` buckets spawns per entry and builds each HISM with ONE batched `AddInstances`
(single cluster-tree build, set cull/shadow BEFORE `RegisterComponent`) — the game-thread hitch-killer for dense cells.
`ApplyRegion` buckets a region's spawns per mesh (all its cells merged) and builds each HISM with ONE batched
`AddInstances` (single cluster-tree build, set cull/shadow BEFORE `RegisterComponent`) — the game-thread
hitch-killer for dense cells.
**Per-entry tier (`StreamTier`, default Far)** picks NearGrid or FarGrid; radius + column spacing are PER-GRID
settings, never per-entry (a per-entry radius would re-introduce the in-place re-stream flicker — see the two-grid
rationale above). This REPLACES the vestigial `MaxLODLevel`; the dead `DecorationActorRadiusChunks` setting is
repurposed as `DecorationNearRadiusChunks`. `CullDistance` still bounds GPU draw on top (orthogonal to which grid
rationale above). `CullDistance` still bounds GPU draw on top (orthogonal to which grid
streams the entry). **No LOD area-density compensation** (placement is per real
surface point, density-stable with distance). **SpawnDensity semantics CHANGED** vs the old vertex scatter: it
rolls per column surface-point (not per mesh vertex) → expect a one-time density re-tune. **Settings
@@ -436,22 +436,23 @@ null entry = "evaluated, nothing placed" so it isn't retried). Deterministic (ha
Strate-bounded (wiped on strate change, like decorations). This is the real home for "rare prop at all
distances" — the job the decoration FarGrid could approximate at moderate range but not at extreme radius.
**(B) WATER — tile-driven, level-0 only (continuous plane, never pops).** `PopulateTileWater(tile)` in
`ApplyMeshToTile` (level-0 tiles), `ClearTileWater(tile)` in `UnloadTile`. One scaled engine plane
(`/Engine/BasicShapes/Plane`) per water-surface chunk (per-chunk-Z plane logic assumes a single chunk's
vertical span — hence level-0 only), keyed `TMap<FIntVector, UStaticMeshComponent*>` (reflected UPROPERTY).
Water Z: `bHasWater` + `WaterLevelRelative` → `StrateManager::GetWaterLevelWorldZForChunk`. Biome
`WaterMaterial` overrides `UVoxelStrateDefinition::WaterMaterial` (level stays strate-global).
**(B) WATER — ONE strate-global ocean plane that follows the player (`UpdateWater`, from `Tick`).** A single
scaled engine plane (`/Engine/BasicShapes/Plane`, no collision, no shadow) snapped to a coarse cell around the
player, so it only repositions when the player crosses a cell; terrain pokes through it, so it reads as water at
every LOD and to the horizon with no per-tile gaps (one draw). Water Z: `bHasWater` + `WaterLevelRelative` →
`StrateManager::GetWaterLevelWorldZForChunk` (no water in the current strate ⇒ plane hidden). Material:
`UVoxelStrateDefinition::WaterMaterial`. ⚠️ OPEN: `UVoxelBiomeDefinition::WaterMaterial` (the biome
content profile's water override, §8.14) is not read by the single-plane path — owner to decide whether
the biome override should come back (REVIEW_FINDINGS, *Owner decisions*).
`ClearAll`/`SetSeed` on `ChangeSeed`/regenerate clears both subsystems (decorations re-stream on the next
Tick via the INT_MIN sentinels). **Per-biome content (§8.14):** decorations resolve the dominant biome **PER COLUMN** on the worker
(`ResolveBiomeSampleAt` via the strate's `FBiomeContext`, box-cached → one rebuild per chunk footprint). The
update builds ONE flat decoration palette (every biome's list concatenated; `CurrentEntryBiome[i]` tags entry
update builds ONE flat decoration palette per grid (every biome's list concatenated; `EntryBiome[i]` tags entry
`i` with its context-biome index, -1 = strate fallback), and `PlaceAtCrossing` rolls only the entries the
column's biome owns. This replaced the old per-CELL `GetDominantBiomeAt` (one biome for a whole 8 m cell →
axis-aligned border snapping); borders now follow the warped-Voronoi field at column resolution, no straight
lines. Water still uses the chunk-centre `GetDominantBiomeAt` for its material. `Initialize` now also takes
`UVoxelSettings*` (for the grid tunables). `ContentMaxLevel` is now legacy/dead for decorations.
column's biome owns. Borders follow the warped-Voronoi field at column resolution, no straight lines (a
per-CELL biome would snap borders to the 8 m cell grid). `Initialize` also takes `UVoxelSettings*` (for the
grid tunables).
### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW)
`UVoxelAtmosphereManager` (owned by `AVoxelWorld`, gated by `bManageAtmosphere`).
@@ -521,7 +522,7 @@ flat floor. **There is no connector to the origin landing room at `(0,0)`.** The
those radial roads after a playtest (`d97373c`, 2026-09-08: they cut straight through everything);
room joins are flattened to walkable height instead. A source-fit landing therefore stays local to
the room it was fitted into (`GeneratePassages`, the `VF_BuildPassageLanding` calls).
⚠️ Open gap (Sol's code review, `DESIGN-SOL-2026-09-15-CODE.md` §1.3/§2.4): when the footing query
⚠️ Open gap (Sol's code review, `Docs/archive/DESIGN-SOL-2026-09-15-CODE.md` §1.3/§2.4): when the footing query
declines, the landing falls back to the historical random reach, and nothing proves that pose joins
the walk network. The canonical capability gate covers one scenario, not every seed or mix.
Proposed fix, not built: retry deterministically at other candidate spots instead of falling back.
@@ -554,8 +555,8 @@ The landing is part of the structural post, in the fixed order
after the passage carve and after MC-space disturbances as a support backstop, still before the
final XY edge seal. Room/floor Z is clamped inside the vertical seal; the XY edge seal is last and
wins over every landing or tube near the rim. Thus no landing can breach a strate boundary or the
world edge. The op-stack `FPassageCarveOp` calls the same manager operation as the legacy path,
including the floor, and `ClassifyTile` kills both uniform hypotheses around the bidirectional
world edge. The op-stack `FPassageCarveOp` calls the manager's `ApplyPassageModifier`, including
the floor, and `ClassifyTile` kills both uniform hypotheses around the bidirectional
landing floor (`Both` in the stack), so an air proof cannot delete support.
The query remains pure — it does not construct an operator stack, touch a cache, or call back into
@@ -621,8 +622,7 @@ driven by `EditorBrush*` props.
inner 3×3 shafts and their capsules for the per-voxel loop, while spatial culling selects cached
connectors. The complete ±2 parent and ±3 fallback windows plus connector reach are contained
by that halo, so no parent decision depends on the evaluation cell. No cell or pair hash, widened
neighbourhood scan, or tree construction may move into the per-voxel loop; the operator-stack
source follows the same cache contract.
neighbourhood scan, or tree construction may move into the per-voxel loop (`FShaftFieldSource`).
- **Maze field cache**: parent/loop decisions are rebuilt only when the thread-local cell, seed, or
loop parameters change. The rebuild evaluates the 24 canonical lower-node/axis edges in the
local `{-1,0}³` window and emits at most 24 capsules; the per-voxel loop performs no hash,
@@ -684,13 +684,13 @@ driven by `EditorBrush*` props.
position → seamless across chunk borders (both sides use identical pure samples). NO per-vertex
`GetDensityAt` (was ~6/vertex, often as costly as the whole grid). `ComputeGradientNormal` is now
unused. Only NORMALS changed vs the old path; geometry is identical.
- **Surface column cache (T1.a)** (`FSurfaceColumnCache` = LRU of `FSurfaceColumnBox`, `GetDensityAt`
SurfaceWorld branch): the heightfield + sky-cap + biome blend are a PURE function of (XY, seed,
- **Surface column cache (T1.a)** (density path: the surface stack's `FSurfaceColumnSource` memo;
`ClassifyTile`: `FSurfaceColumnCache` = LRU of `FSurfaceColumnBox`): the heightfield + sky-cap + biome blend are a PURE function of (XY, seed,
strate) — **ZERO Z dependence** (climate/Voronoi are pure-XY; surface params are per-strate constant
under Hard transitions) — yet sampled ~33× per column (once per Z grid-point). Cached per integer XY
(box-valid, like the SDF cache) and reused down the column. **Keyed by (XY box, StrateKey, Seed), NOT
ChunkZ** (`StrateKey = round(StrateBottomWorldZ)`, taken from the params so it can't disagree with
them) and held as a small **LRU of 6 boxes** so the WHOLE vertical view-distance stack — and XY
(box-valid, like the SDF cache) and reused down the column. **Keyed by XY box + strate identity + seed
(plus layout version / params fingerprint), NOT ChunkZ** (the strate identity is derived from
`StrateBottomWorldZ`, taken from the params so it can't disagree with them) and held as a small **LRU of 6 boxes** so the WHOLE vertical view-distance stack — and XY
neighbours the scheduler interleaves — share one another's heavy column noise instead of each
recomputing it ~once per vertical chunk (this was the dominant `GenerateMesh` cost: the same 2D
heightfield recomputed per altitude). It also makes pure-air / pure-solid chunks cheap (they hit the
@@ -764,28 +764,14 @@ driven by `EditorBrush*` props.
inside a strate band), then the OUTERMOST shell keeps generating tiles outward until it covers
the requested distance (`VF_OuterShell`, shared by `BuildDesiredTiles` and `IsTileInClipRange`
so the cull sees the same horizon; the ring's dz sweep is pre-clamped to the vertical strate
band). As MC tiles the ring cost grows with (distance/2^MaxClipLevel)² — which is why the ring
defaults to **F18 SHEETS** (`bFarSheetRing`): in an open strate the far field is exactly two
heightfields (TerrainZ + CeilSurf, per-column oracle), so the ring streams tiles at level
`MaxClipLevel + FarSheetSpanLevels` (one sheet = 2^span MC footprints per axis → 4-16× fewer
components) meshed by `GenerateSheetMesh` as two displaced grids — ground polygroup 0 / cap
polygroup 1, tags true by construction, same materials/UVs/F6 masks, ~3-6× cheaper gen per
area than band-cut MC. **XY hole**: a partially-covered sheet renders its whole footprint, so
the MC-covered box around the player (level-MaxClipLevel box, shrunk 1 tile for a seam-overlap
ring) is CUT out of the sheet at cell granularity (`SheetHole*Vox`, passed to
`GenerateSheetMesh`); when the player crosses a MaxClipLevel tile the hole moves and the
overlapping sheets re-queue via `BandRemeshQueue` — the shrink means the newly-cut area's MC
tiles were already desired one crossing earlier (loaded before uncovered). Sheet tiles take
the strate from the BAND (mid chunk): band unarmed
(inter-strate gap) ⇒ empty ring until landing; non-open strates ⇒ empty sheets (their far
ring was enclosed rock anyway); carved features (passages/spine/chasms/diff) don't show at
sheet distance — the MC shells keep them near. (A "step cap" variant — raising CoarseTileCells
per level so far levels keep a fine Step — was tried and rejected 2026-07-06.)
band). The ring is made of level-MaxClipLevel MC tiles, so its cost grows with
(distance/2^MaxClipLevel)². The far "sheet" ring (heightfield tiles past `MaxClipLevel`) was
dropped by the owner's decision; it no longer exists in the selector or the settings.
(A "step cap" variant — raising CoarseTileCells per level so far levels keep a fine Step — was
tried and rejected 2026-07-06.)
Trade-offs accepted: other strates simply don't render at far LOD (they're sealed/enclosed —
invisible except through passage mouths, which read as dark holes); passage tubes crossing the gap
are cut at coarse levels only (near levels mesh full).
`GetLODForChunk` / `LODToStep` / `IsChunkInRange` / `GetStrateChunkZBounds` and the
ViewDistance/LOD/strate-Z/ceiling settings are now DEAD/unused (left in place).
- **SKIRTS — LOD-seam crack filler** (`GenerateMesh`, after the cell loop; `VoxelSettings::bGenerateSkirts`
+ `SkirtCells`, wired onto the mesher at setup). Neighbouring shells mesh at different resolutions so
their iso-surfaces don't meet along the shared face → a thin see-through crack. After meshing, every
@@ -818,9 +804,8 @@ driven by `EditorBrush*` props.
drain; `VoxelSettings::MaxUnloadsPerFrame`): the cull APPROVES removals (strict load-before-unload) but
doesn't destroy in place — it queues them. `ProcessUnloadQueue` runs at most `MaxUnloadsPerFrame`
`UnloadTile`s/frame (scaled up to 4× with backlog, capped so a huge backlog can't re-spike). WHY: a
fast traversal culls a whole shell's worth of tiles in ONE frame, and each `UnloadTile` does
`DestroyComponent` + (level 0) `ContentManager::ClearChunk` → `Destroy()` of every decoration actor —
an unbudgeted burst = a game-thread spike ("stuff torn down behind you" at speed). Mesh APPLIES were
fast traversal culls a whole shell's worth of tiles in ONE frame, and each `UnloadTile` strips and
parks its component (T2.c pool) — an unbudgeted burst = a game-thread spike ("stuff torn down behind you" at speed). Mesh APPLIES were
already budgeted; this matches it for DESTROYS. Re-desired tiles are cancelled out of the queue (still
loaded → no reload). `PendingUnload` is cleared in `RegenerateAllChunks`/`EndPlay` (tiles already gone).
- **Collision only at LEVEL 0** (`ApplyMeshToTile`): `UpdateSectionConfig(..., Tile.Level==0)`. Far tiles
@@ -833,7 +818,7 @@ driven by `EditorBrush*` props.
= 0; `=1` restores it as an A/B instrument). Measured on the headless game path with nested
refinement off, the classifier plus its exact validation cost more than they saved. Mode 0 had
ready p95 182 vs 190-195 ms, generation p95 125-127 vs 139-141 ms, and 18% fewer density calls,
with 841/841 tiles triangle-identical (WORK-NEXT.md). Proving a 33³ core uniform costs almost
with 841/841 tiles triangle-identical (Docs/archive/WORK-NEXT.md). Proving a 33³ core uniform costs almost
as much as meshing 35³. The history below explains why it existed. Its soundness tests still
guard it.
- **Trivial-empty tile reject (T1.d) — v2 SHIPPED (2026-07-05); v1 was reverted 2026-06-26.**
@@ -844,9 +829,11 @@ driven by `EditorBrush*` props.
wasted) and gen throughput had become the felt gameplay limit ("standing waiting for generation").
**Why v1 failed & how v2 avoids it:** v1 used a GLOBAL analytic ceiling bound — not conservative when
the cap hangs low (`CeilingRoughness`/`RidgeStrength`) → holes in the roof. v2 makes NO amplitude
guesses: it evaluates `ComputeSurfaceColumn` (the SAME function as the density path, via the SHARED
`GSurfColCache`) **on the exact lattice the mesher would sample** (margin ring included) — same
functions + same inputs ⇒ the same floats ⇒ the verdict is exact at the lattice, not an estimate.
guesses: it evaluates `ComputeSurfaceColumn` (cached per thread in `GSurfColCache`) — the column
the surface stack's height operators reproduce (ground height pinned bit for bit by
`VoxelForge.OpStack.SurfaceHeightEquivalence`) — **on the exact lattice the mesher would sample**
(margin ring included) — same inputs ⇒ the same floats ⇒ the verdict is exact at the lattice, not
an estimate.
Per-z rules: gap chunk = solid; surface seal band (ApplyBoundarySeal inequalities, `BaseDensity>0`)
= solid; surface interior z: air ⇔ `TerrainZ ≤ z ≤ CeilSurf` (MC `D ≥ 0`); ANY other archetype /
out-of-layout chunkZ ⇒ `Mixed` (cave interiors are not provable in v1 of this classifier — a future
@@ -860,7 +847,7 @@ driven by `EditorBrush*` props.
A false `Mixed` only costs CPU; the code must NEVER emit a false AllSolid/AllAir (that's a hole).
Capture tiles (`bWantCapture`, density-volume shadow window) always generate — the volume wants the
grid even for uniform cells. A sparse ~5×5 column pre-pass exits Mixed fast on surface-crossing
tiles; a Mixed verdict leaves its columns warm in `GSurfColCache` for the GenerateMesh that follows.
tiles; the columns stay warm in `GSurfColCache` for the full pass and for neighbouring tiles.
- **Worker-built StreamSet (T1.f)** (`BuildTileStreamSet`, `LoadTile` task → `FChunkResult::Streams`):
the RMC vertex/index buffers (`FRealtimeMeshStreamSet`) are built ON THE GEN WORKER, not on the game
thread. The per-vertex builder loop was the dominant game-thread streaming cost (measured: game
@@ -981,9 +968,9 @@ change *anything*, e.g. frequencies, which scalar multipliers couldn't.)
- **Consumption — SURFACE (output-blend).** Per chunk, `CP_SurfaceBiomeParams[]` holds each biome's
resolved surface params (its override when `bOverrideTerrain` + GeneratorType matches, else the
strate's) with **structural fields forced from the strate** (Z bounds, seal, base density, water
level). Per voxel: `ResolveBiomeSampleAt` → dominant `PD` (+ neighbour `PN`); `GetSurfaceDensity`
computes `ComputeSurfaceTerrainZ` for `PD` and, in the border band, for `PN`, and **lerps the
resulting HEIGHTS**. Blending heights (not params) is seamless across *any* difference (frequencies
level). Per voxel: `ResolveBiomeSampleAt` → dominant `PD` (+ neighbour `PN`); the surface stack's
biome-blend height source (and `ComputeSurfaceColumn`) computes the terrain height for `PD` and, in
the border band, for `PN`, and **lerps the resulting HEIGHTS**. Blending heights (not params) is seamless across *any* difference (frequencies
included) — what per-param blend never could. `PD==PN`, weight 0 ⇒ bit-identical, no biomes.
- **Consumption — CAVES: structural overrides are NOT applied (determinism).** Rooms/tunnels are
decided over a wide COLLECT region spanning chunks (§8.4); making room params vary by region would
@@ -1117,7 +1104,7 @@ pending).** `AVoxelWorld::RegisterStreamingAnchor(Actor, Policy, RadiusChunks)`
(BlueprintCallable) add an actor to `StreamingAnchors`. `UpdateChunksAroundPosition` prunes dead anchors +
detects chunk crossings (rebuilds the desired set when any anchor crosses a level-0 boundary — same cadence
as player movement, coalesced into one rebuild). `AddAnchorDesiredTiles` (inside `BuildDesiredTiles`, after
the player clipmap + sheet ring) folds each anchor's Chebyshev box of **level-0** tiles into the SAME
the player clipmap) folds each anchor's Chebyshev box of **level-0** tiles into the SAME
`DesiredStamped`/`DesiredSorted` set (deduped vs the clipmap by stamp) → the existing delta cull releases an
anchor's tiles automatically when it moves away / unregisters. Zero cost when no anchors (empty loop). The box
defaults to a THIN shape (its chunk + 1 horizontal ring + 1 chunk below for ground safety, nothing above —
+2 -1
View File
@@ -29,7 +29,8 @@ try to run anything.
- **Determinism:** all randomness = hash of (coord, seed, strateIndex). No RNG state — same seed ⇒
same world. Player edits (diff layer) are the only non-deterministic overlay.
- **Async safety:** worker tasks only READ Generator/Mesher, must check `bShuttingDown`, and return via
`ProcessQueue` (must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
the result queues — `ProcessQueue` and its `CriticalProcessQueue` / `EditedProcessQueue` /
`NearProcessQueue` siblings (all must stay `EQueueMode::Mpsc`). `EndPlay` blocks on `ActiveTaskCount → 0`.
- **Carry the `Epoch`** through any new async path (stale results are dropped on mismatch).
- **Don't "optimize" the perf invariants** in ARCHITECTURE `§8.10` — the `thread_local` box-valid
caches, two-pass MC loop, SSE noise, and clipmap streaming are intentional.
+202 -195
View File
@@ -54,9 +54,12 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
▼
UVoxelGenerator::GetDensityAt(x,y,z) ← THE density entry point
│ asks StrateManager which strate/params/generator-type applies
├─ TunnelNetwork → GetDensityWithParams() (rooms+tunnels+ops+worms+seal+passages)
├─ builds the chunk's operator stack once per chunk (VF_BuildOpStackForChunk;
│ editor composer / cooked-season recipe stacks first) → every archetype
│ TunnelNetwork/Underwater → fused evaluator = GetDensityWithParams()
│ └─ VoxelCaveMorphology::BuildChunkCache + EvaluateSDFCached (room/tunnel SDF)
├─ FlatPlain / CrystalChamber → GetSlabDensity() (floor+ceiling+columns+seal+passages)
│ others → FVoxelOpStack::EvalMC (source → modifiers → structural posts)
├─ + disturbances + structural tail (landing floor, tunnel core, XY seal)
└─ + UVoxelDiffLayer::GetDensityOffset() (player carve/fill)
│
▼ FVoxelMeshData (verts/tris/uvs/normals)
@@ -118,27 +121,25 @@ remains `0`, and lateral regions remain gated off.
| `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). |
| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. |
| `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. |
| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. The former ambiguous `Cave Bail Not Op Stack` is split into `Sole Slot`, `Boundary Tile`, `No Layout`, and late `Recheck` counters, so each increment names one guard/context. |
| `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. Each `Cave Bail *` counter names one guard; `Cave Bail No Stack` counts slots with no buildable stack (no definition, or a degenerate strate). |
### 3.2 Foundational types — `Public/VoxelTypes.h` (no UClass, everyone includes it)
| Symbol | Line | Notes |
|--------|------|-------|
| `CHUNK_SIZE` (32), `CHUNK_SIZE_SQUARED`, `CHUNK_VOLUME` | 19-21 | Chunk dimensions. (64³ tried for fewer draws → reverted: streaming too bursty. fps is fixed render-side instead.) |
| `VOXEL_SIZE` (25.0f cm) | 23 | World scale. |
| `EVoxelFace` enum + `GetFaceDirection` / `GetFaceNormal` | 33-61 | 6 cube faces. |
| `WorldToChunkCoord` / `WorldToLocalCoord` / `ChunkToWorldPos` | 74-104 | Coord-space conversions (handle negatives via floor/positive-modulo). |
| `LocalToIndex` / `IndexToLocal` / `IsValidLocalCoord` | 107-131 | Flat-array 3D↔1D indexing. |
| `VoxelMath::IsFiniteFast` / `IsFinite` | ~24-53 | Exact IEEE bit test (`voxel.FastIsFinite` A/B switch). |
| `VoxelMath::DetSinCos` / `DetSin` / `DetCos` | ~63-195 | **The only sin/cos allowed where the world is decided** (2026-09-16, `10fd0b6`). Fixed double period reduction and float polynomial, no CRT: `FMath::Sin/Cos` are CRT `sinf/cosf`, whose FMA3 variant is picked by CPU. Pinned by `VoxelForge.Determinism.DetSinCos`; `-voxel.CrtFma3=0` flips the CRT path to prove it. |
| `SmoothStep01` | 140 | 3x²-2x³ — used everywhere for blends. |
| `VOXEL_NOISE_SCALE` (1.25f) | 147 | Rescales UE PerlinNoise3D to ~[-1,1]. |
| `EVoxelTileClass` enum (`Mixed`/`AllSolid`/`AllAir`) | — | T1.d verdict. **MOVED here from `VoxelGenerator.h` 2026-07-27** so `VoxelDensityOp.h` can share it without a UCLASS dependency. A false `AllSolid`/`AllAir` is a HOLE; a false `Mixed` only costs CPU. |
| `FVoxelMeshData` struct | 157-173 | Mesher output (Vertices/Triangles/UVs/Normals/**Colors**). Plain C++, not USTRUCT. `Colors` = F6 material masks (R=dominant biome palette, G=slope, B=border blend weight, A=neighbour biome palette). §8.15. |
| `CHUNK_SIZE` (32) | 211 | Chunk dimensions. 64³ would cut draw calls but makes streaming too bursty; fps is fixed render-side instead. |
| `VOXEL_SIZE` (25.0f cm) | 213 | World scale. |
| `EVoxelFace` enum | 266 | 6 cube faces. |
| `WorldToChunkCoord` | 287 | World cm → chunk coord (handles negatives via floor). |
| `VoxelMath::IsFiniteFast` / `IsFinite` | 24 / 45 | Exact IEEE bit test (`voxel.FastIsFinite` A/B switch). |
| `VoxelMath::DetSinCos` / `DetSin` / `DetCos` | 63 / 181 / 189 | **The only sin/cos allowed where the world is decided** (2026-09-16, `10fd0b6`). Fixed double period reduction and float polynomial, no CRT: `FMath::Sin/Cos` are CRT `sinf/cosf`, whose FMA3 variant is picked by CPU. Pinned by `VoxelForge.Determinism.DetSinCos`; `-voxel.CrtFma3=0` flips the CRT path to prove it. |
| `SmoothStep01` | 306 | 3x²-2x³ — used everywhere for blends. |
| `VOXEL_NOISE_SCALE` (1.25f) | 313 | Rescales UE PerlinNoise3D to ~[-1,1]. |
| `EVoxelTileClass` enum (`Mixed`/`AllSolid`/`AllAir`) | 231 | T1.d verdict. Lives here so `VoxelDensityOp.h` can share it without a UCLASS dependency. A false `AllSolid`/`AllAir` is a HOLE; a false `Mixed` only costs CPU. |
| `FVoxelMeshData` struct | 365-391 | Mesher output (Vertices/Triangles/UVs/Normals/**Colors**). Plain C++, not USTRUCT. `Colors` = F6 material masks (R=dominant biome palette, G=slope, B=border blend weight, A=neighbour biome palette). §8.15. |
### 3.2b Density operator stack contract — `Public/VoxelDensityOp.h` (plain C++, no UHT)
**Production path behind a per-strate opt-in** — the stack is built/evaluated by `GetDensityAt` for
all eight archetypes when `bUseOperatorStack` is enabled, while the legacy `switch` remains the
reference path for opt-out strates. See [OPSTACK-PLAN.md](OPSTACK-PLAN.md) for the decomposition.
**The production density path** — `GetDensityAt` builds and evaluates a stack for every strate of
all eight archetypes; there is no other path. See [Docs/archive/OPSTACK-PLAN.md](Docs/archive/OPSTACK-PLAN.md) for the decomposition.
| Symbol | Notes |
|--------|-------|
@@ -159,29 +160,24 @@ manager passage post, **moved here 2026-07-27** so the generator and
the operator stack share ONE copy. The XY helper is a radial smooth ramp in actor-space and is a
true no-op when `WorldRadiusVoxels == 0`; `VF_XYEdgeSealForcedMarginOverBox` is the shared sound
proof for `ClassifyBox` and T1.d's global shell skip. `VoxelGenerator.cpp` keeps same-named
`static FORCEINLINE` forwarders for the original three helpers; the new edge helper is called
directly. **Convention: INTERNAL (positive = solid); `VF_ApplyXYEdgeSealMC` is the output wrapper.**
`static FORCEINLINE` forwarders for `ApplyBoundarySeal` and `ApplyOriginSpine`; the passage and
edge helpers are called directly. **Convention: INTERNAL (positive = solid); `VF_ApplyXYEdgeSealMC` is the output wrapper.**
### 3.2d Operator stack — `Public/VoxelDensityOpStack.h` + `Private/VoxelDensityOpStack.cpp`
⚠️ **Feeds the game, behind a per-strate opt-in** (Phase 1 step 3). `GetDensityAt` builds the stack
in its per-chunk refetch block and evaluates it *instead of* the `switch` only when
`UVoxelStrateManager::UsesOperatorStackForChunk` says so — strate ticked `bUseOperatorStack` **and**
archetype in the ported list, which is now **all 8 of 8**: Maze, FlatPlain, CrystalChamber,
SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater. A strate that has not
ticked the box still takes the `switch`, unchanged — the flag is the only thing that switches paths.
**`ClassifyTile` IS wired now**, for CAVE archetypes only and behind the same per-strate opt-in:
where it used to `return Mixed` without a call, it builds the strate's stack through the *same*
factory `GetDensityAt` uses (`VF_BuildOpStackForChunk`) and folds `ClassifyBox`. SurfaceWorld and
bedrock gaps keep their hand-written proofs — the exact-lattice column test is better than any box
bound. Guards, all failing to `Mixed`: one cave slot per tile, no mixed cave/surface/gap tile, the
opt-in true on *every* chunk the box touches, the params **bit-identical** across every chunk coord
the box touches (blended transition bands make one stack unable to represent the tile — `AUDIT §C2`),
a 27-chunk-coord cap, and the disturbances folded in by hand since they are applied after the stack.
⚠️ **This IS the game's generation path.** `GetDensityAt` builds the stack in its per-chunk refetch
block for every strate — **all 8 archetypes**: Maze, FlatPlain, CrystalChamber, SurfaceWorld,
VerticalShafts, FloatingIslands, TunnelNetwork, Underwater. An editor composer candidate or a
cooked-season recipe supplies its own stack; otherwise (or if that stack cannot be built) the
archetype's native stack is used. A degenerate (zero-height) strate has no stack and is open air.
`UVoxelStrateManager::UsesOperatorStackForChunk` only says whether a slot has a known archetype.
**`ClassifyTile`**, for CAVE archetypes, builds the strate's stack through the *same* factory
`GetDensityAt` uses (`VF_BuildOpStackForChunk`) and folds `ClassifyBox`. SurfaceWorld and bedrock
gaps keep their hand-written proofs — the exact-lattice column test is better than any box bound.
Guards, all failing to `Mixed`: one cave slot per tile, no mixed cave/surface/gap tile, a stack on
*every* chunk the box touches, the params **bit-identical** across every chunk coord the box touches
(blended transition bands make one stack unable to represent the tile — `AUDIT §C2`), a
27-chunk-coord cap, and the disturbances folded in by hand since they are applied after the stack.
Brute-forced end to end by `VoxelForge.OpStack.ClassifyTileSoundness`.
⛔ Never run both paths in one world. **Comparing them IS legitimate now** — the ~1 ULP residue of
AUDIT §C10 is gone since `FPSemantics = Precise`, and all eight equivalence tests compare bit for
bit. They are port-correctness oracles, not fidelity checks: the acceptance bar is §2.6.1 (same seed
⇒ same world on every peer), which does not require resembling the pre-refactor world.
| Symbol | Role | Notes |
|--------|------|-------|
@@ -197,7 +193,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**: the proved FBM supremum `1.5` 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. |
| `FShaftFieldSource` (internal) | 1 | VerticalShafts' memoised inner 3×3 shaft/capsule source. Each rebuild uses a direct-indexed wider halo (default 9×9 tree-emission / 15×15 roll) so every emitted shaft has its complete 5×5 parent window plus capsule reach; the nearest strictly more-central shaft in ±2 cells is the deterministic parent, with a deterministic lower-origin ±3 fallback and a direct `(0,0)` spine fallback. Thus every shaft has a provable path to the spine. Existing `VoxelHash::Pair`/`Spacing*1.6` links remain additive texture/loops. Tree and spine capsules use a radius strictly above `sup|FBM|=1.5`, and tree Z is chosen in a ledge-free band; the structural post still owns the column and the ledge modifier keeps its mathematical axis boundary open. The source publishes a conservative interval covering both shafts and connector capsules; it does not answer for the ledge or converter. |
| `VoxelDensityOps::BuildSlabStack` | — | 6 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults, exactly as `GetSlabDensity` already had it. 8 archetypes → 7. |
| `VoxelDensityOps::BuildSlabStack` | — | 6 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults. |
| `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. `FSurfaceColumn` is deliberately trivial: all five outputs are written before the cache's `Computed` publish, so removing its default initialization removes a TLS constructor leaf without changing a value. |
| `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 4 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. |
| `VoxelDensityOps::BuildVerticalShaftStack` | — | 9 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`). Passes the runtime spine radius into `FShaftFieldSource`, which mirrors the generator's connector-only origin endpoint and roughness-safe spine capsule. The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. |
@@ -213,7 +209,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
| `FRoomGraphSource::LocalParams()` | — | **The per-room op override** (DECOMPOSITION §2's "no clean home"). Strate params + the nearest room's `UVoxelTerrainOpDefinition::ApplyTo`, memoised once per voxel and read by eleven modifiers. One op owns the shared state, the rest read it — the same pattern as `FOverhangShelfMod` ← `FSurfaceColumnSource`. The detail consumers use the propagated interval for their shared near/far gate; unknown remains conservative, and the per-room parameter override is never used to claim an unproved box identity. |
| `FIslandBlobSource` (internal) | 1 | Hash-placed tapered flat-top blobs, `SmoothMin`'d, in a **domain-warped XY frame** (the warp stays inside the op — see the deviation note vs DECOMPOSITION §7). SDF channel only. The source publishes an interval covering the full authored radius envelope (`max(MinRadius, MaxRadius)`), the two-axis warp·**√2** envelope, top surface and SmoothMin dip; the fill converter makes its own decision. **No lower Z bound exists** — a hairline thread of matter hangs below each island down its axis, so only the TOP may reject. |
| `VoxelDensityOps::BuildFloatingIslandStack` | — | 8 ops, and **the stack runs backwards**: void source + fill instead of rock source + carve, using the *same* classes with the opposite sign. Only the blob source is new. Reuse by **inversion** — a stronger result than reuse by identity, since it says the abstract axis (the density sign) is the right one. |
| `VoxelDensityOps::BuildMazeStack` | — | The 8-op Maze stack (4 structural posts included). Its lattice source mirrors the origin-directed tree + capped loops and rebuilds the 2×2×2 lower-node window once per cell; the per-voxel loop only evaluates cached capsules. If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): `GetMazeDensity` early-outs to air there and the stack has no such early-out by design — `GetDensityAt` falls back to the `switch`. |
| `VoxelDensityOps::BuildMazeStack` | — | The 8-op Maze stack (4 structural posts included). Its lattice source mirrors the origin-directed tree + capped loops and rebuilds the 2×2×2 lower-node window once per cell; the per-voxel loop only evaluates cached capsules. If this ever becomes one op, the refactor failed its own test (§2.5). Callers must skip it on a **degenerate strate** (top−bottom ≤ 0): the stack has no such early-out by design — `VF_BuildOpStackForChunk` refuses it and `GetDensityAt` returns air. |
#### Lateral region parent (Tier 4d)
@@ -252,29 +248,30 @@ is a **column** property, or one opaque op (`OPSTACK-PLAN §2.5`'s failure mode)
| `VoxelHeightOps::BuildSurfaceHeightStack` | 5 ops in `ComputeSurfaceTerrainZ`'s order — structural → cliff → terrace → layer lines → beach. **Order is not negotiable.** |
### 3.3 Chunk identity
`VoxelChunk.h` (the old `FVoxelChunk` coord wrapper) was DELETED — dead since the tile
redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
Tile identity lives in `FVoxelTileKey` (VoxelTypes.h:326).
### 3.4 Settings — `Public/VoxelSettings.h`
`UVoxelSettings : UPrimaryDataAsset` — the single tuning asset assigned on `AVoxelWorld`.
| Group | Fields (line) |
|-------|---------------|
| Streaming | `ViewDistanceXY=16`, `ViewDistanceUp/Down=5`, `MaxConcurrentTasks=16`, `MaxMeshAppliesPerFrame=4` (defaults — actual values live on the data asset) |
| Clipmap | `ClipRadius`, `MaxClipLevel=5`, `FullResClipLevels`, `CoarseTileCells`, `RenderDistanceChunks=768` (measured ship defaults; custom horizontal reach: the outermost shell keeps generating until it covers this many chunks; 0 = off), `bFarSheetRing=false` by default, plus `FarSheetSpanLevels` (F18 — the optional render-distance ring streams per-surface SHEETS: level MaxClipLevel+span heightfield tiles instead of MC, see `GenerateSheetMesh`), skirts, `LODOctaveDrop` (T2.b octave drop on coarse tiles — 0 = off/byte-identical). Existing serialized data assets retain their authored values until deliberately migrated. |
| Clipmap | `ClipRadius`, `MaxClipLevel=5`, `FullResClipLevels`, `CoarseTileCells`, `RenderDistanceChunks=768` (measured ship defaults; custom horizontal reach: the outermost shell keeps generating until it covers this many chunks; 0 = off), skirts, `LODOctaveDrop` (T2.b octave drop on coarse tiles — 0 = off/byte-identical). Existing serialized data assets retain their authored values until deliberately migrated. |
| World bounds | `WorldRadiusVoxels=8192` and `EdgeSealThickness=64` (actor-space XY radial shell; radius 0 = true legacy no-op) |
| Lighting | `bEnableDensityVolume` + DensityVolume* tunables (§3.11 density clipmap / mini-sun shadows) |
| Rendering | `VoxelMaterial` (61) |
| Rendering | `VoxelMaterial` (377) |
| Strates | Optional cooked `Season`; effective seed/spine/radius/season accessors read it when assigned. Otherwise `Seed`, `CurrentSeason=1`, `StratePool`, `FixedStrates`, and `TotalStrates=10` retain the authored path. |
| Carving budget | `MaxModifications=0` (97), `MaxBrushRadius=15` (102), `MaxTotalVolume=0` (107). 0 = unlimited. |
| Carving budget | `MaxModifications=0` (449), `MaxBrushRadius=15` (454), `MaxTotalVolume=0` (459). 0 = unlimited. |
### 3.5 World orchestrator — `Public/VoxelWorld.h` + `Private/VoxelWorld.cpp`
`AVoxelWorld : AActor` — owns everything, drives streaming. Also `FChunkResult` struct
(VoxelWorld.h:35) = async task payload (coord, chunk, meshdata, LOD, **Epoch**, request timing,
(VoxelWorld.h:99) = async task payload (coord, chunk, meshdata, LOD, **Epoch**, request timing,
worker-built stream geometry counts/bytes, desired-set epoch and obsolete/cancellation state).
**Owned objects (UPROPERTY):** `Settings`, `Generator`, `Mesher`, `StrateManager`,
`DiffLayer` (VoxelWorld.h:55-78). **Storage:** `Chunks` map, `ChunkMeshes` map,
`ChunkLODs`, `ProcessQueue` (TQueue), `PendingChunkCoord` (TSet) (VoxelWorld.h:85-312).
`DiffLayer`, `ContentManager`, `AtmosphereManager`, `DensityVolume` (VoxelWorld.h:188-228).
**Storage:** `LoadedTiles` (TSet), `TileComponents` map + `TileComponentPool` (VoxelWorld.h:260-308),
the four MPSC result queues `CriticalProcessQueue` / `EditedProcessQueue` / `NearProcessQueue` /
`ProcessQueue`, and `PendingTiles` (TSet) (VoxelWorld.h:908-912).
**Async state:** `bShuttingDown`, `ActiveTaskCount` (atomics), `GenerationEpoch`, and the
game-thread `PendingTileCancellation` map whose per-request atomic flags are read-only on workers.
@@ -293,45 +290,45 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
| Method | .cpp line | Role |
|--------|-----------|------|
| `AVoxelWorld()` ctor | 12 | Enables Tick. |
| `RegenerateAllChunks()` | 21 | Bumps epoch, unloads all → Tick reloads. CallInEditor button. |
| `AVoxelWorld()` ctor | 176 | Enables Tick. |
| `RegenerateAllChunks()` | 530 | Bumps epoch, unloads all → Tick reloads. CallInEditor button. |
| `ValidateDeterminism()` | — | **F2 CallInEditor button (PIE)**: re-samples boundary points under left- vs right-chunk cache warm-ups + a same-alignment repeat; any non-zero delta = window-invariance regression (§8.4). Run after every "bit-identical" hot-path refactor. |
| `GetMaxConcurrentTasks()` | — | T2.d — asset `MaxConcurrentTasks` capped to logical cores − 2 (all three budget checks use it). |
| `PostEditChangeProperty` | 45 | Editor live-edit hook. |
| `OnObjectModifiedInEditor` | 58 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
| `EndPlay` | 140 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
| `BeginPlay` | 177 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
| `Tick` | 220 | `UpdateChunksAroundPosition(player, heading)` + `ProcessPendingChunks()`; the clean LOD0 latency summary is flushed at `EndPlay`. |
| `GetPlayerPosition` | 231 | Pawn position or zero. (`GetLODForChunk`/`LODToStep`/`IsChunkInRange` removed — dead since the clipmap.) |
| `ProcessPendingChunks` | 301 | Drains ProcessQueue under per-frame budget; reads the per-request cancellation token before removing its map entry, drops obsolete/aborted results, then applies each live result via `ApplyTileResult`. |
| `PostEditChangeProperty` | 1000 | Editor live-edit hook. |
| `OnObjectModifiedInEditor` | 1022 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
| `EndPlay` | 1118 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
| `BeginPlay` | 1429 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
| `Tick` | 4395 | `UpdateChunksAroundPosition(player, heading)` + `ProcessPendingChunks()`; the clean LOD0 latency summary is flushed at `EndPlay`. |
| `GetPlayerPosition` | 4831 | Pawn position or zero. |
| `ProcessPendingChunks` | 5856 | Drains ProcessQueue under per-frame budget; reads the per-request cancellation token before removing its map entry, drops obsolete/aborted results, then applies each live result via `ApplyTileResult`. |
| `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale generation epochs and keys no longer desired, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). |
| `FVoxelStackSampler` / `FScopedVoxelStackRegistration` (`VoxelStackSampler.h/.cpp`) | — | **Diagnostic raw stack sampler, off by default.** A sampler thread captures registered generation workers' stacks (`CaptureThreadStackBackTrace`) every N µs and tags each with the tile's LOD. It writes bounded raw PCs to CSV plus a symbolization-free summary under `Saved/`; symbol resolution is deferred until after process exit so the measured process never initializes/calls DbgHelp for this diagnostic. Lifecycle teardown is serialized and registration TLS is session-tagged. Registration is per tile TASK in `LoadTile` (and the export's tasks), never per sample; disabled costs one relaxed atomic load. Game `-voxel.SampleStacks=<us>`, commandlet `-samplestacks[=us]`. |
| Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **LOD-gated:** fused path at LOD ≥ `voxel.TileCacheWindowMinLOD` (3), op-stack at LOD ≥ `voxel.TileCacheWindowOpStackMinLOD` (4); lower LODs keep the 4-chunk worker window, because the tile window slowed LOD0 evaluation ~6%. Strates whose authored terrain ops exceed the proven 256-voxel extent (or are malformed/unloaded) fall back to the legacy window (`VF_ComputeTerrainOpEnvelope`). `voxel.SpatialIndex` is -1 auto (off at fused LOD0, on elsewhere), 0 or 1. **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). |
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): [ClassifyTile (T1.d) + exact validation, only when `voxel.OuterClassifierMode=1`; **default 0 since 2026-09-13**] → `GenerateMesh`/`GenerateSheetMesh` → `BuildTileStreamSet`; records vertex count and resident stream allocation bytes for the horizon trace. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): [ClassifyTile (T1.d) + exact validation, only when `voxel.OuterClassifierMode=1`; **default 0 since 2026-09-13**] → `GenerateMesh` → `BuildTileStreamSet`; records vertex count and resident stream allocation bytes for the horizon trace. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. |
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. |
| `UpdateChunksAroundPosition` | 362 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance — as level-MaxClipLevel MC tiles, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. |
| `UpdateChunksAroundPosition` | 6870 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance as level-MaxClipLevel MC tiles (`VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. |
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The `GetDensityAt` column at capsule feet defines the local air/solid surface; each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; support submission/readiness, travel, and holds are diagnostics. Result logs include the terminal density column and the first crossing below spawn height. |
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants) for the reconcile pass. |
| `VoxelClipmapDesiredTiles::Build` / `AlignedRingBounds` (`Private/VoxelClipmapDesiredTiles.h`) | — | The pure clipmap selector. **Every finer ring snaps outward to a child-pair boundary** (`AlignedRingBounds`, alignment 2; the last MC ring aligns to the sheet grouping width), so a coarser tile is wholly inside or wholly outside the finer ring: **no two desired tiles of different levels overlap** (a 7-wide odd ring used to leave one half-covered coarse tile per axis side, drawn over the fine one: 635 pairs at the owner config). Cost: LOD0 ring 343 → 512 tiles. Guarded by `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage`. Anchor tiles appended later are outside this guarantee. |
| `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants) for the reconcile pass. |
| `VoxelClipmapDesiredTiles::Build` / `AlignedRingBounds` (`Private/VoxelClipmapDesiredTiles.h`) | — | The pure clipmap selector. **Every finer ring snaps outward to a child-pair boundary** (`AlignedRingBounds`, alignment 2; the outermost ring needs none), so a coarser tile is wholly inside or wholly outside the finer ring: **no two desired tiles of different levels overlap** (an unaligned 7-wide odd ring leaves one half-covered coarse tile per axis side, drawn over the fine one: 635 pairs at the owner config). Cost: LOD0 ring 343 → 512 tiles. Guarded by `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage`. Anchor tiles appended later are outside this guarantee. |
| `ReconcileReadyTransitionVisibility` | — | Visibility-only pass while streaming is unsettled: hides a retired tile once all desired tiles over its footprint are loaded, reveals new tiles once no visible retired tile overlaps them. **Indexed** (dyadic ancestors + `DesiredTransitionDescendants`, visible retired tiles collected once per pass): moving p95 32 ms → 0.31 ms. `voxel.MeasureReadyTransitionVisibility=1` logs per-frame samples. |
| `RequestCollisionGateSupportTile` / `UpdatePawnCollisionGate` | — | The gate submits the pawn's level-0 support tile **immediately** as `CollisionCritical`, even before the first desired set exists (the desired rebuild then adopts the same pending key). The gate holds downward Z only, with **no timeout** (the old 2 s release fired at every startup, while the game thread applied nothing for ~2.2 s). |
| `RequestCollisionGateSupportTile` / `UpdatePawnCollisionGate` | — | The gate submits the pawn's level-0 support tile **immediately** as `CollisionCritical`, even before the first desired set exists (the desired rebuild then adopts the same pending key). The gate holds downward Z only, with **no timeout** (a 2 s release would fire at every startup, while the game thread applies nothing for ~2.2 s). |
| `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set (deduped by stamp) so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. |
| `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. |
| `RegisterStreamingAnchor` / `UnregisterStreamingAnchor` | — | **BlueprintCallable §9.3:** add/remove an actor as a streaming anchor (`EVoxelAnchorPolicy` CollisionOnly/FullVisual + XY/ZBelow/ZAbove box). Idempotent; forces a rebuild next Tick. §9.4: CollisionOnly tiles cook collision but are hidden (no draw/VSM) unless the player clipmap wants them too. |
| `LoadTile` | 445 | Budget check → `UE::Tasks::Launch` background gen+mesh; RAII task guard. Worker runs `Generator->ClassifyTile` first (T1.d): AllSolid/AllAir ⇒ skip `GenerateMesh`, tile stays empty (capture tiles always generate). It carries `GenerationEpoch`/desired epoch and checks `bShuttingDown`, generation pause, and its read-only cancellation token between expensive stages; obsolete work returns through `ProcessQueue`. STRATE CONTENT CUT: tiles ≥ `StrateContentCutMinLevel` pass the player-strate band (`MeshBandChunkLo/Hi` → voxels) to `GenerateMesh` + stamp it on `FChunkResult::BandChunkLo/Hi`; band change re-queues via `BandRemeshQueue` (see `UpdateChunksAroundPosition`). TOO-COARSE SKIP: if one cell is taller than the band (`Step > band height` — level ≥7 territory) the tile is enqueued EMPTY without launching a task (cell-granular cut could only render garbage). F18: `Tile.Level > MaxClipLevel` = SHEET tile → routed to `GenerateSheetMesh` (band mid-chunk = strate ref; band unarmed ⇒ empty; MC-ring sampling density, cells capped 128/axis; carries the XY hole `SheetHole*Vox` — hole moves ⇒ overlapping sheets re-queue via `BandRemeshQueue`, see `UpdateChunksAroundPosition`). §8.10. |
| `LoadTile` | 7291 | Budget check → `UE::Tasks::Launch` background gen+mesh; RAII task guard. Worker runs `Generator->ClassifyTile` first (T1.d): AllSolid/AllAir ⇒ skip `GenerateMesh`, tile stays empty (capture tiles always generate). It carries `GenerationEpoch`/desired epoch and checks `bShuttingDown`, generation pause, and its read-only cancellation token between expensive stages; obsolete work returns through `ProcessQueue`. STRATE CONTENT CUT: tiles ≥ `StrateContentCutMinLevel` pass the player-strate band (`MeshBandChunkLo/Hi` → voxels) to `GenerateMesh` + stamp it on `FChunkResult::BandChunkLo/Hi`; band change re-queues via `BandRemeshQueue` (see `UpdateChunksAroundPosition`). TOO-COARSE SKIP: if one cell is taller than the band (`Step > band height` — level ≥7 territory) the tile is enqueued EMPTY without launching a task (cell-granular cut could only render garbage). §8.10. |
| `UnloadTile` | — | Clears tile state; marks any matching generation request obsolete without removing its pending slot, so a late result cannot clear a replacement request; the component is PARKED in the pool (T2.c), not destroyed. |
| `ApplyMeshToTile` | — | Upload geometry. One component per tile (clipmap keeps count low; supersedes the old region batching); worker-built streams (T1.f) → `CreateSectionGroup(MoveTemp)`. Reuses the component's existing `URealtimeMesh` (no per-apply mesh alloc). **F17: two polygroups** (0 ground / 1 sky-cap, per-triangle class from the mesher) → RMC auto-section per non-empty group; slot 0 = override/default material, slot 1 = `CeilingMaterial` (fallback ground); config gated by `FChunkResult::bHasGroundTris/bHasCeilingTris`. Takes `FChunkResult&`; strate lookups clamp Z into `Result.BandChunkLo/Hi` (strate content cut) — ground at clamped bottom chunk, cap at clamped top (mid = gap fallback). Collision level-0 only (T1.c) both groups; shadow per SECTION: ground casts at level≤1, cap never. **§9.4:** `SetVisibility(false)` when the tile is in `CollisionOnlyTiles` (anchor-only, hidden — collision still cooks). §8.10 + ARCHITECTURE SurfaceWorld row. |
| `AcquireTileComponent` / `ReleaseTileComponent` | — | **T2.c component pool** (`TileComponentPool`, bounded): park on unload (geometry+collision stripped, hidden, stays registered), pop on apply — no `NewObject`/`RegisterComponent`/GC churn during travel & regen. §8.10. |
| `GetStrateAtPosition` | 965 | Gameplay query → strate index. |
| `GetStrateAtPosition` | 8351 | Gameplay query → strate index. |
| `GetBiomeAtWorldLocation` | — | **BlueprintCallable** biome probe at a world point (undoes actor xf → voxel → `Generator::QueryBiomeAt`). Returns `FVoxelBiomeQuery` for BP debug ("what biome / how many decos under the cursor?"). |
| `GetVoxelSurfaceHeightAt` | ~1534 | **BlueprintCallable** ground finder (F7 bridge): world XY → terrain + sky-cap world-Z via `Generator::GetSurfaceHeightAt`, NO trace/collision, deterministic, available before the area meshes → self-arranging prefab/ruin BPs snap their parts to the real ground. False (outs=input Z) on non-SurfaceWorld strates; ignores passage/spine carving. |
| `CarveAtPosition` / `FillAtPosition` | 691 / 709 | Build `FVoxelModification` → `ApplyModification`. |
| `ApplyModification` | ~1581 | Single funnel for all brushes: DiffLayer → **sync-remesh the brush-centre level-0 tile** (`SyncRemeshTile`, instant hole; skipped if that tile is mid-gen — would race a stale in-flight result) → `RemeshDirtyChunks(..., excludeCenter)` for the neighbours → `RemoveDecorationsInSphere`. |
| `ClearAllModifications` | 726 | Clears diff layer, regenerates. |
| `ChangeSeed` | 740 | **Legacy season reset**: new seed everywhere, clear diffs, bump season, reload. Rejected while a cooked season owns the seed. |
| `GetVoxelSurfaceHeightAt` | 8385 | **BlueprintCallable** ground finder (F7 bridge): world XY → terrain + sky-cap world-Z via `Generator::GetSurfaceHeightAt`, NO trace/collision, deterministic, available before the area meshes → self-arranging prefab/ruin BPs snap their parts to the real ground. False (outs=input Z) on non-SurfaceWorld strates; ignores passage/spine carving. |
| `CarveAtPosition` / `FillAtPosition` | 8413 / 8422 | Build `FVoxelModification` → `ApplyModification`. |
| `ApplyModification` | 8431 | Single funnel for all brushes: DiffLayer → **sync-remesh the brush-centre level-0 tile** (`SyncRemeshTile`, instant hole; skipped if that tile is mid-gen — would race a stale in-flight result) → `RemeshDirtyChunks(..., ExcludeTile)` for the neighbours → `RemoveDecorationsInSphere`. |
| `ClearAllModifications` | 8696 | Clears diff layer, regenerates. |
| `ChangeSeed` | 8710 | **Legacy season reset**: new seed everywhere, clear diffs, bump season, reload. Rejected while a cooked season owns the seed. |
| `GetCurrentSeed` / `GetCurrentSeason` / `GetCurrentSeasonContentHash` | — | Effective cooked-or-legacy identity; the hash is the join-time stale-season comparison value. |
| `RemeshDirtyChunks` | 798 | Queue loaded level-0 dirty tiles onto `DirtyRemeshQueue` (async re-mesh, no pop) + `MarkDirtyVoxelBox` the volume. Optional `ExcludeTile` = the sync'd centre. Drained FIRST in the submit loop at **BackgroundHigh** (ahead of streaming/band) so a dig never waits behind streaming; in-flight tiles stay QUEUED (not dropped) so a stale pre-carve result is corrected once it lands — fixes "hole shows up a beat late / not until I move". |
| `RemeshDirtyChunks` | 8801 | Queue loaded level-0 dirty tiles onto `DirtyRemeshQueue` (async re-mesh, no pop) + `MarkDirtyVoxelBox` the volume. Optional `ExcludeTile` = the sync'd centre. Drained FIRST in the submit loop at **BackgroundHigh** (ahead of streaming/band) so a dig never waits behind streaming; in-flight tiles stay QUEUED (not dropped) so a stale pre-carve result is corrected once it lands — fixes "hole shows up a beat late / not until I move". |
> **Game-thread profiling (Perf):** `AVoxelWorld::Tick` and its sub-steps are wrapped in `TRACE_CPUPROFILER_EVENT_SCOPE` — `VoxelForge_Tick / UpdateChunks / BuildDesiredTiles / CullTiles / SubmitTiles / ProcessPending / ProcessUnload / UpdateDecorations / UpdateWater`. Capture a `Count/Incl/Excl` Insights timer export and read the `Excl` column to see which step owns the per-frame cost (the actor tick shows as `BP_VoxelWorld_C` if subclassed in BP). `VoxelForge_ClassifyTile` (T1.d) / `VoxelForge_GenerateMesh` + `VoxelForge_BuildStreams` are worker-side (off the frame): the RMC `FRealtimeMeshStreamSet` is now built on the gen worker (`BuildTileStreamSet`) and carried on `FChunkResult::Streams` (TSharedPtr), so `ApplyMeshToTile` is game-thread-cheap — just material/ceiling resolve + `CreateSectionGroup(MoveTemp)`. See ARCHITECTURE §8.10 "Worker-built StreamSet (T1.f)".
@@ -343,31 +340,30 @@ This is **where terrain shape lives.**
| Symbol | .cpp line | Role |
|--------|-----------|------|
| `UVoxelGenerator` / `DensityCacheOwnerId` | — | Constructor allocates a process-unique integer identity (relaxed atomic, once per object). `GetDensityAt` includes it in the `CP_*` thread-local key, preventing a worker from serving another generator/world's params, biome context, `CP_UseOpStack`, or stack when `(ChunkCoord, LayoutVersion)` happens to match. Hot-path cost: one `uint64` compare per voxel. Scope is deliberately only the proved `CP_*` path. |
| `FractalNoise3D` (static) | 25 | fBM (layered Perlin). |
| `RidgedNoise3D` (static) | 55 | Ridged multifractal — craggy. |
| `CellularNoise3D` (static) | 101 | Worley/cellular — grotto/scallop. |
| `ApplyBoundarySeal` (static) | 170 | Solidifies strate top/bottom shells. |
| `ApplyPassageCarving` (static) | 197 | Punches passages/elevator through the seal. |
| `InitializeSettings` | 211 | Copies the effective seed, spine radius, and global XY world-bound settings from `UVoxelSettings` (the season owns all three when assigned); radius 0 preserves the unbounded field. |
| **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, materialises a cooked-season recipe on the existing per-chunk refetch when present, dispatches, then adds disturbances/diff. Its `CP_*` state is keyed by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; workers copy immutable recipe/vector data and never read editor composer state. |
| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. The optional `voxel.WormBlockSkip` path (default 0) uses `VF_TryGetWormBlockSkip` + a worker-local 4³ lattice-block cache to prove `|N1| >= WormThreshold` and remove both worm noise calls only; it never changes `NetworkMask` or the fallback field path. |
| **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. |
| `FractalNoise3D` (static) | 931 | fBM (layered Perlin). |
| `RidgedNoise3D` (static) | 949 | Ridged multifractal — craggy. |
| `CellularNoise3D` (static) | 976 | Worley/cellular — grotto/scallop. |
| `ApplyBoundarySeal` (static) | 996 | Solidifies strate top/bottom shells. |
| `InitializeSettings` | 2691 | Copies the effective seed, spine radius, and global XY world-bound settings from `UVoxelSettings` (the season owns all three when assigned); radius 0 preserves the unbounded field. |
| **`GetDensityAt`** | 2712 | **Entry point.** Picks strate + generator type and, on the per-chunk refetch, builds the chunk's operator stack (editor composer → cooked-season recipe → native archetype stack); evaluates it (TunnelNetwork/Underwater through the fused `GetDensityWithParams`), then adds disturbances, the structural tail and the diff layer. A degenerate strate has no stack and is air. Its `CP_*` state is keyed by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; workers copy immutable recipe/vector data and never read editor composer state. |
| **`GetDensityWithParams`** | 3749 | TunnelNetwork pipeline (~1000 lines) — the fused evaluator of the TunnelNetwork/Underwater stack in `GetDensityAt`. See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. The optional `voxel.WormBlockSkip` path (default 0) uses `VF_TryGetWormBlockSkip` + a worker-local 4³ lattice-block cache to prove `|N1| >= WormThreshold` and remove both worm noise calls only; it never changes `NetworkMask` or the fallback field path. |
| `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. |
| `ComputeSurfaceTerrainZ` / `GetSurfaceDensity` | — | SurfaceWorld heightfield → terrain Z, then density; biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/sheets/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in `SurfaceDensityFromColumn`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. The field-preserving LOD0 pass removes only needless temporaries/default construction; every output remains bit-identical. §8.14. |
| `VF_BuildOpStackForChunk` (file-static) | — | **The archetype → stack mapping, written down once.** `GetDensityAt` and `ClassifyTile` both call it; params are passed in, never fetched here. A second copy would be the worst bug available in this file — a tile skipped on the verdict of a stack that is not the one producing its density is a hole. Returns false (⇒ caller falls back to the `switch`) for an unported archetype, missing params, or a **degenerate strate**, since five archetype functions early-out to air there and the stack deliberately has no such early-out. `Refs.Surface == nullptr` makes it refuse SurfaceWorld, which is how `ClassifyTile` keeps its own exact-lattice proof. |
| `ComputeSurfaceTerrainZ` / `ComputeSurfaceColumn` | — | SurfaceWorld heightfield → terrain Z (+ sky-cap ceiling and overhang gate per column); biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in the stack's `FOverhangShelfMod`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. The field-preserving LOD0 pass removes only needless temporaries/default construction; every output remains bit-identical. §8.14. |
| `VF_BuildOpStackForChunk` (file-static) | — | **The archetype → stack mapping, written down once.** `GetDensityAt` and `ClassifyTile` both call it; params are passed in, never fetched here. A second copy would be the worst bug available in this file — a tile skipped on the verdict of a stack that is not the one producing its density is a hole. Returns false for an unknown archetype, missing params, or a **degenerate strate** (the stack deliberately has no degenerate early-out): `GetDensityAt` then returns air and `ClassifyTile` returns `Mixed`. `Refs.Surface == nullptr` makes it refuse SurfaceWorld, which is how `ClassifyTile` keeps its own exact-lattice proof. |
| `ClassifyTile` | — | **T1.d trivial-tile reject. ⛔ Not called by streaming by default since 2026-09-13** (`voxel.OuterClassifierMode=0`; net-negative in game, see ARCHITECTURE §8.10). Cooked/editor recipes now build the exact recipe stack and call its proved `ClassifyBox`; a tile crossing a slot/gap, any diff, or any disturbance returns `Mixed`. This replaces the old global `HasComposerRecipeOverride()` force-to-Mixed. Native cave/surface/gap proofs and all §8.10 caches remain unchanged. |
| `SampleRelief` / `SampleMoisture` | — | Climate fields (pure XY, [0,1]). Relief = shared source of truth for the relief map M. §8.14. |
| `SampleBiomeAt` | — | Warped-Voronoi + climate biome query (dominant + neighbour + weight). Reference used by the preview bake + `GetDominantBiomeAt`. §8.14. |
| `ResolveBiomeSampleAt` / `RebuildBiomeGrid` | — | Hot-path biome resolve (FBiomeSample) via a box-validated per-chunk cell-grid cache. Bit-identical to `SampleBiomeAt`. §8.14, §8.10. |
| `GetDominantBiomeAt` | — | Game-thread query → dominant biome ASSET (content/atmosphere). §8.14. |
| `QueryBiomeAt` | — | Rich game-thread biome probe → `FVoxelBiomeQuery` (dominant/neighbour asset, relief/moisture, blend weight, dominant deco count). Diagnostic behind `AVoxelWorld::GetBiomeAtWorldLocation`. §8.14. |
| `EvaluateTerrainConditions` | ~2548 | **F7 aware placement:** AND-evaluate an entry's `FTerrainCondition[]` (relief/moisture/biome-border) at a candidate voxel XY. Empty = true (zero cost). Pure query (SampleRelief/SampleMoisture/SampleBiomeAt) → worker-safe + game-thread; caller passes the strate's `FBiomeContext` (freq/contrast + Voronoi map). Consumed by deco `BuildCellSpawns` + `SpawnLandmarkInstance`; shared core of the future quest FindFeature locator. |
| `EvaluateTerrainConditions` | 6973 | **F7 aware placement:** AND-evaluate an entry's `FTerrainCondition[]` (relief/moisture/biome-border) at a candidate voxel XY. Empty = true (zero cost). Pure query (SampleRelief/SampleMoisture/SampleBiomeAt) → worker-safe + game-thread; caller passes the strate's `FBiomeContext` (freq/contrast + Voronoi map). Consumed by deco `BuildCellSpawns` + `SpawnLandmarkInstance`; shared core of the future quest FindFeature locator. |
> **Per-voxel hot-path memos (perf pass 2, all bit-identical — same hashes/math, hoisted per
> chunk/cell):** `GetDensityAt` uses the DiffLayer snapshot cache (§3.9); `GetDensityWithParams`
> memoizes the strate index per (chunkZ, `GetLayoutVersion()`); `thread_local` per-cell lattice
> bakes cover slab columns (`GetSlabDensity` step 4), maze open edges, vertical shafts +
> cross-connectors, floating-island constants, and the disturbance chasms/bridges/ridges; worm
> bakes cover slab columns (`FGridColumnMod`), maze open edges (`FLatticeCorridorSource`), vertical
> shafts + cross-connectors (`FShaftFieldSource`), floating-island constants (`FIslandBlobSource`),
> and the disturbance chasms/bridges/ridges; worm
> tunnels short-circuit the 2nd Perlin when N1 ≥ WormThreshold (N2 ≥ 0 ⇒ can't carve). With
> `voxel.WormBlockSkip=1`, a worker-local cache proves whole 4³ lattice blocks only when the
> actual 3D Perlin bound (`L = 8.118988160`, including `VOXEL_NOISE_SCALE`) plus a `1e-3`
@@ -438,22 +434,21 @@ is the shared digest used by `Tools/VoxelForgeTest.ps1 -Scenario parity`.
### 3.7 Cave morphology (SDF rooms/tunnels) — `Public/VoxelCaveMorphology.h` + `.cpp`
Header is rich with inline docs. Two namespaces + a per-chunk cache system.
- `namespace VoxelSDF` (h:46): `Sphere`, `Ellipsoid`, `Capsule`, `RoundedBox`,
- `namespace VoxelSDF` (h:78): `Sphere`, `Ellipsoid`, `Capsule`, `RoundedBox`,
`TaperedCapsule`, `SmoothMin`, `SmoothMax` — all FORCEINLINE SDF primitives.
- `namespace VoxelHash` (h:158): `Mix`, `Cell`, `Pair`, `ToFloat01`, `ToFloatSigned`
- `namespace VoxelHash` (h:210): `Mix`, `Cell`, `Pair`, `ToFloat01`, `ToFloatSigned`
— deterministic hashing for room/tunnel placement (no storage, infinite worlds).
- Cache structs (h:224-330): `FCachedRoom`, `FCachedTunnel`, `FCachedPit`,
- Cache structs (h:520-950): `FCachedRoom`, `FCachedTunnel`, `FCachedPit`,
`FCachedChimney`, `FCachedColumn`, `FChunkSDFCache`.
- `namespace VoxelCaveMorphology`:
| Function | .cpp line | Role |
|----------|-----------|------|
| `BuildChunkCache` | 47 | **Phase 1** (once/chunk): collect rooms, guaranteed backbone (`bTunnelsFlowTowardOrigin`: tree rooted at the (0,0) hub — every room reachable, links flow inward; false = legacy NN forest), slope-aware link metric (`TunnelHorizontalBias` now applies to backbone too), decide tunnels, **cull zero-connection rooms** (no sealed bubbles), store rooms by their OWN reach (fixes origin-room clipping at `MaxInfluence`), pre-bake pits/chimneys/columns via the shared `BakeRoomFeature` hash-placement skeleton (one gate/XY/radius pattern + per-type Emit lambda), hash-roll per-room terrain op. |
| `EvaluateSDFCached` | 757 | **Phase 2** (per voxel): SmoothMin over cached rooms/tunnels; returns nearest room idx for terrain-op lookup. **Signature changed (perf pass 2): `RoomShapeVariety` param REMOVED** — the shape roll + capsule trig are pre-baked into `FCachedRoom` (`ShapeType/ShapeA/ShapeB/ShapeR`) by `BuildChunkCache`, bit-identical. |
| `EvaluateSDF` | 738 | Convenience wrapper (builds temp cache) for one-off queries. |
| `BuildChunkCache` | 6782 | **Phase 1** (once/chunk): collect rooms, guaranteed backbone (`bTunnelsFlowTowardOrigin`: tree rooted at the (0,0) hub — every room reachable, links flow inward; false = legacy NN forest), slope-aware link metric (`TunnelHorizontalBias` now applies to backbone too), decide tunnels, **cull zero-connection rooms** (no sealed bubbles), store rooms by their OWN reach (fixes origin-room clipping at `MaxInfluence`), pre-bake pits/chimneys/columns via the shared `BakeRoomFeature` hash-placement skeleton (one gate/XY/radius pattern + per-type Emit lambda), hash-roll per-room terrain op. |
| `EvaluateSDFCached` | 8569 | **Phase 2** (per voxel): SmoothMin over cached rooms/tunnels; returns nearest room idx for terrain-op lookup. **Signature changed (perf pass 2): `RoomShapeVariety` param REMOVED** — the shape roll + capsule trig are pre-baked into `FCachedRoom` (`ShapeType/ShapeA/ShapeB/ShapeR`) by `BuildChunkCache`, bit-identical. |
`MakeStrateSeed` (h:~477) is the shared pure world-seed/strate-index salt used by both
`MakeStrateSeed` (h:965) is the shared pure world-seed/strate-index salt used by both
`BuildChunkCache` and the TunnelNetwork/Underwater destination landing queries.
`VF_SuggestLandingPoint` (h:~550, implemented in `VoxelCaveMorphology.cpp`) is the pure player-fit
`VF_SuggestLandingPoint` (h:1121, implemented in `VoxelCaveMorphology.cpp`) is the pure player-fit
landing-site query: it evaluates the local archetype source through the same capsule/support
contract as the measurement pass — support within step height, walkable slope, and full capsule
clearance. Room queries use a room-floor core, slabs keep their requested XY, and the sparse
@@ -467,7 +462,7 @@ SurfaceWorld remains refused because its production height can depend on manager
context. It never touches a generator, operator stack, manager, cache, or mutable state.
`FVoxelPassageLanding`, `VF_BuildPassageLanding`, `VF_EvaluatePassageLandingSDF`, and
`VF_IsPassageLandingFloor` (h:~56/~189, implemented near `.cpp:2059`) turn each inter-strate mouth
`VF_IsPassageLandingFloor` (h:57/184-199, implemented near `.cpp:6468`) turn each inter-strate mouth
into a deterministic rounded room plus a hard support slab. The room is sized from the player
capsule (minimum 12 voxels / 3 m flat floor, 12 voxels / 3 m clear height, and at least 14 voxels /
3.5 m authored width); the `(0,0)` origin landing is enlarged for the reserved future shaft, while
@@ -476,62 +471,61 @@ The manager's shared thread-local passage shortlist serves tube, room, and floor
landing search occurs per voxel. `GeneratePassages` stores the full descriptors and the
order-independence test compares them bit-for-bit.
Performance note (h:209-220): caching rooms/tunnels once per chunk instead of per
Performance note (h:520-531): caching rooms/tunnels once per chunk instead of per
voxel is the single biggest CPU win.
### 3.8 Strate system
**`Public/VoxelStrateTypes.h`** — shared structs/enums (1228 lines, the data vocabulary):
| Symbol | Line | Role |
|--------|------|------|
| `EVoxelPassageType` | 38 | Sloped/Vertical/Spiral/Cascading/Crack passage shapes. |
| `ESurfaceType` | 78 | Floor/Wall/Ceiling/Any (decoration placement). |
| `EVoxelNoiseType` | 99 | FBM/Ridged/Mixed/Cellular. |
| `ECaveGeneratorType` | 146 | TunnelNetwork / FlatPlain / CrystalChamber. |
| `EVoxelStrateTransition` | 183 | Gradient / Hard / Interleaved boundary blends. |
| **`FStrateGenerationParams`** | ~350 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, live terrain-detail fields, dead terrain-op transport slots, boundary seal). `Lerp()` static blends two sets at boundaries — it expands the **`VF_STRATE_PARAM_FIELDS` X-macro** (defined just above the struct): **adding a field to the struct? add it to that list** or blends silently reset it to default. |
| `FStrateTerrainOpEntry` | 965 | Soft-ptr to a terrain op + Weight + Probability. |
| **`FSlabGenerationParams`** | 1019 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
| **`FPlacementProfile`** | ~1747 | **Shared placement vocabulary** for every scatter primitive (`FStrateDecoration`, `FStrateLandmark`, coming `FStrateSetPiece`): spawn (ActorClass/InstancedMesh), Filter gates (surface/slope/overhang/water/RequiredBiome + **F7 awareness `Conditions[]`** — `FTerrainCondition` relief/moisture/biome-border predicates, AND-ed, evaluated by `Generator::EvaluateTerrainConditions`), Transform (align/offsets/RotationOffset+RandomRotation/scale), Render (cull/shadow). Each primitive embeds it as `Profile` + keeps only its own DISTRIBUTION fields. Per-primitive defaults set in each struct's ctor (deco: scale 0.8-1.2 + RandomRotation.Yaw=360; landmark: Ceiling + no align). |
| `FStrateDecoration` / `FStrateLandmark` | ~1830 / ~1900 | `Profile` + distribution: deco = StreamTier/SpawnDensity/MaxPerChunk; landmark = SpacingChunks/JitterFraction/SpawnProbability/StreamRadiusChunks + Light-Orb block. |
| `ELandmarkAnchor` (on `FStrateLandmark`) | ~1940 | F7: `AnchorMode` = HashLattice / PassageMouth + passage toggles + exclusion (`ExclusionRadiusChunks`/`Priority`) folded into `FStrateLandmark` (set-pieces merged in — one primitive, one `Landmarks` list, `UpdateLandmarks`). |
| `FStrateAmbientActor` / `FStrateCreature` | ~2090 / ~2110 | Content spawn entries (consumed by future systems). |
| `EVoxelPassageType` | 42 | Sloped/Vertical/Spiral/Cascading/Crack passage shapes. |
| `ESurfaceType` | 81 | Floor/Wall/Ceiling/Any (decoration placement). |
| `EVoxelNoiseType` | 128 | FBM/Ridged/Mixed/Cellular. |
| `ECaveGeneratorType` | 175 | TunnelNetwork / FlatPlain / CrystalChamber. |
| `EVoxelStrateTransition` | 228 | Gradient / Hard / Interleaved boundary blends. |
| **`FStrateGenerationParams`** | 373 | The giant TunnelNetwork param bag (rock, worms, rooms, tunnels, warp, roughness, live terrain-detail fields, dead terrain-op transport slots, boundary seal). `Lerp()` static blends two sets at boundaries — it expands the **`VF_STRATE_PARAM_FIELDS` X-macro** (defined just above the struct): **adding a field to the struct? add it to that list** or blends silently reset it to default. |
| `FStrateTerrainOpEntry` | 1169 | Soft-ptr to a terrain op + Weight + Probability. |
| **`FSlabGenerationParams`** | 1223 | Floor/ceiling heights, roughness, columns, seal — for slab generators. |
| **`FPlacementProfile`** | 2000 | **Shared placement vocabulary** for every scatter primitive (`FStrateDecoration`, `FStrateLandmark`; set-pieces are landmarks): spawn (ActorClass/InstancedMesh), Filter gates (surface/slope/overhang/water/RequiredBiome + **F7 awareness `Conditions[]`** — `FTerrainCondition` relief/moisture/biome-border predicates, AND-ed, evaluated by `Generator::EvaluateTerrainConditions`), Transform (align/offsets/RotationOffset+RandomRotation/scale), Render (cull/shadow). Each primitive embeds it as `Profile` + keeps only its own DISTRIBUTION fields. Per-primitive defaults set in each struct's ctor (deco: scale 0.8-1.2 + RandomRotation.Yaw=360; landmark: Ceiling + no align). |
| `FStrateDecoration` / `FStrateLandmark` | 2207 / 2271 | `Profile` + distribution: deco = StreamTier/SpawnDensity/MaxPerChunk; landmark = SpacingChunks/JitterFraction/SpawnProbability/StreamRadiusChunks + Light-Orb block. |
| `ELandmarkAnchor` (on `FStrateLandmark`) | 2251 | F7: `AnchorMode` = HashLattice / PassageMouth + passage toggles + exclusion (`ExclusionRadiusChunks`/`Priority`) folded into `FStrateLandmark` (set-pieces merged in — one primitive, one `Landmarks` list, `UpdateLandmarks`). |
| `FStrateAmbientActor` / `FStrateCreature` | 2405 / 2425 | Content spawn entries (consumed by future systems). |
**`Public/VoxelStrateDefinition.h`** — `UVoxelStrateDefinition : UPrimaryDataAsset`
(line 36). One asset = one strate *type*. Fields: identity, `StrateHeightInChunks`(60),
`TransitionType`(79)/`TransitionBlendChunks`(89), `GeneratorType`(102),
`GenerationParams`(113), `SlabParams`(124), `Biomes[]`+`BiomeMapParams` (the biome list +
field tuning — empty ⇒ unchanged world, §8.14), `TerrainOperations`(147), visuals/fog/light,
content lists, audio, `GameplayTags`(223), `bUseOperatorStack` (the OPSTACK A/B opt-in — only bites
if the archetype is in `UsesOperatorStackForChunk`'s ported list). EditConditions show/hide param
groups by generator type.
(line 46). One asset = one strate *type*. Fields: identity, `StrateHeightInChunks`(75),
`TransitionType`(93)/`TransitionBlendChunks`(103), `GeneratorType`(116),
`GenerationParams`(137), `SlabParams`(148), `Biomes[]`+`BiomeMapParams` (the biome list +
field tuning — empty ⇒ unchanged world, §8.14), `TerrainOperations`(252), visuals/fog/light,
content lists, audio, `GameplayTags`(393). EditConditions show/hide param groups by generator
type.
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:108).
**`Public/VoxelStrateManager.h` + `.cpp`** — `UVoxelStrateManager : UObject` (h:152).
Maps depth→strate at runtime; owns passages.
- `FVoxelPassage` (h:39): standing endpoints, radius, type, wandering control points, and
- `FVoxelPassage` (h:46): standing endpoints, radius, type, wandering control points, and
upper/lower local landing descriptors (room, floor, and door; no root connector).
- `FStrateSlot` (h:84): definition + chunk-Z range + index.
- `FStrateSlot` (h:128): definition + chunk-Z range + index.
| Method | .cpp line | Role |
|--------|-----------|------|
| `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
| `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
| `EvaluateModifierSDF` | 357 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. |
| `ApplyPassageModifier` | 1319 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
| `VF_ApplyPassageLandingCarving` | — | Idempotent density-independent landing-air threshold shared by the structural pass and the post-disturbance MC backstop; keeps legacy and operator-stack results bit-identical. |
| `ApplyPassageLandingFloorMC` | 1347 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
| `Initialize` | 1164 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. |
| `GeneratePassages` | 1659 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. |
| `EvaluateModifierSDF` | 2643 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. |
| `ApplyPassageModifier` | 2772 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. |
| `VF_ApplyPassageLandingCarving` | — | Idempotent density-independent landing-air threshold shared by the structural pass and the post-disturbance MC backstop, so the two agree bit for bit. |
| `ApplyPassageLandingFloorMC` | 2983 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. |
| `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. |
| `AnyPassageLandingFloorNearBox` | — | Conservative local-landing-floor AABB guard. It kills the `AllAir` hypothesis wherever a proved landing floor can occur; the op-stack returns `Both` for the same box. |
| `FindSlotIndexForChunkZ` | 427 | Z → layout index. |
| `GetStrateAt` / `GetStrateIndex` | 443 / 455 | World-Z queries. |
| `GetLayoutVersion` | h:161 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
| `GetStrateForChunk` | 466 | Chunk → definition. |
| `GetGeneratorTypeForChunk` | 476 | Chunk → generator type. |
| `UsesOperatorStackForChunk` | 559 | Chunk → should `GetDensityAt` take the operator stack? `bUseOperatorStack` on the definition **AND** archetype in the ported list — **now all 8 of 8** (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater). **That list is written down here and nowhere else.** With every archetype ported the flag is now the *only* thing that decides the path, so ticking the box is no longer a no-op anywhere — it is a real switch onto the operator stack for that strate. |
| `FindSlotIndexForChunkZ` | 6111 | Z → layout index. |
| `GetStrateAt` / `GetStrateIndex` | 6127 / 6139 | World-Z queries. |
| `GetLayoutVersion` | h:250 (inline) | Layout/passage generation counter (= `PassagesVersion`, bumped by every `Initialize` and by the editor composer override). Hot-path callers key `thread_local` memos on it (strate-index memo in `GetDensityWithParams`, passage shortlist) so live changes never serve stale data. |
| `GetStrateForChunk` | 6150 | Chunk → definition. |
| `GetGeneratorTypeForChunk` | 6176 | Chunk → generator type. |
| `UsesOperatorStackForChunk` | 6203 | Chunk → does its slot have an operator stack? True for any in-layout slot with a definition whose archetype is one of the 8 (Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork, Underwater); false outside the layout. **That list is written down here and nowhere else.** `ClassifyTile` guards on it; `GetDensityAt` uses it to share TunnelNetwork stacks by chunk Z. |
| `GetRecipeForChunk` | — | Runtime immutable recipe/vector/strate-seed copy for cooked seasons; editor slot overrides use the same worker hand-off. |
| `GetSlabParamsForChunk` | 490 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
| `GetSlabParamsForChunk` | 6263 | Slab params with runtime Z bounds (no blend — slabs use Hard). |
| `GetBiomeContextForChunk` | — | Flatten the strate's `Biomes[]` + `BiomeMapParams` into a POD `FBiomeContext` for the biome field. Empty ⇒ biomes disabled. §8.14. |
| `GetGenerationParams` | 515 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
| `GetGenerationParams` | 6509 | **Blended** TunnelNetwork params (handles Gradient/Hard/Interleaved transitions). |
| `SetComposerOverrideForStrate` / `GetComposerOverrideForChunk` | editor-only | Temporary density-only candidate overlay for one existing slot. Copies immutable params/recipe into the worker refetch path, leaves layout/content/passages intact, and bumps `PassagesVersion` to invalidate generator memos. |
| `BuildParamsFromDefinition` (static) | 1399 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
| `BuildParamsFromDefinition` (static) | 6815 | Returns the definition's base `GenerationParams` only. Terrain-op assets are passed separately to `BuildChunkCache`, which applies one selected op per room and pre-bakes Column/Pit/Chimney features. |
**`Public/VoxelTerrainOpDefinition.h` + `.cpp`** — `UVoxelTerrainOpDefinition : UPrimaryDataAsset`
(h:67). One asset = one terrain op. `EVoxelTerrainOpType` (h:36): Terrace, LayerLines,
@@ -556,7 +550,7 @@ atmosphere override, `WaterMaterial`, `MaterialPaletteIndex` (F6 — baked to ve
`UVoxelStrateDefinition::Biomes[]`. Generator-agnostic (surface biomes now, cave biomes later). §8.14.
**`Public/VoxelStrateComposer.h` + `Private/VoxelStrateComposer.cpp`** (Tier 4a/4b + Tier 5) — offline
`FVoxelStrateCorpus`, `VF_RollStrateParams`, and `VF_RollStrateStructure`. `LoadFromAssetRegistry` enumerates every project
`FVoxelStrateCorpus`, `VF_RollStrateParamsDetailed`, and `VF_RollStrateStructure`. `LoadFromAssetRegistry` enumerates every project
`UVoxelStrateDefinition` through the Asset Registry, excludes `Saved/Autosaves` and `Saved/Cooked`
copies, and adds one `VoxelStrateTypes.h` default vector for each of the eight exact archetypes.
The corpus is grouped by exact `ECaveGeneratorType`: the 2026-09-04 run loaded **4 project
@@ -668,31 +662,29 @@ text plus independently copied metadata/hash, and retains soft references to any
`UVoxelSettings::Season` is the only activation switch.
### 3.9 Player edits — `Public/VoxelDiffLayer.h` + `.cpp`
`UVoxelDiffLayer : UObject` (h:77). Stores `FVoxelModification` (h:43: Center/Radius/Strength;
`UVoxelDiffLayer : UObject` (h:137). Stores `FVoxelModification` (h:60: Center/Radius/Strength;
**negative Strength = carve, positive = fill**) grouped by chunk in `TMap ChunkMods`.
| Method | .cpp line | Role |
|--------|-----------|------|
| `SetBudget` | 10 | From VoxelSettings carving caps. |
| `CanModify` | 20 | Budget check (no consume) — for UI. |
| `GetRemainingModifications` / `GetRemainingVolume` | 47 / 53 | -1 = unlimited. |
| `ApplyModification` | 63 | Enforces budget, stores in all overlapped chunks, returns dirty coords. |
| `GetDensityOffset` | 131 | Per-voxel combined diff (smoothstep falloff, additive). |
| `HasModifications` | 160 | Fast reject for hot path. |
| `HasAnyMods` / `GetModsVersion` | h:238 / h:241 (inline) | Lock-free atomics: any-mod-exists flag + monotonic mod-state version (bumped by `ApplyModification`/`Clear`). |
| `SetBudget` | 102 | From VoxelSettings carving caps. |
| `CanModify` | 112 | Budget check (no consume) — for UI. |
| `GetRemainingModifications` / `GetRemainingVolume` | 169 / 175 | -1 = unlimited. |
| `ApplyModification` | 185 | Enforces budget, stores in all overlapped chunks, returns dirty coords. |
| `GetDensityOffset` | 299 | Per-voxel combined diff (smoothstep falloff, additive). |
| `HasModifications` | 421 | Fast reject for hot path. |
| `HasAnyMods` / `GetModsVersion` | h:246 / h:249 (inline) | Lock-free atomics: any-mod-exists flag + monotonic mod-state version (bumped by `ApplyModification`/`Clear`). |
| `GetChunkModsSnapshot` | — | Copy one chunk's mod list under ONE read lock. Workers snapshot per (chunk, version) instead of locking per voxel — `GetDensityAt` keys a `thread_local` 64-slot direct-mapped cache on it (~27 lock ops per tile task instead of ~86k once any carve exists). |
| `HasAnyModInChunkRange` | — | Any modified chunk key in an inclusive chunk box? One key walk under a read lock — ClassifyTile's diff guard (per tile, conservative by construction: mods are stored in every chunk their radius overlaps). |
| `EvaluateMods` (static) | — | Lock-free pure evaluation of a mod list at a voxel — shared core of `GetDensityOffset` and the generator's snapshot path. |
| `Clear` | 170 | Wipe all (season reset). |
| `GetTotalModificationCount` / `GetModifiedChunkCount` | 182 / 192 | Stats. |
| `Clear` | 435 | Wipe all (season reset). |
| `GetTotalModificationCount` / `GetModifiedChunkCount` | 454 / 465 | Stats. |
### 3.10 Mesher — `Public/VoxelMarchingCubesMesher.h` + `.cpp`
`UVoxelMarchingCubesMesher : UObject` (h:21). Holds `Generator` ptr, `IsoLevel=0`, skirt params.
(The dead trio `GetDensity`/`InterpolateEdge`/`ComputeGradientNormal` + `GradientOffset` was
removed — since T1.b the pre-sampled grid supplies positions AND gradients inline.)
`UVoxelMarchingCubesMesher : UObject` (h:42). Holds `Generator` ptr, `IsoLevel=0`, skirt params.
The pre-sampled grid supplies positions AND gradients inline (T1.b).
| Method | .cpp line | Role |
|--------|-----------|------|
| **`GenerateSheetMesh`** | ~455 | **F18 far-field SHEET** (render-distance ring, `Tile.Level > MaxClipLevel`): two displaced heightfield grids per tile — ground (polygroup 0) + sky-cap (polygroup 1) from `GetSurfaceHeightAt` columns (StrateChunkZ = band mid identifies the strate; non-SurfaceWorld ⇒ empty). Classes true BY CONSTRUCTION (no vote/probes). Same conventions as `GenerateMesh` (world-cm positions, planar UVs, F6 colour masks, −N double-faced perimeter skirts per bucket, ground‖cap + `NumCeilingTriangles`). Margin ring keeps normals continuous between sheets. XY HOLE params: cells fully inside the MC-covered box around the player (`AVoxelWorld::SheetHole*Vox`, shrunk 1 tile for seam overlap) are skipped — a partially-covered sheet must not overlay near terrain; hole-edge cells get no skirt. Carved features (passages/spine/chasms) + diff layer NOT represented — accepted at sheet distance. |
| **`GenerateMesh`** | ~15 | The MC loop over cells; `Step` controls LOD sampling. Sets the T2.b octave bias for the tile (`TGuardValue` on `VoxelGenLOD::OctaveBias`, from `LODOctaveDrop` × log2(Step); 0 at LOD0/off). Edge `t` + grid-gradient normals computed inline (`SampleG`/`GradAt`). Optional `OutCaptureGrid` (4th arg) = CAPTURE-DURING-MESHING: when non-null + full-res (`CellsPerAxis==CHUNK_SIZE`), copies the already-sampled `CHUNK_SIZE³` density grid (quantized via `VF_QuantizeDensity`, VoxelTypes.h) so the density clipmap reuses it instead of re-sampling `GetDensityAt`. Pure read of the grid — §8.10 untouched. **F17 surface class**: each unique vertex is classified sol/sky-cap in `GetOrCreateVertex` (down-facing only → memoized `GetSurfaceHeightAt`, nearer `CeilSurf` = cap); triangles bucket by majority into `GroundTris`/`CapTris` (thread_local), skirts emit per bucket, then `Triangles = ground‖cap` + `FVoxelMeshData::NumCeilingTriangles` (→ polygroups in `BuildTileStreamSet`). Same tris, only index ORDER changes. STRATE CONTENT CUT: optional `BandZMin/MaxVox` params restrict the cz cell loop (+ gz sampling rows) to the player-strate band — coarse straddling tiles mesh ONE strate (kills far-LOD inter-strate aliasing holes); meshed cells bit-identical. |
| **`GenerateMesh`** | 96 | The MC loop over cells; `Step` controls LOD sampling. Sets the T2.b octave bias for the tile (`TGuardValue` on `VoxelGenLOD::OctaveBias`, from `LODOctaveDrop` × log2(Step); 0 at LOD0/off). Edge `t` + grid-gradient normals computed inline (`SampleG`/`GradAt`). Optional `OutCaptureGrid` (4th arg) = CAPTURE-DURING-MESHING: when non-null + full-res (`CellsPerAxis==CHUNK_SIZE`), copies the already-sampled `CHUNK_SIZE³` density grid (quantized via `VF_QuantizeDensity`, VoxelTypes.h) so the density clipmap reuses it instead of re-sampling `GetDensityAt`. Pure read of the grid — §8.10 untouched. **F17 surface class**: each unique vertex is classified sol/sky-cap in `GetOrCreateVertex` (down-facing only → memoized `GetSurfaceHeightAt`, nearer `CeilSurf` = cap); triangles bucket by majority into `GroundTris`/`CapTris` (thread_local), skirts emit per bucket, then `Triangles = ground‖cap` + `FVoxelMeshData::NumCeilingTriangles` (→ polygroups in `BuildTileStreamSet`). Same tris, only index ORDER changes. STRATE CONTENT CUT: optional `BandZMin/MaxVox` params restrict the cz cell loop (+ gz sampling rows) to the player-strate band — coarse straddling tiles mesh ONE strate (kills far-LOD inter-strate aliasing holes); meshed cells bit-identical. |
**`Public/MarchingCubesTables.h`** — `EdgeTable` + `TriTable` reference data (Paul
Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs editing.
@@ -711,8 +703,10 @@ Bourke). Cube corner/edge layout documented at top (lines 7-37). Rarely needs ed
---
### 3.12 Automation tests — `Private/Tests/` (added 2026-07-27, `#if WITH_DEV_AUTOMATION_TESTS`)
The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor's
**Session Frontend → Automation**, filter `VoxelForge`.
The plugin's first tests (`Docs/archive/OPSTACK-PLAN.md` Phase 0.5). Run them from the editor's
**Session Frontend → Automation**, filter `VoxelForge`. The `OpStack.*Equivalence` names are
historical: those tests check the shipped stacks themselves and no longer compare against a
second density path.
| File | Test name | What it proves |
|------|-----------|----------------|
@@ -724,61 +718,74 @@ The plugin's first tests (`OPSTACK-PLAN.md` Phase 0.5). Run them from the editor
| `VoxelForgeClassifyTileTest.cpp` | `VoxelForge.OpStack.BoxVerdictFold` | Pure-logic walk of the fold in `VoxelDensityOp.h`, case by case — including the seal-forces-AllSolid case that justifies `ClassifyBox` existing. Also the only `.cpp` that includes the op header, so the build actually sees it. |
| `VoxelForgeHeightStackTest.cpp` | `VoxelForge.OpStack.SurfaceHeightEquivalence` | The height-space stack vs `ComputeSurfaceTerrainZ`, in **altitudes**. Runs twice: defaults, then **all F20 terrain ops ON** — the load-bearing pass, since the ops are off by default and the defaults pass exercises only the structural source. Also brute-forces `MaxDisplacement` (a false bound would be a hole). Bar is bit-identity; a height delta is a visibly different world, not rounding. |
| `VoxelForgeCrossPlatformTest.cpp` | `VoxelForge.Determinism.CrossPlatformDigest` | SHAPE digest (sign of density = the world) + FIELD digest (bit-for-bit) over a fixed integer grid, plus `NearIso` bounding how many samples could flip sign. Reports rather than asserts until pinned. Run on Windows and Linux and compare. |
| `VoxelForgeOpStackSlabTest.cpp` | `VoxelForge.OpStack.SlabEquivalence` | **Phase 2's first port.** The same 6-op slab stack vs `GetSlabDensity` over 20k points, run twice — FlatPlain **and** CrystalChamber — which is what demonstrates the two archetypes really are one op. Plus window-invariance and box-verdict brute force. Compares against the reference **as it is now** (post Z-term removal), so green = pure refactor and any visual delta is attributable to §3.1 alone. |
| `VoxelForgeOpStackSlabTest.cpp` | `VoxelForge.OpStack.SlabEquivalence` | The same 6-op slab stack run three times — FlatPlain, CrystalChamber and a tuned CrystalChamber (the worst case for the `ClassifyBox` amplitude bounds): finite density over 20k points, window-invariance across order and threads, and box-verdict brute force. |
| `VoxelForgeOpStackTunnelTest.cpp` | `VoxelForge.OpStack.TunnelNetworkSpineEquivalence` | **Stage A of the last port.** Zeroes the 13 detail-op amplitudes so the *original* takes the path stage A ported — that is what makes an incomplete stack verifiable now. Samples in **clusters** (24 chunks × 250 points), because the SDF cache rebuilds when a query leaves its box and uniform sampling would rebuild per point on both paths. Check 3 (two param sets, A/B interleaved) compares each stack **to itself alone, never to the original** — the original would fail it, see AUDIT §C2. Asserts **zero** box verdicts, which is the honest stage-A result. |
| `VoxelForgeOpStackShaftTest.cpp` | `VoxelForge.OpStack.VerticalShaftEquivalence` | The port that tests **reuse**, not fidelity: three of the five ops are Maze's, unchanged. Forces connectors + ledges on, because both are off or negligible at defaults and a resting param is an untested operator. The source and mirror now share a deterministic inner-3×3-emitted drainage tree whose parents come from rebuild-only ±2/±3 windows in a wider direct-indexed halo (default 9×9 emit / 15×15 roll), plus the old random links. Tree connector Z is exact and ledge-free; tree and random capsules are spatially culled from the cached rebuild. **Fixed 2026-07-29:** its box query used to return `CarveOnly` because a shaft merely *existed* within a `Spacing*1.6` halo — true almost everywhere at `ShaftSpacing 55 / ShaftDensity 0.6`, hence **0 of 60** tiles. It now publishes an isolated interval for tree and random capsules (same row-major cell order ⇒ same `VoxelHash::Pair`, so symmetry of `Pair()` is not assumed), with **Z exact** and XY conservative; the ledge and converter decide their own effects. The sampler was also widened from ±48 voxels to ±440. Every proved tile is brute-forced over its full lattice, and the test reports rebuild-vs-hot cache cost. |
| `VoxelForgeOpStackIslandTest.cpp` | `VoxelForge.OpStack.FloatingIslandEquivalence` | The port that runs the stack **backwards** — void + fill vs rock + carve, same classes with the opposite sign. Counts interior-solid and open-void samples separately (on this archetype an aggregate "N solid" is dominated by the seal bands and says nothing about the islands). Counts `AllSolid` and `AllAir` verdicts **separately** too: `AllAir` is the one no cave archetype could ever prove, and it is the entire perf argument here. |
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | **Phase 1's load-bearing test.** The 8-op Maze stack vs `GetMazeDensity` over 20k points (bit-identity; a side-of-iso disagreement is the hard fail), plus purity across workers, a 64-seed spanning-tree/connectivity audit, corridor morphology, cache rebuild-vs-hot timing, and brute force on every box verdict the stack emits. Latest audit: **0 tree edge-count violations, 0 tree-disconnected, 0 Maze-disconnected**; interior degrees `d1=15.91%`, `d2=36.60%`, `d3=31.73%`, `d4=12.95%`, `d5=2.56%`, `d6=0.25%`, mean **2.504** versus a cubic grid's degree-6/mean-6.000; runs `min=1 / median=1 / p90=3 / max=18`. Latest focused cache timing: **1.436 μs rebuild**, **0.150 μs hot call**; final namespace timing: **1.130 μs rebuild**, **0.145 μs hot call**. Latest box audit: **104 proved, 152 Mixed, 138,424 voxels, 0 violations**. |
| `VoxelForgeOpStackMazeTest.cpp` | `VoxelForge.OpStack.MazeEquivalence` | The 8-op Maze stack: decomposition, purity across workers, a 64-seed spanning-tree/connectivity audit, corridor morphology, cache rebuild-vs-hot timing, and brute force on every box verdict the stack emits. Latest audit: **0 tree edge-count violations, 0 tree-disconnected, 0 Maze-disconnected**; interior degrees `d1=15.91%`, `d2=36.60%`, `d3=31.73%`, `d4=12.95%`, `d5=2.56%`, `d6=0.25%`, mean **2.504** versus a cubic grid's degree-6/mean-6.000; runs `min=1 / median=1 / p90=3 / max=18`. Latest focused cache timing: **1.436 μs rebuild**, **0.150 μs hot call**; final namespace timing: **1.130 μs rebuild**, **0.145 μs hot call**. Latest box audit: **104 proved, 152 Mixed, 138,424 voxels, 0 violations**. |
| `VoxelForgeStrateParamCoverageTest.cpp` | `VoxelForge.Determinism.StrateParamBlendCoverage` | **The X-macro guard** (added 2026-08-17). `FStrateGenerationParams::Lerp` blends the hand-written `VF_STRATE_PARAM_FIELDS` list, **not** the struct — so a field added to one and not the other compiles, tests green, and silently takes its **default** inside every Gradient/Interleaved transition band. This expands the X-macro a **third** way (after LERP and SNAP), into a name list, and diffs it against the struct's UObject reflection. Pure shape test: no fixture, no world, instant. `GExemptFieldNames` is **empty** — every reflected field is covered today, and any exemption must be written down as a decision. Stakes rise with the world composer, which intends to invent parameter sets through this same `Lerp` (`COMPOSER-NOTES.md`). |
| `VoxelForgeComposerParameterRollTest.cpp` | `VoxelForge.Composer.ParameterRoll` | Asset-Registry corpus audit + complete per-archetype spread/exclusion/clamp table; asserts bit-identical deterministic rerolls and same-archetype parents; measures 64 transient candidate strates with `VF_MeasureStrate` plus the exact unsnapped arrival→departure law; brute-forces every rolled production box verdict. World-scale run: **1,422 applications / 168 distinct fields**; **36/64 survival**, 60 non-vacuous, 58 largest-component, 40 exact-law passes; **1,411 Mixed + 731 AllSolid + 418 AllAir = 1,149 proved boxes**, **1,529,319 lattice voxels checked, 0 violations**, 148.387 s. |
| ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice, legacy and operator-stack paths; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 24 rows match. |
| ″ | `VoxelForge.Composer.TerrainDetailLiveness` | Fixed 4,096-point `GetDensityAt` lattice on the TunnelNetwork slot; changes one terrain-detail group at a time with an empty terrain-op pool. Proves 9 live groups / 23 fields and 3 dead groups / 11 fields; all 12 rows must match. |
| `VoxelForgeComposerPromotionTest.cpp` | `VoxelForge.Composer.Promotion` | Re-measures the 12 project/default members, simulates five deterministic 24-candidate seasons with normalized measured-metric novelty (`<0.20`), cap 6, cumulative JSON promotion, provenance counts, corpus-hash checks, fresh-load gate verification, spread/survival reporting, and deliberate stale-metric corruption. The final landing validation passed the strict existing gates; no assertion was weakened. |
| `VoxelForgeComposerStructureRollTest.cpp` | `VoxelForge.Composer.StructureRoll` | Rolls root polarity → legal shape source → polarity-derived conversion → 4–8 declaration-legal modifiers → mandatory structural posts; blends the six native parameter families independently, measures 64 novel stacks, captures the same grid for the deterministic filled/contour XZ/XY preview, runs a separate step-1 radius-64 ROI pass for the 33 survivors centred on `LargestComponentPoint`, checks exact arrival→departure connectivity, rerolls every recipe/stack for determinism, and brute-forces every uniform box verdict. World-scale run: **33/64 survival (51.6%)**, **64 distinct recipes**, **0 invalid recipes**, **715 proved custom boxes / 951,665 lattice voxels / 0 violations**, blank plan/card **4/33→5/33**, **174.854 s**, **0 refusals**. |
| `VoxelForgeComposerShowcaseTest.cpp` | `VoxelForge.Composer.Showcase` | Exhausts the bounded parameter-roll set (seeds **0, 7331**, indices **0–63**), excludes multi-region rolls while the lateral gate is off, and measures every missing-family candidate in all six interior target slots at step 4 / radius 256 / `MaxCells=8,000,000`. Hard gates are non-vacuous, largest air share ≥ **0.50**, and exact unsnapped arrival→departure connectivity; selection score is floor-area fraction + clearance tie-break + projected-surface tie-break. It asserts roll/manifest determinism, density sign, zero `WorldRadiusVoxels`, bit-identical metric reruns, and writes one alphabetized card per archetype to `Saved/VoxelForge/Showcase/index.html`, with step-1 radius-64 filled/contour plan + vertical ROI images centred on `LargestComponentPoint`. The final landing regeneration evaluated **256** rolls (**82** eligible single-region, **174** skipped multi-region), produced **7 fine renders, 1 refusal, 0 blanks**, and retained a diagnostic hard-gate survivor set of **0/8**. |
| `VoxelForgeComposerSeasonTest.cpp` | `VoxelForge.Composer.Season` | Existing compose/review/393,216-sample round trip plus schema hash tamper rejection, two independent runtime manager/generator instances, season-authoritative seed/spine/radius, unset-season regression, and brute-force verification of every sampled non-Mixed recipe tile verdict (up to 64). |
| `VoxelForgeComposerCorpusFreeTest.cpp` | `VoxelForge.Composer.CorpusFree` | Shares each structure recipe across today's corpus blend, naive independent uniform rolls, and constraint-sampled rolls. The completed equal-arm world-scale run uses **16 candidates per arm**, step 4 / radius 256 / `MaxCells=8,000,000`, fixed passage-law mouths, and 40 box probes per candidate. Result: **10/16, 9/16, 8/16** survival; survivor feature-scale ranges are **8..268**, **8..60**, **8..92** voxels. Box checks: **185/246,235**, **260/346,060**, **299/397,969** proved/voxels, **0 violations** in every arm; constraint parameter and box-stop violations were also 0. |
| `VoxelForgeLayoutOrderIndependenceTest.cpp` | `VoxelForge.Determinism.LayoutOrderIndependence` | Builds a known transient soft-pointer pool, then rebuilds it in original, reversed, and swapped orders. Requires a non-empty layout and passage set, and compares every slot's definition/Z/height plus passage endpoints, landing descriptors, control geometry, and bounds bit-for-bit. |
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries, then audits all inter-strate landing ends: body-derived room dimensions, final-density flat-floor slope, local floor/join geometry, seal containment, hot-call/rebuild timing, and targeted `ClassifyTile` box soundness. Reports source-fit/refused mouths and per-archetype landing slopes; fails on any floor, join, seal, or box violation. |
| `VoxelForgePassageOpenSpaceTest.cpp` | `VoxelForge.Determinism.PassageLandsInOpenSpace` | Uses the real fixture density path to check both generated mouths against independent source queries (Maze/VerticalShafts/FloatingIslands footings are probed on the archetype stack built without a strate manager, i.e. without passage carving), then audits all inter-strate landing ends: body-derived room dimensions, final-density flat-floor slope, local floor/join geometry, seal containment, hot-call/rebuild timing, and targeted `ClassifyTile` box soundness. Reports source-fit/refused mouths and per-archetype landing slopes; fails on any floor, join, seal, or box violation. |
| `VoxelForgeStrateConnectivityTest.cpp` | `VoxelForge.Generation.StrateConnectivity` / `VoxelForge.Generation.StrateConnectivityRefinement` / `VoxelForge.Generation.VerticalShaftSeamFreedom` | Bounded strate metrics with density-polarity and solid-gap controls, deterministic route rechecks, and refinement sweeps. The refinement test uses the bounded 4-chunk density fixture, fits each measurement AABB to the arrival/departure mouth pair plus three shaft spacings (the prescribed local tree fallback), and caps the fitted control at 120,000,000 cells. It reports exact before/after seed-6 cell counts, reruns every negative with the fitted margin doubled, measures tree orphan candidates/path reachability and roughness-bubble proxies, asserts mouths remain inside the deterministic shaft feature core (the exact axis is an unsupported open cylinder), checks source/mirror physical paths, and requires 16/16 effective arrival→departure results across the 16 VerticalShafts seeds. Margin-binding negatives are reported as measurement limits, never as gap findings. The shaft seam test re-evaluates cell-boundary positions after warming distinct neighbouring chunk contexts. |
| `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes after warming six different neighbouring chunk contexts in both legacy and operator-stack paths. Latest result: **0 legacy mismatches, 0 operator mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. |
| `VoxelForgeClipmapNoOverlapTest.cpp` | `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage` | Sweeps 6,174 centres (negative coords, every parity) × radius/level/render-distance/vertical-clamp/sheet combinations: **zero overlapping desired pairs AND coverage ⊇ the frozen `7486c29` selector**. Failed on `7486c29` (1,022,679 overlapping pairs) as intended. |
| `VoxelForgeMazeSeamTest.cpp` | `VoxelForge.Generation.MazeSeamFreedom` | Re-evaluates **42** cell-boundary probes through `GetDensityAt` after warming six different neighbouring chunk contexts; must report **0 mismatches**. The Maze source's 2×2×2 lower-node window is deliberately local; no wide collect is involved. |
| `VoxelForgeClipmapNoOverlapTest.cpp` | `VoxelForge.Streaming.ClipmapDesiredTilesNoOverlapCoverage` | Sweeps 5,145 cases (343 centres with negative coords and every parity × 5 radius/level/render-distance settings × 3 vertical clamps): **zero overlapping desired pairs AND coverage ⊇ the frozen `7486c29` selector**. Failed on `7486c29` (1,022,679 overlapping pairs) as intended. |
| `VoxelForgeDensityBlockEquivalenceTest.cpp` | `VoxelForge.Density.OperatorBlockMatchesScalarGrid` | Block evaluation (`EvalBlock` after `BuildActiveOpList` pruning) vs scalar, **bit-identical**: 5.1M mesher-grid samples (6 seeds × 8 archetypes × levels 0–5, with edits) + 300k direct `EvalBlock` samples. Scopes `voxel.UseFusedEvaluator=0` so the real block path runs. An injected optimistic `FPassageCarveOp::EffectOverBox` produced 5,612 mismatches. |
| `VoxelForgeWorldEdgeSealTest.cpp` | `VoxelForge.Generation.WorldEdgeSeal` | Positive samples outside the radius across every fixture strate, bit-identical interior negative control against radius 0, monotonic smooth-ramp check, radius-0 no-op, radial `ClassifyBox`/T1.d proofs brute-forced for every reported voxel, and synthetic plus generated near-rim passage coverage. Emits one aggregate summary with proved-box and voxel counts. |
| `VoxelForgeCorrectnessRoundTest.cpp` | `VoxelForge.Determinism.DiffApiBoundary` / `DiffLayerIdentity` / `EmptyPassageVersion` / `SurfaceBiomeCacheIdentity` / `TileCacheContextSwitch` / `ZeroTransitionBlend` | Regression coverage for the cold-audit correctness fixes (diff-layer API edges, cache identities, empty-passage layout version, zero transition blend). |
| `VoxelForgeDescentCorridorTest.cpp` | `VoxelForge.Descent.OwnerPassageCorridor` / `VoxelForge.Descent.OwnerSeedSweep` | Samples the authored A-to-B passage as a body-sized walking corridor in the final owner field. |
| `VoxelForgeDetSinCosTest.cpp` | `VoxelForge.Determinism.DetSinCos` | Pinned contract for `VoxelMath::DetSin` / `DetCos` / `DetSinCos`. |
| `VoxelForgeIsFiniteTest.cpp` | `VoxelForge.Math.IsFiniteExact` | Bit-exact equivalence of the generation-path finite predicate (`VoxelMath::IsFinite`). |
| `VoxelForgeLargeSeedTest.cpp` | `VoxelForge.Determinism.LargeSeedSurvives` | AUDIT C1: the world must still be a world at a large seed (bounded `VoxelHash::SeedOffset`). |
| `VoxelForgeLateralRegionsTest.cpp` | `VoxelForge.Composer.LateralRegions` | Tier 4d lateral region partition, density blending, conservative box proofs, and the arrival→departure gate (asserts `VF_LateralRegionsAreShippable` stays false while the seam rate is imperfect). |
| `VoxelForgeOpStackChannelTest.cpp` | `VoxelForge.OpStack.ChannelDAG` | Channel/resource declarations are executable stack-assembly metadata (`FVoxelOpStack::ValidateChannelOrder`). |
| `VoxelForgeScaleDiagnosisTest.cpp` | `VoxelForge.Composer.ScaleDiagnosis` | Measurement-only scale diagnosis for the player-fit showcase. |
| `VoxelForgeSealedSolidProofTest.cpp` | `VoxelForge.Determinism.SealedSolidProofSoundness` | Sealed-solid tile proof soundness and invalidation (one-sided: may say false too often, never a wrong true). |
| `VoxelForgeTilePostReachTest.cpp` | `VoxelForge.Correctness.TilePostReachProof` | The tile-reach proof must fail loudly when its envelope is deliberately made too small. |
| `VoxelForgeVerticalStrateReachTest.cpp` | `VoxelForge.Streaming.VerticalStrateReach` | Surface-strate streaming keeps the ground visible when the pawn is high in the same strate. |
| `VoxelForgeWormBlockSkipTest.cpp` | `VoxelForge.Determinism.WormBlockSkipSoundness` | Soundness and coverage of the worker-local worm N1 block proof (`voxel.WormBlockSkip`). |
| `VoxelForgePlayerFitWindow.h` | — | Shared measurement-window policy for player-fit diagnostics (test/editor side only). |
## 4. The density pipeline (most-edited hot path)
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:277
### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749
Stage order (negative=solid throughout). Each stage's anchor:
| Step | Line | What |
|------|------|------|
| 1 — Vertical scale | 307 | Stretch Z before noise (`VerticalScale`). |
| 2 — Base density | 316 | Everything starts solid at `BaseDensity`. |
| 3 — Cave warp | 321 | Domain-warp the SDF query coords (organic shapes). |
| 4 — SDF morphology | 368 | Rooms+tunnels via `BuildChunkCache`/`EvaluateSDFCached`. |
| 4b — Surface roughness | 564 | Volumetric noise near surfaces (fBM/Ridged/Mixed). |
| 4c–4h — Terrain ops | 688 | Per-room op applied near surfaces. Sub-anchors below. |
| · Terracing | 727 | Step-like ledges. |
| · Layer lines | 823 | Horizontal grooves (sin of Z). |
| · Ribbing | 853 | Parallel ridges (sin of Z). |
| · Overhangs | 882 | Low-Z-freq noise shelves. |
| · Cliff sharpening | 918 | Amplify vertical gradient. |
| · Scallop | 962 | Cellular erosion bowls. |
| · Arch/Bridge | 998 | Hash-placed capsules across voids. |
| 4d — Columns | 1053 | Pre-baked vertical cylinders. |
| 4g — Domes | 1077 | Room-relative hemispherical ceilings. |
| 4h — Pinch | 1142 | Passage bottlenecks. |
| 5 — Worm tunnels | 1241 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
| 6 — Boundary seal | 1274 | Solid top/bottom shells (`ApplyBoundarySeal`). |
| 7 — Inter-strate passages + landings | `GetDensityWithParams`: 2217; `GetSlabDensity`: 2434+ | Carve wandering passage tubes and deterministic local landing rooms, restore their support floors, then leave the final XY seal to the outer MC post (`ApplyPassageModifier` / `ApplyPassageLandingFloorMC`). |
| 1 — Vertical scale | 3799 | Stretch Z before noise (`VerticalScale`). |
| 2 — Base density | 3808 | Everything starts solid at `BaseDensity`. |
| 3 — Cave warp | 3813 | Domain-warp the SDF query coords (organic shapes). |
| 4 — SDF morphology | 3863 | Rooms+tunnels via `BuildChunkCache`/`EvaluateSDFCached`. |
| 4b — Surface roughness | 4267 | Volumetric noise near surfaces (fBM/Ridged/Mixed). |
| 4c–4h — Terrain ops | 4395 | Per-room op applied near surfaces. Sub-anchors below. |
| · Terracing | 4437 | Step-like ledges. |
| · Layer lines | 4534 | Horizontal grooves (sin of Z). |
| · Ribbing | 4564 | Parallel ridges (sin of Z). |
| · Overhangs | 4593 | Low-Z-freq noise shelves. |
| · Cliff sharpening | 4629 | Amplify vertical gradient. |
| · Scallop | 4673 | Cellular erosion bowls. |
| · Arch/Bridge | 4709 | Hash-placed capsules across voids. |
| 4d — Columns | 4738 | Pre-baked vertical cylinders. |
| 4g — Domes | 4766 | Room-relative hemispherical ceilings. |
| 4h — Pinch | 4831 | Passage bottlenecks. |
| 5 — Worm tunnels | 4905 | abs(noise1)+abs(noise2), masked by distance-to-network (`WormNetworkRange`: braids hugging rooms/tunnels, no far-field speckle; 0 = legacy unmasked). |
| 6 — Boundary seal | 4987 | Solid top/bottom shells (`ApplyBoundarySeal`). |
| 7 — Inter-strate passages + landings | 5008 | Carve wandering passage tubes and deterministic local landing rooms, restore their support floors, then leave the final XY seal to the outer MC post (`ApplyPassageModifier` / `ApplyPassageLandingFloorMC`). |
### 4.2 `GetSlabDensity` (FlatPlain / CrystalChamber) — VoxelGenerator.cpp:1306
| Step | Line | What |
|------|------|------|
| 1 — Floor surface | 1317 | Noisy floor height. |
| 2 — Ceiling surface | 1343 | Formations hang downward (`abs(noise)`). |
| 3 — Void→base density | 1379 | Solid outside [floor,ceiling]. |
| 4 — Columns | 1399 | World-space hash grid, full-height. |
| 5+6 — Seal + passages | 1461 | Same seal, passage-tube, landing-room, connector, and support-floor path as TunnelNetwork. |
### 4.2 `BuildSlabStack` (FlatPlain / CrystalChamber) — VoxelDensityOpStack.cpp
| Step | Op | What |
|------|----|------|
| 1 — Floor surface | `FSlabVoidSource` | Noisy floor height (XY-pure). |
| 2 — Ceiling surface | `FSlabVoidSource` | Formations hang downward (`abs(noise)`). |
| 3 — Void→base density | `FSlabVoidSource` | Solid outside [floor,ceiling]. |
| 4 — Columns | `FGridColumnMod` | World-space hash grid, full-height. |
| 5+6 — Seal + passages | `AppendStructuralPost` | Spine, vertical seal, passage tube/landing floor, XY seal — the same structural post as every stack. |
---
@@ -786,25 +793,25 @@ Stage order (negative=solid throughout). Each stage's anchor:
| Goal | Location |
|------|----------|
| Chunk size / voxel scale | `VoxelTypes.h:19-23` (rebuild everything). |
| Chunk size / voxel scale | `VoxelTypes.h:211-213` (rebuild everything). |
| View distance / task budget / LOD distances | `VoxelSettings.h` (no recompile of logic — data asset). |
| LOD step mapping | `AVoxelWorld::LODToStep` VoxelWorld.cpp:268; `GetLODForChunk` :242. |
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:362. |
| Async threading / stale-result handling | `LoadTile` :445, `ProcessPendingChunks` :301, generation Epoch + per-request cancellation token. |
| LOD step mapping | `LoadTile` VoxelWorld.cpp:7329-7335: extent `CHUNK_SIZE << Level`, cells from `FullResClipLevels`/`CoarseTileCells`, `Step = Extent / Cells`; tile selection in `VoxelClipmapDesiredTiles::Build` (`Private/VoxelClipmapDesiredTiles.h`). |
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:6870. |
| Async threading / stale-result handling | `LoadTile` :7291, `ProcessPendingChunks` :5856, generation Epoch + per-request cancellation token. |
| Add a new cave feature / terrain op | Add enum in `VoxelTerrainOpDefinition.h:36`, params there, `ApplyTo` (.cpp:6), transport fields in `FStrateGenerationParams`, consume it in a new Step inside `GetDensityWithParams`. |
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :47 / `EvaluateSDFCached` :757. |
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:1241. |
| Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :10. |
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :6782 / `EvaluateSDFCached` :8569. |
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:4905. |
| Strate stacking / which strate where | `UVoxelStrateManager::Initialize` :1164. |
| Publish/use an offline season | Create `UVoxelSeasonAsset`, set `SourceManifestJson`, press `ImportSeasonManifestJson`, then assign it to `UVoxelSettings::Season`. |
| Boundary blend between strates | `GetGenerationParams` :515 + `FStrateGenerationParams::Lerp` (expands `VF_STRATE_PARAM_FIELDS`, StrateTypes.h — new fields go in that list). |
| Passages / landing geometry between strates | `GeneratePassages` :146 + `EvaluateModifierSDF` :371 + `ApplyPassageModifier` / `ApplyPassageLandingFloorMC` (StrateManager.cpp) + `ClassifyTile` floor guard (Generator.cpp). |
| Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:691/709 → `UVoxelDiffLayer::ApplyModification` :63. |
| Boundary blend between strates | `GetGenerationParams` :6509 + `FStrateGenerationParams::Lerp` (expands `VF_STRATE_PARAM_FIELDS`, StrateTypes.h — new fields go in that list). |
| Passages / landing geometry between strates | `GeneratePassages` :1659 + `EvaluateModifierSDF` :2643 + `ApplyPassageModifier` / `ApplyPassageLandingFloorMC` (StrateManager.cpp) + `ClassifyTile` floor guard (Generator.cpp). |
| Player carve/fill | `CarveAtPosition`/`FillAtPosition` VoxelWorld.cpp:8413/8422 → `UVoxelDiffLayer::ApplyModification` :185. |
| Mesh smoothness / normals | Grid-gradient in `GenerateMesh` (`GradAt` lambda), `IsoLevel` (h). |
| New slab/flat-world generator | `GetSlabDensity` Generator.cpp:1306 + `FSlabGenerationParams` (StrateTypes.h:1019). |
| New slab/flat-world generator | `VoxelDensityOps::BuildSlabStack` + `FSlabVoidSource` / `FGridColumnMod` (VoxelDensityOpStack.cpp) + `FSlabGenerationParams` (StrateTypes.h:1223). |
| Biome placement / layout | `BiomeMapParams` on the strate (cell size, warp, climate freqs) + each biome's climate box. Bake `AVoxelWorld::BakeBiomePreview` to tune. §8.14. |
| What a biome does to terrain | A full archetype param override on the biome (`bOverrideTerrain` + `SurfaceParams`); surface output-blends dominant/neighbour heights in `GetSurfaceDensity`. Caves = content/atmosphere only (determinism, §8.14). |
| What a biome does to terrain | A full archetype param override on the biome (`bOverrideTerrain` + `SurfaceParams`); surface output-blends dominant/neighbour heights in the surface stack (`BuildSurfaceStack` with per-biome params) and in `ComputeSurfaceColumn`. Caves = content/atmosphere only (determinism, §8.14). |
| Add a biome / biome content | New `UVoxelBiomeDefinition` asset → add to the strate's `Biomes[]`. §8.14 / §8.12. |
| Season reset | `AVoxelWorld::ChangeSeed` :740. |
| Season reset | `AVoxelWorld::ChangeSeed` :8710. |
---
+3 -3
View File
@@ -36,7 +36,7 @@ composer fills in; nobody opens it in the editor any more.
### This is op-stack "Phase 3", and it is REOPENED
The 2026-08-16 `OPSTACK-HANDOFF.md` says in bold *"Do not start Phase 3."* **That is superseded.**
The 2026-08-16 `Docs/archive/OPSTACK-HANDOFF.md` says in bold *"Do not start Phase 3."* **That is superseded.**
Asked what the world was missing, Jahni described exactly Phase 3 plus a composer above it.
It is **not** a restart of the refactor. The eight operators are live, composable, and production. What
@@ -1490,7 +1490,7 @@ roughness and should stay late in the stack.
Kept because each cost real time or would otherwise be repeated.
- ⛔ **"Do not start Phase 3" is SUPERSEDED.** `OPSTACK-HANDOFF.md` still says it; believe this file.
- ⛔ **"Do not start Phase 3" is SUPERSEDED.** `Docs/archive/OPSTACK-HANDOFF.md` still says it; believe this file.
- ⛔ **Jahni does not want to author strates either.** The composer must invent them. An earlier reading
had him authoring strates while the system arranged them — one level too shallow, and it shaped a
whole afternoon of wrong design.
@@ -1553,7 +1553,7 @@ build succeeded (14 actions). The full headless suite reported **22 succeeded, 0
determinism, box-verdict, and connectivity checks stayed green. The 4b→4h fuse was deliberately
not applied after the intervening-channel audit above.
**⭐ This closes the pending-verification thread opened by `OPSTACK-HANDOFF.md`.** That file flagged
**⭐ This closes the pending-verification thread opened by `Docs/archive/OPSTACK-HANDOFF.md`.** That file flagged
commits `4d33321` (Sol's boundary fold) and `91585ea` as *built but NOT re-verified*, with two
acceptance conditions. Both now met:
- **`violations` = 0** everywhere. Measured: TunnelNetwork at production defaults **12 of 40 tiles
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+39 -13
View File
@@ -49,19 +49,8 @@ Legend: ✅ verified against code · ◻️ checklist box.
> ("nothing is set in stone"). ✅ BUILT & WORKING together with pass 2 (ticked 2026-07-27):
> • **Terracing gradient Z-only** (6→2 SDF samples — see the ticked item above).
> • **Lerp X-macro** + **BakeRoomFeature dedupe** (both ticked above, bit-identical).
> • **T2.b LOD octave drop** — opt-in `UVoxelSettings::LODOctaveDrop` (default 0 = byte-identical);
> `VoxelGenLOD::OctaveBias` thread_local set per tile in `GenerateMesh`, per-voxel fractal sites
> wrapped in `VoxelGenLOD::Eff(N)`, XY-field noise deliberately excluded. ARCHITECTURE §8.10.
> • **T2.c tile component pool** — `TileComponentPool` + `Acquire/ReleaseTileComponent`; unload
> parks (RemoveSectionGroup strips geometry+collision, hidden, stays registered), apply pops.
> Bonus: `ApplyMeshToTile` reuses the existing `URealtimeMesh` (`GetRealtimeMeshAs`) — the old
> unconditional `InitializeRealtimeMesh` allocated + orphaned one mesh UObject PER APPLY.
> • **T2.d worker clamp** — `GetMaxConcurrentTasks()` caps the asset budget to logical cores − 2
> (the BackgroundNormal priority half had already shipped). **Tier 2 is now COMPLETE** —
> T2.a was already done (float SSE `VoxelNoise` core, §8.10).
> • **F2 determinism validator** — `AVoxelWorld::ValidateDeterminism` CallInEditor button:
> boundary points sampled under two cache-window alignments + a repeat pass; any non-zero
> delta = §8.4 regression. The tool that VERIFIES all the "bit-identical" claims above.
> • **T2.b / T2.c / T2.d and F2** shipped in the same batch — tracked in `fable-idea.md` (Tier 2
> status, F2) and ARCHITECTURE §8.10, not repeated here.
## Performance
- [x] ~~**`VoxelGenerator.cpp` (terrain-op gradient)** — compute once per voxel and reuse.~~
@@ -116,6 +105,43 @@ Legend: ✅ verified against code · ◻️ checklist box.
two-grid deco redesign (`StreamTier` / `DecorationNearRadiusChunks`).
**CORRECTION: `GetStrateChunkZBounds` is NOT dead** — `BuildDesiredTiles` uses it for the
strate-aware vertical clamp; struck from the dead list.
- [x] **2026-09 cleanup (`cleanup/2026-09`)** — the far sheet ring runtime (`GenerateSheetMesh` + its
streaming path) and ~40 unreferenced functions/members/types removed (each proven by a search of
`Source/`, `Tools/`, `Config/` and build files; see that branch's `refactor(dead-code)` commit).
Reflected (`UPROPERTY`/`UFUNCTION`/`UENUM`…) and enum candidates were NOT removed — see
*Owner decisions* below.
## Owner decisions (2026-09 cleanup)
Items left for the owner: unused data that assets, Blueprints, tools or tests may reference by name,
and changes that would alter generated terrain.
- [x] `UVoxelSettings::bFarSheetRing` / `FarSheetSpanLevels`, `voxel.TestFarSheetRing` and the
selector's sheet branch — removed (owner go-ahead); the harness no longer passes the switch and the
`sheet_tile` dump field stays for the parity compare.
- [x] `UVoxelSettings::CeilingViewMultiplier` / `CeilingBandChunks` / `DensityVolumeMaxTasks` — removed
(never read).
- [x] **Operator stack as the only density path** (cleanup/2026-09, owner-approved). Every strate —
authored, cooked-season (recipe or native) and editor composer candidate — now generates through its
operator stack; a degenerate strate is air. Removed: `UVoxelStrateDefinition::bUseOperatorStack`, the
archetype `switch` in `GetDensityAt`, `GetSlabDensity` / `GetMazeDensity` / `GetSurfaceDensity` /
`SurfaceDensityFromColumn` / `GetVerticalShaftDensity` / `GetFloatingIslandDensity`, the four
`Cave Bail Not Op Stack *` stats, the startup opt-in warning, commandlet `-opstack`, and the
legacy-vs-stack comparisons in the tests. **Terrain changes for any strate asset that did not
already have the opt-in ticked.**
- [ ] **No-strate-manager fallback world.** A world whose settings have no season, pool or fixed
strates runs `GetDensityAt`'s fallback: default `FStrateGenerationParams` through
`GetDensityWithParams` with its direct-caller ("legacy") structural posts. It is the only caller of
that post path outside tests/commandlet diagnostics. Decide whether an empty configuration should
keep generating generic caves or refuse to generate.
- [ ] `UVoxelStrateDefinition::WaterColor`, `Creatures`, `AmbientSound`, `Music`, `MusicVolume` and the
`FStrateCreature` / `FStrateAmbientActor` spawn fields (`SpawnChancePerChunk`, `MaxPerStrate`,
`SpawnWeight`) — authored data with no consuming system yet (F9 audio, creatures).
- [ ] `UVoxelBiomeDefinition::WaterMaterial` — never read; the water plane uses the strate's
`WaterMaterial` only.
- [ ] Plain C++ enums with no user: `EVoxelFace` (`VoxelTypes.h`), `EVoxelSeasonSelectionReason`
(`VoxelSeasonManifest.h`), `EVoxelOpCombine` (`VoxelDensityOp.h` — its comment carries the
two-channel sign convention; keep that text if the enum goes).
- [ ] `IVoxelHeightOp::MaxDisplacement` / `FVoxelHeightStack::MaxTotalDisplacement` — no caller; kept as
the documented hook for a future heightfield `ClassifyBox`.
## Over-complexity (behavior-preserving splits, optional)
@@ -39,12 +39,8 @@
#include "Misc/AutomationTest.h"
#include "VoxelForgeTestFixture.h"
// Inclus ici DÉLIBÉRÉMENT : VoxelDensityOp.h n'est encore inclus par aucun .cpp, donc le
// compilateur ne le verrait jamais. Le fold qu'il définit prétend reproduire ClassifyTile — ce
// fichier est l'endroit naturel pour que cette prétention soit à la fois COMPILÉE et TESTÉE.
// Deliberately included here: VoxelDensityOp.h is not yet included by any .cpp, so the compiler
// would never see it. Its fold claims to reproduce ClassifyTile, so this is the natural place for
// that claim to be both compiled and tested.
// Le fold défini par VoxelDensityOp.h prétend reproduire ClassifyTile : ce fichier le teste.
// The fold defined in VoxelDensityOp.h claims to reproduce ClassifyTile; this file tests it.
#include "VoxelDensityOp.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
@@ -153,7 +149,7 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("ClassifyTile verdicts over %d scanned tiles: Mixed %d, AllSolid %d, AllAir %d ")
TEXT("(brute-forcing %d of them). Cave archetypes now use the opt-in operator-stack ")
TEXT("(brute-forcing %d of them). Cave archetypes use the operator-stack ")
TEXT("ClassifyBox fold; the global XY edge proof also skips proven outer-shell tiles. A low ")
TEXT("non-Mixed count is expected: every uncertain case remains Mixed for safety."),
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
@@ -215,10 +211,10 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
TestEqual(TEXT("no tile was classified uniform while containing a surface (a false verdict is a hole)"),
NumHoles, 0);
// ── Stabilité du verdict : ClassifyTile partage GSurfColCache avec GetDensityAt, donc le
// brute-force ci-dessus a réchauffé les caches. Re-classifier doit rendre le MÊME verdict.
// Verdict stability: ClassifyTile shares GSurfColCache with GetDensityAt, so the brute force
// above warmed the caches. Re-classifying must yield the SAME verdict.
// ── Stabilité du verdict : le premier ClassifyTile a réchauffé son cache de colonnes et le
// brute-force ci-dessus les caches de GetDensityAt. Re-classifier doit rendre le MÊME verdict.
// Verdict stability: the first ClassifyTile warmed its column cache and the brute force
// above warmed GetDensityAt's caches. Re-classifying must yield the SAME verdict.
for (const FTileSpec& Spec : ToVerify)
{
const EVoxelTileClass A = Gen->ClassifyTile(Spec.Origin, Spec.Step, Spec.Cells);
@@ -237,25 +233,24 @@ bool FVoxelForgeClassifyTileTest::RunTest(const FString& Parameters)
}
//=============================================================================
// LE CHEMIN PILE D'OPÉRATEURS DE ClassifyTile — MÊME FORCE BRUTE, MONDE OPT-IN
// LE CHEMIN PILE D'OPÉRATEURS DE ClassifyTile — MÊME FORCE BRUTE, STRATES DE CAVE
//=============================================================================
// `ClassifyTile` rendait `Mixed` sans appel pour tout archétype de CAVE. Il consulte désormais
// `FVoxelOpStack::ClassifyBox` quand la strate a coché `bUseOperatorStack` — donc **un tout nouveau
// chemin peut faire sauter le maillage d'une tuile**, et son erreur est un TROU : pas de triangles,
// pas de collision, invisible jusqu'à ce qu'un joueur tombe au travers.
// Pour un slot de CAVE, `ClassifyTile` consulte `FVoxelOpStack::ClassifyBox` — donc **la pile peut
// faire sauter le maillage d'une tuile**, et son erreur est un TROU : pas de triangles, pas de
// collision, invisible jusqu'à ce qu'un joueur tombe au travers.
//
// Ce test est le même oracle par force brute que `ClassifyTileSoundness`, sur un monde dont TOUTES
// les strates ont coché la case. Il ne vérifie pas le pliage (c'est `BoxVerdictFold`) ni les
// opérateurs (ce sont les huit tests d'équivalence) : il vérifie le **câblage** — que la pile
// interrogée par le classifieur est bien celle qui produit la densité, params, drapeau et
// disturbances compris.
// Ce test est le même oracle par force brute que `ClassifyTileSoundness`, avec un tirage uniforme
// sur tout le layout. Il ne vérifie pas le pliage (c'est `BoxVerdictFold`) ni les opérateurs : il
// vérifie le **câblage** — que la pile interrogée par le classifieur est bien celle qui produit la
// densité, params et disturbances compris.
//
// ⚠️ LE COMPTEUR À LIRE EN PREMIER est le nombre de tuiles réellement brute-forcées. Un run vert
// avec zéro verdict non-Mixed ne prouverait RIEN — exactement le piège que ce fichier documente
// depuis sa première version, et la raison pour laquelle l'absence de verdict est une ERREUR ici.
//
// Same brute-force oracle as ClassifyTileSoundness, on a world where every strate has opted in.
// It checks the WIRING, not the fold and not the operators.
// Same brute-force oracle as ClassifyTileSoundness, drawn uniformly over the whole layout so cave
// slots (which classify through their stack) dominate. It checks the WIRING, not the fold and not
// the operators.
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
FVoxelForgeOpStackClassifyTileTest,
@@ -266,11 +261,10 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
{
using namespace VoxelForgeTest;
// ⚠️ `bUseOperatorStack = true` sur toutes les strates : c'est LE point du test. La fixture
// donne à ce monde une `LayoutVersion` unique dans le processus, sans quoi les caches par chunk
// de `GetDensityAt` — dont `CP_UseOpStack` — pourraient encore porter ceux d'un autre test.
// La fixture donne à ce monde une `LayoutVersion` unique dans le processus, sans quoi les caches
// par chunk de `GetDensityAt` — dont `CP_UseOpStack` — pourraient encore porter ceux d'un autre test.
FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
World.Build(/*Seed*/1337, /*GapChunks*/2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -287,8 +281,8 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
const int32 BottomVoxelZ = World.BottomChunkZ * CHUNK_SIZE;
// Tirage uniforme sur tout le layout, PAS biaisé vers SurfaceWorld comme l'autre test : ici ce
// sont précisément les strates de cave qui intéressent, puisque ce sont elles qui passent par le
// nouveau chemin. SurfaceWorld continue d'être prouvé par le code écrit à la main.
// sont précisément les strates de cave qui intéressent, puisque ce sont elles que ClassifyTile
// juge par leur pile. SurfaceWorld est prouvé par le test colonne écrit à la main.
for (int32 t = 0; t < NumTilesScanned; ++t)
{
FTileSpec Spec;
@@ -316,20 +310,17 @@ bool FVoxelForgeOpStackClassifyTileTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("ClassifyTile ON THE OPERATOR-STACK PATH, %d scanned tiles: Mixed %d, AllSolid %d, ")
TEXT("AllAir %d (brute-forcing %d). Compare with VoxelForge.Determinism.ClassifyTileSoundness, ")
TEXT("which runs the SAME scan on a world that has NOT opted in: every verdict beyond what ")
TEXT("that test reports is a tile the mesher now skips and did not before. That difference ")
TEXT("IS the T1.d prize OPSTACK-PLAN has been aiming at -- and every one of those tiles is a ")
TEXT("hole if the wiring is wrong, which is what the brute force below is for."),
TEXT("AllAir %d (brute-forcing %d). Every cave-slot verdict is a tile the mesher skips on ")
TEXT("the strength of the stack's ClassifyBox fold -- and a hole if the wiring is wrong, ")
TEXT("which is what the brute force below is for."),
NumTilesScanned, NumMixed, NumAllSolid, NumAllAir, ToVerify.Num()));
if (ToVerify.Num() == 0)
{
AddError(TEXT("VACUOUS: not one tile got a non-Mixed verdict on the operator-stack path, so ")
TEXT("this test verified NOTHING about the new wiring. Either no strate actually ")
TEXT("opted in (check FTestWorld::Build's bUseOperatorStack), or every guard in the ")
TEXT("cave branch of ClassifyTile bailed to Mixed -- the params-identical check and ")
TEXT("the 27-chunk-coord cap are the likeliest. Do NOT read a green run as proof."));
TEXT("this test verified NOTHING about the wiring. Every guard in the cave branch ")
TEXT("of ClassifyTile bailed to Mixed -- the params-identical check and the ")
TEXT("27-chunk-coord cap are the likeliest. Do NOT read a green run as proof."));
return false;
}
@@ -122,22 +122,15 @@ namespace
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
const int32 OuterRadius = VoxelClipmapDesiredTiles::OuterShellRadius(
Parameters, Radius, MaxLevel);
int32 SheetLevel = MaxLevel;
int32 SheetRadius = OuterRadius;
VoxelClipmapDesiredTiles::OuterShell(
Parameters, Radius, MaxLevel, SheetLevel, SheetRadius);
const bool bSheetRing = SheetLevel > MaxLevel;
const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel;
for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level)
for (int32 Level = 0; Level <= MaxLevel; ++Level)
{
const int32 Pow = 1 << Level;
const FIntVector CenterLevel = VoxelClipmapDesiredTiles::FloorDiv(Center, Pow);
const FIntVector CenterFiner = Level > 0
? VoxelClipmapDesiredTiles::FloorDiv(Center, Pow >> 1)
: FIntVector::ZeroValue;
const int32 LevelRadius = (Level == MaxLevel && !bSheetRing)
? OuterRadius : Radius;
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
int32 DeltaZMin = -LevelRadius;
int32 DeltaZMax = LevelRadius;
@@ -178,50 +171,6 @@ namespace
OutTiles.Emplace(Tile, Level);
}
}
if (bSheetRing)
{
const int32 SheetPow = 1 << SheetLevel;
const FIntVector CenterSheet = VoxelClipmapDesiredTiles::FloorDiv(Center, SheetPow);
const FIntVector CenterMC = VoxelClipmapDesiredTiles::FloorDiv(
Center, 1 << MaxMCRingLevel);
const int32 LevelDelta = SheetLevel - MaxMCRingLevel;
int32 DeltaZMin = -SheetRadius;
int32 DeltaZMax = SheetRadius;
if (MinChunkZ != MIN_int32)
{
DeltaZMin = FMath::Max(DeltaZMin,
VoxelClipmapDesiredTiles::FloorDiv(MinChunkZ, SheetPow) - CenterSheet.Z);
DeltaZMax = FMath::Min(DeltaZMax,
VoxelClipmapDesiredTiles::FloorDiv(MaxChunkZ, SheetPow) - CenterSheet.Z);
}
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
for (int32 DeltaY = -SheetRadius; DeltaY <= SheetRadius; ++DeltaY)
for (int32 DeltaX = -SheetRadius; DeltaX <= SheetRadius; ++DeltaX)
{
const FIntVector Tile = CenterSheet + FIntVector(DeltaX, DeltaY, DeltaZ);
const int32 TileMinZ = Tile.Z << SheetLevel;
const int32 TileMaxZ = ((Tile.Z + 1) << SheetLevel) - 1;
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
{
continue;
}
const bool bCovered =
((Tile.X << LevelDelta) >= CenterMC.X - Radius)
&& ((((Tile.X + 1) << LevelDelta) - 1) <= CenterMC.X + Radius)
&& ((Tile.Y << LevelDelta) >= CenterMC.Y - Radius)
&& ((((Tile.Y + 1) << LevelDelta) - 1) <= CenterMC.Y + Radius)
&& ((Tile.Z << LevelDelta) >= CenterMC.Z - Radius)
&& ((((Tile.Z + 1) << LevelDelta) - 1) <= CenterMC.Z + Radius);
if (!bCovered)
{
OutTiles.Emplace(Tile, SheetLevel);
}
}
}
}
static FString DescribeMeasure(const FOverlapMeasure& Measure)
@@ -245,7 +194,6 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
Owner.ClipRadius = 3;
Owner.MaxClipLevel = 5;
Owner.RenderDistanceChunks = 768;
Owner.bFarSheetRing = false;
FParameters Defaults;
@@ -291,16 +239,13 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
int32 Radius;
int32 MaxLevel;
int32 RenderDistance;
bool bSheet;
int32 SheetSpan;
};
static const FSweepSettings SweepSettings[] = {
{1, 0, 0, false, 2},
{2, 2, 0, false, 2},
{3, 4, 0, false, 2},
{2, 3, 49, false, 2},
{2, 3, 49, true, 2},
{2, 4, 87, true, 3},
{1, 0, 0},
{2, 2, 0},
{3, 4, 0},
{2, 3, 49},
{2, 4, 87},
};
int32 SweptCases = 0;
@@ -319,8 +264,6 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
P.ClipRadius = Sweep.Radius;
P.MaxClipLevel = Sweep.MaxLevel;
P.RenderDistanceChunks = Sweep.RenderDistance;
P.bFarSheetRing = Sweep.bSheet;
P.FarSheetSpanLevels = Sweep.SheetSpan;
// Exercise the unbounded view, a tight local clamp, and a clamp crossing a tile boundary.
for (int32 ClampCase = 0; ClampCase < 3; ++ClampCase)
@@ -353,9 +296,9 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
if (FirstFailure.IsEmpty())
{
FirstFailure = FString::Printf(
TEXT("overlap center=(%d,%d,%d) radius=%d level=%d distance=%d sheet=%d clamp=%d: %s"),
TEXT("overlap center=(%d,%d,%d) radius=%d level=%d distance=%d clamp=%d: %s"),
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
Sweep.bSheet ? 1 : 0, ClampCase, *DescribeMeasure(CurrentOverlap));
ClampCase, *DescribeMeasure(CurrentOverlap));
}
}
if (!CoversEveryLegacyTile(Legacy, Current, MinZ, MaxZ))
@@ -364,9 +307,9 @@ bool FVoxelForgeClipmapNoOverlapTest::RunTest(const FString& Parameters)
if (FirstFailure.IsEmpty())
{
FirstFailure = FString::Printf(
TEXT("coverage center=(%d,%d,%d) radius=%d level=%d distance=%d sheet=%d clamp=%d"),
TEXT("coverage center=(%d,%d,%d) radius=%d level=%d distance=%d clamp=%d"),
X, Y, Z, Sweep.Radius, Sweep.MaxLevel, Sweep.RenderDistance,
Sweep.bSheet ? 1 : 0, ClampCase);
ClampCase);
}
}
}
@@ -356,7 +356,7 @@ bool FVoxelForgeComposerCorpusFreeTest::RunTest(const FString& Parameters)
Corpus.GetEntries().Num(), 12);
FTestWorld World;
World.Build(1337, 2, true);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -955,7 +955,7 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(1337, 2, true);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -1117,7 +1117,6 @@ bool FVoxelForgeComposerParameterRollTest::RunTest(const FString& Parameters)
CandidateDefinition->SurfaceParams = Roll.ArchetypeParams.SurfaceParams;
CandidateDefinition->VerticalShaftParams = Roll.ArchetypeParams.VerticalShaftParams;
CandidateDefinition->FloatingIslandParams = Roll.ArchetypeParams.FloatingIslandParams;
CandidateDefinition->bUseOperatorStack = true;
CandidateDefinition->TransitionType = EVoxelStrateTransition::Hard;
World.Reinitialize();
@@ -1342,15 +1341,13 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
constexpr int32 ExpectedLatticeSamples = 16 * 16 * 16;
FString Table = TEXT(
"TERRAIN DETAIL LIVENESS (same 4096-point lattice; only one group changed per row)\n"
"path | group | samples | baseline digest | variant digest | baseline mean | variant mean | "
"group | samples | baseline digest | variant digest | baseline mean | variant mean | "
"changed | sign changes | sum abs delta | max abs delta | verdict\n");
int32 TotalRows = 0;
int32 FailedRows = 0;
for (const bool bUseOperatorStack : { false, true })
{
FTestWorld World;
World.Build(1337, 2, bUseOperatorStack);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -1365,17 +1362,11 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
return false;
}
Definition->TerrainOperations.Reset();
TestEqual(
FString::Printf(TEXT("%s fixture has no terrain-op asset pool"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
Definition->TerrainOperations.Num(), 0);
TestEqual(TEXT("fixture has no terrain-op asset pool"), Definition->TerrainOperations.Num(), 0);
TArray<FVector> Points;
VF_BuildTerrainDetailLattice(World, Points);
TestEqual(
FString::Printf(TEXT("%s experiment uses the fixed lattice"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")),
Points.Num(), ExpectedLatticeSamples);
TestEqual(TEXT("experiment uses the fixed lattice"), Points.Num(), ExpectedLatticeSamples);
FStrateGenerationParams BaselineParams = Definition->GenerationParams;
VF_ClearTerrainDetailFields(BaselineParams);
@@ -1403,9 +1394,7 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]);
}
TestTrue(
FString::Printf(TEXT("%s baseline is stable before %s probe"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Spec.Name),
FString::Printf(TEXT("baseline is stable before %s probe"), Spec.Name),
bSameBaseline);
}
@@ -1428,14 +1417,11 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
++FailedRows;
}
TestTrue(
FString::Printf(TEXT("%s %s field group has the measured liveness verdict"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
Spec.Name),
FString::Printf(TEXT("%s field group has the measured liveness verdict"), Spec.Name),
bVerdictMatches);
Table += FString::Printf(
TEXT("%s | %s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
TEXT("%s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"),
Spec.Name,
Points.Num(),
BaselineSummary.Digest,
@@ -1448,17 +1434,16 @@ bool FVoxelForgeTerrainDetailLivenessTest::RunTest(const FString& Parameters)
Delta.MaxAbs,
bObservedLive ? TEXT("LIVE") : TEXT("DEAD"));
}
}
AddInfo(Table);
AddInfo(FString::Printf(
TEXT("Terrain detail liveness: %d group/path rows measured, %d mismatches; LIVE groups "
TEXT("Terrain detail liveness: %d group rows measured, %d mismatches; LIVE groups "
"must have at least one bit-changed density and DEAD groups must have none. No "
"terrain-operation assets were present in the fixture pool."),
TotalRows, FailedRows));
TestEqual(TEXT("all terrain detail liveness rows matched their measured verdict"), FailedRows, 0);
TestEqual(TEXT("both density paths measured every terrain detail group"), TotalRows,
static_cast<int32>(2 * UE_ARRAY_COUNT(GTerrainDetailProbeSpecs)));
TestEqual(TEXT("every terrain detail group was measured"), TotalRows,
static_cast<int32>(UE_ARRAY_COUNT(GTerrainDetailProbeSpecs)));
return FailedRows == 0;
}
@@ -394,7 +394,7 @@ bool FVoxelForgeComposerPromotionTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(1337, 2, true);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -1590,7 +1590,7 @@ bool FVoxelForgeComposerShowcaseTest::RunTest(const FString& Parameters)
FTestWorld World;
// The shared fixture defaults to a bounded 4-chunk volume for fast density tests. The
// owner-facing scale audit must exercise the production/default 8-chunk envelope.
World.Build(BaseComposerSeed, 2, true, 8);
World.Build(BaseComposerSeed, 2, 8);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -262,7 +262,7 @@ bool FVoxelForgeComposerStructureRollTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(1337, 2, true);
World.Build(1337, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -132,7 +132,7 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Parameters)
{
FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
World.Build(/*Seed*/1337, /*GapChunks*/2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -208,8 +208,8 @@ bool FVoxelForgeTileCacheContextDeterminismTest::RunTest(const FString& Paramete
// level-0 tile at the same origin is compared with a fresh world's level-0 output.
FTestWorld WarmWorld;
FTestWorld FreshWorld;
WarmWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
FreshWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
WarmWorld.Build(/*Seed*/1337, /*GapChunks*/2);
FreshWorld.Build(/*Seed*/1337, /*GapChunks*/2);
if (!WarmWorld.IsValid() || !FreshWorld.IsValid())
{
AddError(WarmWorld.IsValid() && FreshWorld.IsValid()
@@ -348,7 +348,7 @@ bool FVoxelForgeDensityBlockEquivalenceTest::RunTest(const FString& Parameters)
for (int32 SeedIndex = 0; SeedIndex < UE_ARRAY_COUNT(Seeds); ++SeedIndex)
{
VoxelForgeTest::FTestWorld World;
World.Build(Seeds[SeedIndex], /*gap chunks=*/2, /*operator stack=*/true);
World.Build(Seeds[SeedIndex], /*gap chunks=*/2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -470,8 +470,8 @@ bool FVoxelForgeDensityBlockEquivalenceTest::RunTest(const FString& Parameters)
// No-edit grids. In the two canonical network archetypes this enters the actual worker
// block session, which partitions each mesher grid by resolved chunk before running the
// active operator list. The six legacy density routes still run their production scalar
// evaluator, as they do outside the block-safe network branch.
// active operator list. The six other archetypes run the scalar stack evaluator, as they
// do in production outside the block-safe network branch.
for (int32 GridIndex = 0; GridIndex < GridSetCount; ++GridIndex)
{
BaseResults[GridIndex] = CompareGeneratorBlockWithScalar(
@@ -13,8 +13,8 @@
// seam, and in multiplayer a world divergence.
//
// POURQUOI CE TEST EXISTE / WHY THIS TEST EXISTS:
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, GSurfColCache, les
// slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes
// ~30 caches thread_local à clé manuelle vivent sous GetDensityAt (CP_*, les mémos par cellule
// et par colonne des piles, les slots de diff, le cache SDF). Chacun est correct exactement tant que sa CLÉ contient toutes
// les entrées dont dépend la valeur cachée. Une entrée oubliée ne casse rien tout de suite :
// elle produit une mauvaise valeur seulement quand le cache est chaud pour une AUTRE entrée —
// c'est-à-dire de façon intermittente, dépendante de l'ordre, et invisible en jeu jusqu'à ce
@@ -351,7 +351,7 @@ bool FVoxelForgeLiveEditInvalidationTest::RunTest(const FString& Parameters)
TEXT("STALE PARAMS (AUDIT C2): the SurfaceWorld heightfield params were tripled and the ")
TEXT("layout rebuilt, yet all %d probe densities are bit-identical. A per-chunk cache is ")
TEXT("still keyed on ChunkCoord alone and skipped its refetch. Suspects, in order: ")
TEXT("CP_Chunk/CP_Version in GetDensityAt, the GSurfColCache box key, and the ")
TEXT("CP_Chunk/CP_Version in GetDensityAt, the FSurfaceColumnSource column memo key, and the ")
TEXT("FChunkBiomeCache validity box (which says nothing about the FBiomeContext its ")
TEXT("cells were classified against)."), Probes.Num()));
}
@@ -761,14 +761,13 @@ namespace
const float SimulatedCarvedFinalDensity = -SimulatedCarvedInternalDensity;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeDescentWriterContext] seed=0 sample=(%.3f,%.3f,%.3f) "
"chunk=(%d,%d,%d) archetype=%s asset_stack_flag=%d effective_op_stack=%d "
"chunk=(%d,%d,%d) archetype=%s effective_op_stack=%d "
"passage_sdf=%.6f simulated_passage_carve_density=%.6f "
"terrain_ops='%s' terrain_op_paths='%s' "
"base=%.3f tunnel_columns=(%.3f,%.3f,%.3f) slab_columns=(%.3f,%.3f,%.3f)"),
OutSamples.FirstSolidPosition.X, OutSamples.FirstSolidPosition.Y,
OutSamples.FirstSolidPosition.Z, ChunkCoord.X, ChunkCoord.Y, ChunkCoord.Z,
*GeneratorName, Definition != nullptr && Definition->bUseOperatorStack ? 1 : 0,
bUsesOperatorStack ? 1 : 0, PassageSDF, SimulatedCarvedFinalDensity,
*GeneratorName, bUsesOperatorStack ? 1 : 0, PassageSDF, SimulatedCarvedFinalDensity,
*TerrainOps, *TerrainOpPaths, Params.BaseDensity, Params.ColumnDensity,
Params.ColumnMinRadius, Params.ColumnMaxRadius,
Definition != nullptr ? Definition->SlabParams.ColumnDensity : -1.0f,
@@ -8,23 +8,15 @@
// voudrait dire soit un canal par-voxel pour une propriété de COLONNE, soit un seul opérateur
// opaque — ce que `OPSTACK-PLAN §2.5` appelle exactement l'échec du refactor.
//
// D'où une seconde famille, `VoxelHeightOp.h`. **Ce test est ce qui dit si elle était une bonne
// idée** — la même méthode que la Phase 1 a appliquée à la densité : décomposer, puis MESURER
// contre l'original, avant de construire par-dessus.
// D'où une seconde famille, `VoxelHeightOp.h`, mesurée ici contre `ComputeSurfaceTerrainZ`.
//
// ⚠️ CE QUE CE TEST COUVRE
// ✅ la pile de HAUTEUR du sol, contre `ComputeSurfaceTerrainZ` (défauts puis tous les ops F20) ;
// ✅ les canaux de densité courants : ordre, fenêtre et overhang ;
// ✅ la pile de densité SurfaceWorld : décomposition, ordre des canaux, densité finie, overhang ;
// ✅ le mélange synthétique à poids zéro.
//
// The old GetSurfaceDensity helper remains a differential diagnostic only: it does not carry the
// current common structural post. The assertions target the height stack and the density stack
// used by the owner path, so room ownership and the authored-floor removal are not mistaken for
// a failed height port.
//
// LA BARRE : **bit à bit** remains the rule for height-space operations. Density comparisons to
// the retired direct helper are reported, not asserted, because room ownership and the common
// MC post intentionally make those paths different.
// LA BARRE : **bit à bit** pour les opérations en espace hauteur.
// The bar is bit-identity for height-space operations.
#if WITH_DEV_AUTOMATION_TESTS
@@ -285,18 +277,9 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
// ÉTAPE 2a — LE PONT VERS L'ESPACE DENSITÉ
//=========================================================================
// `FSurfaceColumnSource` consomme les DEUX piles de hauteur (sol + voûte) et rend une densité.
// La référence est `GetSurfaceDensity`, qui est exactement la variante **sans overhang**
// (il passe `OverhangAmp = 0`) et **sans biomes** (ParamsD == ParamsN, poids 0) — donc la
// comparaison est nette plutôt qu'approximative.
//
// ⚠️ Ce que ce bloc NE teste PAS, et qu'il ne faut pas croire testé : l'overhang et le mélange
// de biomes. Tous deux arrivent à l'étape 2b, avec le chemin CACHÉ pour référence — c'est le
// seul qui les calcule.
// Cette passe est la variante **sans overhang** et **sans biomes** ; l'overhang a sa propre
// passe juste en dessous. / No-overhang, no-biome variant; the overhang has its own pass below.
{
// ⚠️ `OverhangStrength = 0` EXPLICITEMENT : `GetSurfaceDensity` passe `OverhangAmp = 0`,
// donc il n'en calcule aucun. Comparer une pile qui en produit à une référence qui n'en
// produit pas ferait échouer le test pour la seule raison que la référence est incomplète.
// L'overhang a sa propre passe juste en dessous, avec le bon oracle.
FSurfaceGenerationParams P = AllOps;
P.OverhangStrength = 0.0f;
@@ -318,8 +301,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
Ctx.StrateBottomWorldZ = P.StrateBottomWorldZ;
Stack.PrepareChunk(Ctx);
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumNonFinite = 0;
float WorstDelta = 0.0f;
int32 NumNonFinite = 0;
FRandomStream Rng(5150);
for (int32 i = 0; i < NumHeightSamples; ++i)
@@ -327,44 +309,23 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
const float X = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
const float Y = (float)Rng.RandRange(-4 * CHUNK_SIZE, 4 * CHUNK_SIZE);
const float Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
// ParamsD == ParamsN, poids 0 ⇒ une seule évaluation, pas de biomes.
const float Old = Gen->GetSurfaceDensity(X, Y, Z, P, P, 0.0f);
const float New = Stack.EvalMC(X, Y, Z);
if (!FMath::IsFinite(New))
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
{
++NumNonFinite;
}
if (!BitEqual(Old, New))
{
++NumDiff;
const float Delta = FMath::Abs(Old - New);
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
}
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
}
AddInfo(FString::Printf(
TEXT("SurfaceWorld density legacy diagnostic: %d of %d samples differ from "
"GetSurfaceDensity (largest |delta| %.9g), %d side disagreements; "
"current stack non-finite samples=%d."),
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumNonFinite));
TestEqual(TEXT("surface: current density stack has no non-finite samples"),
NumNonFinite, 0);
}
//=========================================================================
// ÉTAPE 2b — L'OVERHANG, contre le SEUL oracle qui le calcule
// ÉTAPE 2b — L'OVERHANG
//=========================================================================
// `GetSurfaceDensity` passe `OverhangAmp = 0`. La seule référence est donc le chemin caché :
// `ComputeSurfaceColumn` (qui résout le gate et la direction amont par colonne) suivi de
// `SurfaceDensityFromColumn` (qui applique l'union par voxel). Les deux viennent d'être
// exposées pour ça.
//
// C'est aussi la passe qui vérifie le MÉMO DE COLONNE de `FSurfaceColumnSource` : l'op overhang
// lit la colonne produite par la source, et s'ils divergeaient d'un XY, l'union se ferait au
// mauvais endroit. Un mémo mal clé se verrait ici.
// `ComputeSurfaceColumn` résout le gate et la direction amont par colonne ; la moitié des
// échantillons est placée DANS la fenêtre d'overhang qu'il rapporte, pour que l'op overhang de
// la pile soit réellement exercé. / Half the samples are placed inside the overhang window that
// ComputeSurfaceColumn reports, so the stack's overhang op is actually exercised.
{
FSurfaceGenerationParams P = AllOps;
P.OverhangStrength = 0.8f;
@@ -391,8 +352,7 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
TArray<FSurfaceGenerationParams> NoBiomeParams;
FChunkBiomeCache BiomeCache;
int32 NumDiff = 0, NumSideDisagree = 0, NumInWindow = 0, NumNonFinite = 0;
float WorstDelta = 0.0f;
int32 NumInWindow = 0, NumNonFinite = 0;
FRandomStream Rng(1337);
for (int32 i = 0; i < NumHeightSamples; ++i)
@@ -418,27 +378,16 @@ bool FVoxelForgeHeightStackTest::RunTest(const FString& Parameters)
Z = (float)Rng.RandRange(BottomVoxelZ, TopVoxelZ);
}
const float Old = Gen->SurfaceDensityFromColumn(X, Y, Z, TerrainZ, CeilSurf,
Amp, DirX, DirY, P);
const float New = Stack.EvalMC(X, Y, Z);
if (!FMath::IsFinite(New))
if (!FMath::IsFinite(Stack.EvalMC(X, Y, Z)))
{
++NumNonFinite;
}
if (!BitEqual(Old, New))
{
++NumDiff;
WorstDelta = FMath::Max(WorstDelta, FMath::Abs(Old - New));
}
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
}
AddInfo(FString::Printf(
TEXT("Overhang legacy diagnostic: %d of %d samples differ from "
"SurfaceDensityFromColumn (largest |delta| %.9g), %d side disagreements, "
"%d deliberately inside the overhang window; current stack non-finite samples=%d."),
NumDiff, NumHeightSamples, WorstDelta, NumSideDisagree, NumInWindow, NumNonFinite));
TEXT("Overhang: %d of %d samples deliberately inside the overhang window; current "
"stack non-finite samples=%d."),
NumInWindow, NumHeightSamples, NumNonFinite));
TestEqual(TEXT("overhang: current density stack has no non-finite samples"),
NumNonFinite, 0);
@@ -549,7 +549,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(Seed, 2, false);
World.Build(Seed, 2);
int32 TopWorldZ = 0;
int32 BottomWorldZ = 0;
FVoxelStrateRegionManifest Manifest;
@@ -612,7 +612,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
}
FTestWorld World;
World.Build(Seed, 2, false);
World.Build(Seed, 2);
int32 TopWorldZ = 0;
int32 BottomWorldZ = 0;
FVoxelStrateRegionManifest Manifest;
@@ -661,7 +661,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
TestTrue(TEXT("found a single-region recipe identity case"), bRecipeIdentityChecked);
FTestWorld World;
World.Build(MultiSeed, 2, false);
World.Build(MultiSeed, 2);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -955,7 +955,7 @@ bool FVoxelForgeLateralRegionsTest::RunTest(const FString& Parameters)
}
FTestWorld LawWorld;
LawWorld.Build(Seed, 2, false);
LawWorld.Build(Seed, 2);
FVoxelStrateRegionManifest LawManifest;
FVoxelOpStack LawStack;
FVoxelOpContext LawContext;
@@ -100,65 +100,45 @@ bool FVoxelForgeMazeSeamTest::RunTest(const FString& Parameters)
{
using namespace VoxelForgeTest;
FTestWorld LegacyWorld;
LegacyWorld.Build(7331, 2, false, 4);
FTestWorld StackWorld;
StackWorld.Build(7331, 2, true, 4);
if (!LegacyWorld.IsValid() || !StackWorld.IsValid())
FTestWorld World;
World.Build(7331, 2, 4);
if (!World.IsValid())
{
AddError(LegacyWorld.IsValid() ? StackWorld.WhyInvalid() : LegacyWorld.WhyInvalid());
AddError(World.WhyInvalid());
return false;
}
int32 LegacyTop = 0, LegacyBottom = 0;
int32 StackTop = 0, StackBottom = 0;
if (!LegacyWorld.GetSlotVoxelZRange(FTestWorld::SlotMaze, LegacyTop, LegacyBottom)
|| !StackWorld.GetSlotVoxelZRange(FTestWorld::SlotMaze, StackTop, StackBottom))
int32 MazeTopZ = 0, MazeBottomZ = 0;
if (!World.GetSlotVoxelZRange(FTestWorld::SlotMaze, MazeTopZ, MazeBottomZ))
{
AddError(TEXT("The seam fixture has no Maze slot."));
return false;
}
const int32 LegacyMidChunkZ = ((LegacyTop + LegacyBottom) / 2) / CHUNK_SIZE;
const int32 StackMidChunkZ = ((StackTop + StackBottom) / 2) / CHUNK_SIZE;
const FMazeGenerationParams LegacyParams = LegacyWorld.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, LegacyMidChunkZ));
const FMazeGenerationParams StackParams = StackWorld.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, StackMidChunkZ));
const int32 MazeMidChunkZ = ((MazeTopZ + MazeBottomZ) / 2) / CHUNK_SIZE;
const FMazeGenerationParams MazeParams = World.StrateManager->GetMazeParamsForChunk(
FIntVector(0, 0, MazeMidChunkZ));
TArray<FVector> LegacyProbes;
TArray<FVector> StackProbes;
AddMazeBoundaryProbes(LegacyParams, LegacyBottom, LegacyTop, LegacyProbes);
AddMazeBoundaryProbes(StackParams, StackBottom, StackTop, StackProbes);
TestEqual(TEXT("legacy and operator seam tests have the same probe count"),
LegacyProbes.Num(), StackProbes.Num());
TArray<FVector> SeamProbes;
AddMazeBoundaryProbes(MazeParams, MazeBottomZ, MazeTopZ, SeamProbes);
int32 LegacyFirst = INDEX_NONE, StackFirst = INDEX_NONE;
float LegacyBefore = 0.0f, LegacyAfter = 0.0f;
float StackBefore = 0.0f, StackAfter = 0.0f;
const int32 LegacyMismatches = CountContextMismatches(
LegacyWorld, LegacyProbes, LegacyFirst, LegacyBefore, LegacyAfter);
const int32 StackMismatches = CountContextMismatches(
StackWorld, StackProbes, StackFirst, StackBefore, StackAfter);
int32 FirstProbe = INDEX_NONE;
float FirstBefore = 0.0f, FirstAfter = 0.0f;
const int32 NumMismatches = CountContextMismatches(
World, SeamProbes, FirstProbe, FirstBefore, FirstAfter);
TestEqual(TEXT("legacy Maze has no chunk-context seam mismatches"), LegacyMismatches, 0);
TestEqual(TEXT("operator-stack Maze has no chunk-context seam mismatches"), StackMismatches, 0);
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), LegacyProbes.Num() > 0);
TestEqual(TEXT("Maze has no chunk-context seam mismatches"), NumMismatches, 0);
TestTrue(TEXT("Maze seam test sampled cell-boundary probes"), SeamProbes.Num() > 0);
if (LegacyFirst != INDEX_NONE)
if (FirstProbe != INDEX_NONE)
{
AddError(FString::Printf(TEXT("Legacy Maze seam mismatch at probe %d: %.9g -> %.9g"),
LegacyFirst, LegacyBefore, LegacyAfter));
}
if (StackFirst != INDEX_NONE)
{
AddError(FString::Printf(TEXT("Operator Maze seam mismatch at probe %d: %.9g -> %.9g"),
StackFirst, StackBefore, StackAfter));
AddError(FString::Printf(TEXT("Maze seam mismatch at probe %d: %.9g -> %.9g"),
FirstProbe, FirstBefore, FirstAfter));
}
AddInfo(FString::Printf(
TEXT("Maze seam freedom: %d boundary probes, legacy mismatches=%d, operator mismatches=%d; "
TEXT("Maze seam freedom: %d boundary probes, mismatches=%d; "
"local window=2x2x2 child nodes ({-1,0}^3), no wide collect."),
LegacyProbes.Num(), LegacyMismatches, StackMismatches));
SeamProbes.Num(), NumMismatches));
return true;
}
@@ -175,7 +155,7 @@ bool FVoxelForgeCaveWarpTileLodSeamTest::RunTest(const FString& Parameters)
using namespace VoxelForgeTest;
FTestWorld World;
World.Build(1337, 2, true, 4);
World.Build(1337, 2, 4);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -90,9 +90,8 @@ bool FVoxelForgeOpStackChannelTest::RunTest(const FString& Parameters)
TestTrue(*FString::Printf(TEXT("%s shipping stack has a legal channel DAG"), Name), bValid);
};
// These are the eight builders currently shipped behind bUseOperatorStack. Keep the
// archetype list explicit so adding a builder without adding its validator assertion is
// visible in this test.
// These are the eight shipped archetype builders. Keep the archetype list explicit so adding
// a builder without adding its validator assertion is visible in this test.
int32 MidChunkZ = 0;
if (MidChunkForSlot(FTestWorld::SlotTunnelNetwork, TEXT("TunnelNetwork"), MidChunkZ))
@@ -1,30 +1,18 @@
// VoxelForgeOpStackIslandTest.cpp
// FloatingIslands — le portage qui fait tourner la pile À L'ENVERS.
// FloatingIslands — the port that runs the stack BACKWARDS.
// FloatingIslands — la pile qui tourne À L'ENVERS / the stack that runs BACKWARDS.
//
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES
// `VerticalShaftEquivalence` a mesuré la réutilisation À L'IDENTIQUE : trois opérateurs de Maze
// repris sans une ligne de changement. Celui-ci mesure quelque chose de plus fort, et de plus
// risqué pour l'abstraction : **la réutilisation PAR INVERSION**.
//
// Les quatre archétypes déjà portés partent tous de ROC et CREUSENT. FloatingIslands part du VIDE
// et REMPLIT. Si l'axe abstrait choisi (le SIGNE de la densité, convention interne positif = solide)
// est le bon, alors les deux extrémités de la pile doivent être les MÊMES opérateurs au signe près :
// Les autres archétypes partent de ROC et CREUSENT ; FloatingIslands part du VIDE et REMPLIT, avec
// les MÊMES opérateurs au signe près (convention interne positif = solide) :
//
// FConstantFieldSource(+Base) ←→ FConstantFieldSource(-Base)
// FSdfConvertOp(Sign = -1) ←→ FSdfConvertOp(Sign = +1)
//
// Et c'est le cas : le seul opérateur neuf de ce portage est le blob d'île. Un archétype qui se
// réutilise en s'INVERSANT est une preuve plus forte qu'un archétype qui se réutilise à l'identique
// — le premier dit que l'abstraction a trouvé le bon axe, le second seulement que deux archétypes
// se ressemblaient.
// Le seul opérateur propre à l'archétype est le blob d'île. / The only archetype-specific op is the
// island blob; the rest is Maze's rock + carve with the opposite sign.
//
// ET LE VERDICT DE BOÎTE : c'est ici que `ClassifyBox` peut rendre **AllAir** pour la première fois
// de tout le plugin. Une strate d'îles flottantes est, par construction, surtout vide ; aucun
// archétype de grotte n'a jamais su prouver « tout air » (`OPSTACK-DECOMPOSITION §7`). Le test
// compte les deux verdicts SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce gain-là.
//
// LA BARRE : bit à bit, comme les autres depuis `FPSemantics = Precise` (AUDIT §C9/§C10).
// ET LE VERDICT DE BOÎTE : une strate d'îles flottantes est surtout vide, donc `ClassifyBox` doit
// savoir rendre **AllAir** (`OPSTACK-DECOMPOSITION §7`). Le test compte AllSolid et AllAir
// SÉPARÉMENT, parce qu'un total agrégé masquerait exactement ce verdict-là.
#if WITH_DEV_AUTOMATION_TESTS
@@ -92,8 +80,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
{
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds, which sends ")
TEXT("GetFloatingIslandDensity down its early-out. The op stack has none by design."));
AddError(TEXT("The FloatingIslands strate has degenerate Z bounds; GetDensityAt builds no ")
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
return false;
}
@@ -127,7 +115,7 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
}
//=========================================================================
// 1. ÉQUIVALENCE
// 1. LIVENESS — roche d'île et vide ouvert / island rock and open void
//=========================================================================
// On compte SÉPARÉMENT le solide d'intérieur et le solide de seal : sur cet archétype la
// quasi-totalité du volume est de l'air, donc un « N solides » agrégé serait dominé par les
@@ -135,74 +123,34 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
const float InnerBot = P.StrateBottomWorldZ + P.BoundarySealThickness;
const float InnerTop = P.StrateTopWorldZ - P.BoundarySealThickness;
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1;
int32 NumInsideIsland = 0, NumOpenVoid = 0;
float WorstDelta = 0.0f;
for (int32 i = 0; i < NumIslandSamples; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float Old = Gen->GetFloatingIslandDensity(X, Y, Z, P);
const float New = Stack.EvalMC(X, Y, Z);
const bool bInterior = (Z > InnerBot && Z < InnerTop);
if (bInterior && Old < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
if (bInterior && Old >= 0.0f) { ++NumOpenVoid; }
if (!BitEqual(Old, New))
{
++NumDiff;
const float D = FMath::Abs(Old - New);
if (D > WorstDelta) { WorstDelta = D; WorstIdx = i; }
if (bInterior && New < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île
if (bInterior && New >= 0.0f) { ++NumOpenVoid; }
}
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
}
if (NumDiff == 0)
{
AddInfo(FString::Printf(
TEXT("FloatingIslands: bit-identical across %d samples (%d inside island rock away from ")
TEXT("the seal bands, %d in open void, so the void source, the blobs, the roughness and ")
TEXT("the fill were all exercised). The stack runs BACKWARDS -- void source + fill ")
TEXT("instead of rock source + carve -- using the SAME operators with the opposite ")
TEXT("sign. Only the blob source is new (OPSTACK-PLAN 2.5)."),
NumIslandSamples, NumInsideIsland, NumOpenVoid));
}
else
{
AddError(FString::Printf(
TEXT("FloatingIslands: %d of %d samples differ (largest |delta| %.9g at (%.0f, %.0f, ")
TEXT("%.0f)); %d cross the isosurface. Since /fp:precise the bar is bit-identity, so ")
TEXT("this is a real port error. Check, in order: the C1 warp fix (BOTH paths must now ")
TEXT("use VoxelHash::SeedOffset(S, 0.0007f) -- if only one was changed, EVERY warped ")
TEXT("sample differs), then the SdfConvert SIGN (+1 fills, -1 carves), then the 'Isld' ")
TEXT("salt (0x49736C64), the roughness frequency (0.08 / 4 octaves here, NOT Maze's ")
TEXT("0.12 / 3), the per-island TaperEnd and TopFlatten dome branch, and the ")
TEXT("SmoothMin blend K = max(SDFBlendRadius, 0.01)."),
NumDiff, NumIslandSamples, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSideDisagree));
}
TestEqual(TEXT("no sample lands on the opposite side of the isosurface"), NumSideDisagree, 0);
TEXT("FloatingIslands: %d of %d samples inside island rock away from the seal bands, %d in ")
TEXT("open void."),
NumInsideIsland, NumIslandSamples, NumOpenVoid));
if (NumInsideIsland == 0)
{
AddWarning(TEXT("No sample landed inside island rock away from the seal bands, so the blob ")
TEXT("source and the fill were never meaningfully exercised -- the equivalence ")
TEXT("above then only proves that two empty voids agree. Raise IslandDensity or ")
TEXT("IslandMaxRadius."));
TEXT("source and the fill were never meaningfully exercised. Raise IslandDensity ")
TEXT("or IslandMaxRadius."));
}
//=========================================================================
// 2. INVARIANCE DE FENÊTRE
//=========================================================================
// La source garde un cache 3×3 `thread_local` dont la clé est le jeu de params — et cette clé
// inclut délibérément `BoundarySealThickness`, que l'original omet alors que `SpreadZ` le lit
// (voir la note dans FIslandBlobSource::GetCells).
// inclut délibérément `BoundarySealThickness`, que `SpreadZ` lit (voir la note dans
// FIslandBlobSource::GetCells).
{
std::atomic<int32> Impure{ 0 };
const int32 NumBlocks = FMath::Max(4, FMath::Min(16, FPlatformMisc::NumberOfCores()));
@@ -296,9 +244,8 @@ bool FVoxelForgeOpStackIslandTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("Box verdicts over 60 FloatingIslands tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("IslandSpacing %.0f): %d proved AllSolid, %d proved AllAir, ")
TEXT("%d Mixed, %d voxels checked, %d violations. Today's ClassifyTile proves ZERO of these. The AllAir count is the new ")
TEXT("thing: no cave archetype has ever been able to prove 'all air', and a floating-")
TEXT("island strate is mostly exactly that (OPSTACK-DECOMPOSITION 7)."),
TEXT("%d Mixed, %d voxels checked, %d violations. The AllAir count matters most: a ")
TEXT("floating-island strate is mostly open air (OPSTACK-DECOMPOSITION 7)."),
SpanVoxels, (float)SpanVoxels / FMath::Max(P.IslandSpacing, 1.0f), P.IslandSpacing,
NumProvedSolid, NumProvedAir, NumMixed, NumBruteSamples, NumUnsound));
@@ -1,73 +1,17 @@
// VoxelForgeOpStackMazeTest.cpp
// PHASE 1, LE TEST QUI COMPTE — la pile d'opérateurs Maze contre GetMazeDensity.
// PHASE 1'S LOAD-BEARING TEST — the Maze operator stack against GetMazeDensity.
// La pile d'opérateurs Maze / The Maze operator stack.
//
// CE QUE LA PHASE 1 DEVAIT PROUVER / WHAT PHASE 1 HAD TO PROVE
// Le déclencheur d'arrêt de `OPSTACK-PLAN §4` : **« est-ce que la séparation source / modifier tombe
// naturellement du code existant ? »** Réponse mesurée : oui. Maze se décompose en huit opérateurs
// sans contorsion, le SDF est reproduit BIT POUR BIT, et aucun échantillon ne change de côté de
// l'isosurface.
//
// ═════════════════════════════════════════════════════════════════════════════════════════
// ✅ MISE À JOUR 2026-07-27 : LE PLANCHER ULP N'EXISTE PLUS. C'ÉTAIT `/fp:fast`.
// ═════════════════════════════════════════════════════════════════════════════════════════
// `FPSemantics = Precise` sur le module (AUDIT §C9, posé pour le cross-play Linux/Windows) fait
// passer ce test à **BIT-IDENTIQUE**. La section ci-dessous décrit un état RÉVOLU ; elle est gardée
// parce qu'elle explique pourquoi les cinq expériences d'isolation avaient toutes échoué (sous
// `/fp:fast` le compilateur transforme selon le CONTEXTE — il n'y avait aucune variable à isoler)
// et parce qu'elle dit quoi regarder si la bit-identité régresse un jour.
//
// **Conséquence pratique : ce test est maintenant un instrument BEAUCOUP plus fin.** Le moindre
// écart est désormais une vraie trouvaille, pas du bruit à noter. La machinerie de gradation ULP
// est conservée exprès — c'est elle qui signalerait une régression du modèle flottant.
//
// UPDATE: the ULP floor is GONE — FPSemantics = Precise makes this test bit-identical. The section
// below describes a past state, kept because it explains why five isolation experiments all failed
// (under /fp:fast the compiler transforms by CONTEXT — there was no variable to isolate) and what to
// look at if bit-identity ever regresses.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ LE PLANCHER ULP (HISTORIQUE) — lire ceci avant de « corriger » un écart résiduel
// ─────────────────────────────────────────────────────────────────────────────────────────
// (HISTORIQUE, avant `FPSemantics = Precise`) la pile reproduisait `GetMazeDensity` à ~1-2 ULP près
// sur ~2 % des échantillons (ceux qui tombent
// dans la coquille de blend du SDF, où `Blend - Sdf` annule catastrophiquement et amplifie le
// dernier arrondi). **Zéro échantillon ne traverse l'isosurface**, donc pas un triangle ne bouge.
//
// L'origine exacte de ce dernier arrondi n'a PAS été identifiée, après six cycles de build et cinq
// hypothèses toutes réfutées par la mesure (aller-retour FVector · fenêtre de rugosité · `/fp:fast`
// entre unités de compilation · contexte d'inlining · constante de compilation vs donnée
// d'exécution). Ce qui EST établi par la mesure :
//
// • le SDF est bit-identique sur 126/126 des écarts — le treillis, les hashs, l'ensemble d'arêtes
// et `VoxelSDF::Capsule` sont donc exacts ;
// • l'écart naît entièrement dans la conversion SDF→densité, au dernier arrondi ;
// • il est DÉTERMINISTE (mêmes échantillons, même delta, même coordonnée à chaque run) ;
// • il ne dépend ni de l'unité de compilation, ni de l'inlining, ni du modèle flottant.
//
// **Décision (Jahni, 2026-07-27) : on l'accepte et on avance.** Aucune décision du projet ne dépend
// de la réponse, et la chasse coûtait plus que l'information. Consigné comme point ouvert dans
// `AUDIT-2026-07.md §C10`.
//
// ⚠️ LA RÈGLE QUI EN DÉCOULE, ELLE, EST IMPORTANTE :
// **ne jamais faire tourner les deux chemins (switch d'archétype et pile d'opérateurs) dans le même
// monde, et ne jamais comparer leurs sorties pour égalité.** Ce n'est PAS un risque de désync entre
// clients — dans un même binaire le champ est prouvé pur (`VoxelForge.Determinism.DensityPurity`,
// bit-identique entre threads et ordres de requête) et tous les pairs exécutent le même chemin. Mais
// une strate à moitié migrée produirait une couture. Le vrai sujet multijoueur est ailleurs :
// `AUDIT §C9` (le défaut FP d'UBT diffère selon la toolchain).
//
// Never run both paths in one world and never compare their outputs for equality. This is NOT a
// client-desync risk — within one binary the field is proven pure and every peer runs the same path —
// but a half-migrated strate would produce a seam.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LA BARRE D'ACCEPTATION, ENCODÉE CI-DESSOUS / THE ACCEPTANCE BAR, ENCODED BELOW
// ─────────────────────────────────────────────────────────────────────────────────────────
// • ÉCHEC DUR : un seul échantillon qui change de côté de l'isosurface (la géométrie bouge).
// • INFO : des écarts à l'échelle de l'ULP (le plancher, attendu).
// • WARN : un écart plus grand — ÇA, c'est une vraie dérive de portage, et il faut chercher.
// Un test qui avertit à chaque portage serait ignoré par le portage qui compte.
// Ce que ce test vérifie / What this test checks:
// • TOPOLOGIE — le graphe parent orienté vers l'origine est un arbre couvrant (N-1 arêtes, zéro
// cellule déconnectée), les boucles optionnelles restent bornées, et le graphe est déterministe
// sur 64 seeds. / The origin-directed parent graph is a spanning tree, deterministic per seed.
// • DÉCOMPOSITION — huit opérateurs (roche + couloirs + rugosité + carve + 4 structurels), pas un
// `FMazeOp` monolithique. / Eight ops, not one wrapped monolith.
// • INVARIANCE DE FENÊTRE — le cache par cellule est `thread_local` : bit-identique quel que soit
// l'ordre des requêtes et le thread. / Bit-identical across query order and worker threads.
// • VERDICTS DE BOÎTE — chaque verdict uniforme survit à la force brute (un faux verdict est un
// trou), y compris pour une pile source/convertisseur jamais écrite à la main.
// Every uniform box verdict survives brute force, including for a novel source/converter stack.
#if WITH_DEV_AUTOMATION_TESTS
@@ -355,13 +299,13 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
return false;
}
// GetMazeDensity court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde appartient
// à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate valide.
// La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
// dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one.
if (MazeParams.StrateTopWorldZ - MazeParams.StrateBottomWorldZ <= 0.0f)
{
AddError(FString::Printf(
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f), which sends ")
TEXT("GetMazeDensity down its early-out. The op stack has no such early-out by design."),
TEXT("The Maze strate has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt ")
TEXT("builds no stack for it. The op stack has no degenerate-strate early-out by design."),
MazeParams.StrateTopWorldZ, MazeParams.StrateBottomWorldZ));
return false;
}
@@ -485,70 +429,6 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
}
}
//=========================================================================
// ÉQUIVALENCE — géométrie d'abord, bits ensuite.
//=========================================================================
int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0;
float WorstDelta = 0.0f;
for (int32 i = 0; i < NumMazeSamples; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float Old = Gen->GetMazeDensity(X, Y, Z, MazeParams); // MC : négatif = solide
const float New = Stack.EvalMC(X, Y, Z);
if (!BitEqual(Old, New))
{
++NumDiff;
const float Delta = FMath::Abs(Old - New);
if (Delta > WorstDelta) { WorstDelta = Delta; WorstIdx = i; }
// `Blend - Sdf` annule catastrophiquement au bord de la coquille de blend, donc un
// écart d'ULP sur le SDF ressort amplifié sur la densité : marge généreuse, mais bornée.
const float UlpNoise = 16.0f * FMath::Max(FMath::Abs(Old), 1.0f) * FLT_EPSILON;
if (Delta > UlpNoise) { ++NumBeyondUlpNoise; }
}
// Le mesher ne lit que le SIGNE (D >= IsoLevel ⇒ air). Un désaccord de CÔTÉ bouge la géométrie.
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; }
}
if (NumDiff == 0)
{
AddInfo(FString::Printf(TEXT("Bit-identical across %d samples."), NumMazeSamples));
}
else if (NumBeyondUlpNoise == 0)
{
AddInfo(FString::Printf(
TEXT("%d of %d samples differ, ALL at ULP scale (largest |delta| %.9g at (%.0f, %.0f, ")
TEXT("%.0f)), and 0 cross the isosurface -- not one triangle would move. This is the ")
TEXT("accepted floor; see the header comment and AUDIT-2026-07.md C10. The SDF itself is ")
TEXT("reproduced BIT FOR BIT, so the lattice, the hashes and VoxelSDF::Capsule are exact; ")
TEXT("only the final SDF->density rounding differs. Do not go hunting this again without ")
TEXT("reading C10 first -- five hypotheses have already been measured and refuted."),
NumDiff, NumMazeSamples, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f));
}
else
{
AddWarning(FString::Printf(
TEXT("%d of %d samples differ and %d are TOO LARGE to be the accepted ULP floor (largest ")
TEXT("|delta| %.9g at (%.0f, %.0f, %.0f)); %d cross the isosurface. THIS one is real port ")
TEXT("drift, not the known floor. Check, in order: the roughness apply-window ")
TEXT("(R + SurfaceRoughness + 2), the carve blend (2.0), the noise frequency (0.12) and ")
TEXT("octave count (3), and the order of the structural post ops."),
NumDiff, NumMazeSamples, NumBeyondUlpNoise, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSolidDisagreements));
}
// Le SEUL échec dur : un désaccord de côté d'iso EST une différence de géométrie.
TestEqual(TEXT("no sample lands on the opposite side of the isosurface from the original"),
NumSolidDisagreements, 0);
//=========================================================================
// INVARIANCE DE FENÊTRE — la pile doit tenir les mêmes règles que le générateur.
//=========================================================================
@@ -624,10 +504,9 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
//=========================================================================
// LE VERDICT DE BOÎTE — le prix perf de l'intervalle SDF conservatif.
//=========================================================================
// ClassifyTile renvoie Mixed pour tout archétype de grotte, donc TunnelNetwork, Maze,
// VerticalShafts, FloatingIslands, FlatPlain, CrystalChamber et Underwater ne captent RIEN du
// gain T1.d. Tout nombre > 0 ici est le saut de tuile propre à la pile, pas une décision du
// classifieur historique.
// Pour un slot de cave, ClassifyTile plie `ClassifyBox` de cette pile : tout nombre > 0 ici est
// une tuile que le mesher saute. / For a cave slot ClassifyTile folds this stack's ClassifyBox:
// every proved box here is a tile the mesher skips.
{
int32 NumProved = 0, NumMixed = 0, NumUnsound = 0, NumBruteSamples = 0;
FRandomStream Rng(24680);
@@ -689,17 +568,14 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("Box verdicts over %d Maze tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ClassifyTile ")
TEXT("proves ZERO of these -- every cave archetype falls through to \"pas prouvable en ")
TEXT("v1\". Any number above zero here is tile-skipping Maze has never had."),
TEXT("CellSize %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations."),
NumMazeBoxTests, SpanVoxels, (float)SpanVoxels / FMath::Max(MazeParams.CellSize, 1.0f), MazeParams.CellSize,
NumProved, NumMixed, NumBruteSamples, NumUnsound));
if (NumProved == 0)
{
AddWarning(TEXT("The stack proved no tile uniform, so it is not yet better than today's ")
TEXT("classifier for Maze. Not a correctness problem, but the perf case for ")
TEXT("the port rests on this number."));
AddWarning(TEXT("The stack proved no tile uniform, so ClassifyTile skips no Maze tile. ")
TEXT("Not a correctness problem, but a perf regression."));
}
}
@@ -1,21 +1,15 @@
// VoxelForgeOpStackShaftTest.cpp
// VerticalShafts — le portage qui teste la RÉUTILISATION et le contrat de l'opérateur courant.
// VerticalShafts — the port that tests REUSE and the current operator contract.
// VerticalShafts — la RÉUTILISATION d'opérateurs et le contrat de la pile.
// VerticalShafts — operator REUSE and the stack's contract.
//
// CE QUE CELUI-CI PROUVE EN PLUS DES AUTRES
// Les portages précédents demandaient « la décomposition reproduit-elle l'original ? ». Celui-ci
// demande **« les opérateurs se RÉUTILISENT-ils vraiment entre archétypes ? »**, qui est la thèse
// de `OPSTACK-PLAN §2.5` et la seule raison de faire ce refactor plutôt que de nettoyer le `switch`.
// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, repris tels quels :
// `ConstantRock`, `SdfRoughness`, `SdfCarve` — mêmes ops, autre source et autres réglages
// (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`). C'est la thèse
// de `OPSTACK-PLAN §2.5` : les opérateurs se réutilisent entre archétypes.
//
// Trois des cinq opérateurs de VerticalShafts sont ceux de Maze, **repris sans une ligne de
// changement** : `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, `GetMazeDensity` et
// `GetVerticalShaftDensity` sont deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil.
// En opérateurs, ce sont les mêmes trois ops avec une source différente et d'autres réglages
// (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
//
// The direct helper remains useful migration telemetry, but it is not the owner path after the
// structural-post split. The assertions below target the operator stack actually used in-game:
// current channel count/order, feature liveness, cache isolation and box-proof soundness.
// Three of the five VerticalShafts ops are Maze's, reused unchanged. The assertions below target
// the stack used in-game: channel count/order, feature liveness, window invariance, cache cost and
// box-proof soundness.
#if WITH_DEV_AUTOMATION_TESTS
@@ -93,8 +87,8 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
if (P.StrateTopWorldZ - P.StrateBottomWorldZ <= 0.0f)
{
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds, which sends ")
TEXT("GetVerticalShaftDensity down its early-out. The op stack has none by design."));
AddError(TEXT("The VerticalShafts strate has degenerate Z bounds; GetDensityAt builds no ")
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."));
return false;
}
@@ -128,49 +122,17 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
}
//=========================================================================
// 1. CURRENT-OWNER DIAGNOSTIC
// GetVerticalShaftDensity is the retired direct path. Its delta is reported so a future
// port can be audited, but it is not an owner-path failure: the game evaluates Stack plus
// the common MC tail.
// 1. LIVENESS — des échantillons tombent dans l'air des puits / samples land in shaft air
//=========================================================================
int32 NumDiff = 0, NumSideDisagree = 0, WorstIdx = -1, NumInsideShaft = 0;
float WorstDelta = 0.0f;
int32 NumInsideShaft = 0;
for (int32 i = 0; i < NumShaftSamples; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float Old = Gen->GetVerticalShaftDensity(X, Y, Z, P);
const float New = EvalStackMCForTest(Stack, X, Y, Z);
if (Old >= 0.0f) { ++NumInsideShaft; } // air ⇒ dans un puits/connecteur/étagère
if (!BitEqual(Old, New))
{
++NumDiff;
const float D = FMath::Abs(Old - New);
if (D > WorstDelta) { WorstDelta = D; WorstIdx = i; }
// air ⇒ dans un puits/connecteur/étagère / air ⇒ inside a shaft, connector or ledge
if (EvalStackMCForTest(Stack, X, Y, Z) >= 0.0f) { ++NumInsideShaft; }
}
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; }
}
if (NumDiff == 0)
{
AddInfo(FString::Printf(
TEXT("VerticalShafts: bit-identical across %d samples (%d of them inside a shaft, so ")
TEXT("the cylinders, connectors, roughness, carve and ledges were all exercised). ")
TEXT("THREE of the five ops here are Maze's, reused unchanged -- operator reuse across ")
TEXT("archetypes is now measured rather than intended (OPSTACK-PLAN 2.5)."),
NumShaftSamples, NumInsideShaft));
}
AddInfo(FString::Printf(
TEXT("VerticalShafts legacy diagnostic: %d of %d samples differ (largest |delta| %.9g at "
"(%.0f, %.0f, %.0f)); %d cross the isosurface; %d samples are air in the stack."),
NumDiff, NumShaftSamples, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSideDisagree, NumInsideShaft));
AddInfo(FString::Printf(TEXT("VerticalShafts: %d of %d samples are air in the stack."),
NumInsideShaft, NumShaftSamples));
TestTrue(TEXT("shaft feature sampling exercised the operator stack"), NumInsideShaft > 0);
if (NumInsideShaft == 0)
@@ -268,14 +230,14 @@ bool FVoxelForgeOpStackShaftTest::RunTest(const FString& Parameters)
const int32 CellY = (i / 16) - 8;
LastX = (static_cast<float>(CellX) + 0.37f) * PerfSpacing;
LastY = (static_cast<float>(CellY) + 0.61f) * PerfSpacing;
Sink += Gen->GetVerticalShaftDensity(LastX, LastY, PerfZ, P);
Sink += EvalStackMCForTest(Stack, LastX, LastY, PerfZ);
}
const double RebuildSeconds = FPlatformTime::Seconds() - RebuildStart;
const double HotStart = FPlatformTime::Seconds();
for (int32 i = 0; i < NumHotSamples; ++i)
{
Sink += Gen->GetVerticalShaftDensity(LastX, LastY, PerfZ, P);
Sink += EvalStackMCForTest(Stack, LastX, LastY, PerfZ);
}
const double HotSeconds = FPlatformTime::Seconds() - HotStart;
@@ -1,32 +1,20 @@
// VoxelForgeOpStackSlabTest.cpp
// PHASE 2, PREMIER PORTAGE — la pile Slab courante, sur LES DEUX archétypes.
// PHASE 2'S FIRST PORT — the current Slab operator stack, on BOTH archetypes.
// La pile Slab, sur LES DEUX archétypes / The Slab operator stack, on BOTH archetypes.
//
// CE QUE CE TEST DOIT PROUVER / WHAT THIS TEST HAS TO PROVE
// Trois choses, et la troisième est la raison d'être du portage :
//
// 1. CURRENT OWNER — the stack's channels, window invariance and conservative box verdicts
// guard the path used by the game. `GetSlabDensity` is retained as a legacy diagnostic only:
// it still owns the former full passage/support tail, while the operator stack is followed by
// the current common MC post.
// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET
// CrystalChamber. The legacy helper is sampled for migration telemetry, not used as the
// owner oracle; if the current stack becomes non-finite or loses its channel contract, this
// test still fails.
// 1. LE CHEMIN DU JEU — la pile rend une densité finie, invariante à la fenêtre (ordre et
// threads), et ses verdicts de boîte sont conservatifs.
// The stack's density is finite and window-invariant, and its box verdicts are conservative.
// 2. UN OPÉRATEUR, DEUX ARCHÉTYPES — la MÊME pile est vérifiée pour FlatPlain ET CrystalChamber.
// ⚠️ La fixture ne règle que `GeneratorType`, donc les deux slots portent des params PAR
// DÉFAUT : à eux seuls ils exécutent la même configuration à deux profondeurs. C'est la
// TROISIÈME passe (`CrystalChamber(tuned)`, `CeilingRoughness` 6 → 20) qui fait réellement
// varier ce qui distingue les deux archétypes — et qui sert en même temps de pire cas aux
// bornes d'amplitude de `ClassifyBox`. Voir le bloc en bas de fichier.
// 3. LE VERDICT DE BOÎTE — et c'est ici que §3.1 se paie. `ClassifyTile` prouve ZÉRO tuile pour
// FlatPlain et CrystalChamber aujourd'hui. Depuis que les deux surfaces sont XY-PURES, leurs
// bornes en Z sont connues exactement (contrat [-1,1] de FBM), donc toute tuile entièrement
// sous le sol ou entre les deux bandes se prouve SANS échantillonner.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// The old direct helper contains the removed support-floor/room tail. Room ownership and the
// authored-floor removal deliberately split that structural post from the operator stack, so its
// differences remain visible as diagnostics while the owner assertions below cover the live path.
// 3. LE VERDICT DE BOÎTE — les deux surfaces sont XY-PURES, donc leurs bornes en Z sont connues
// exactement (contrat [-1,1] de FBM) : toute tuile entièrement sous le sol ou entre les deux
// bandes se prouve SANS échantillonner.
#if WITH_DEV_AUTOMATION_TESTS
@@ -73,14 +61,13 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
int32 TopVoxelZ, int32 BottomVoxelZ,
int32 SlotIndex, const TCHAR* SlotName)
{
// `GetSlabDensity` court-circuite sur une strate dégénérée (`return 1.0f`). Cette garde
// appartient à la fonction d'archétype, pas à un opérateur ; la pile suppose une strate
// valide, et `GetDensityAt` retombe sur le `switch` dans ce cas.
// La pile suppose une strate valide ; `GetDensityAt` rend de l'air sans pile sur une strate
// dégénérée. / The stack assumes a valid strate; GetDensityAt returns air for a degenerate one.
if (SlabParams.StrateTopWorldZ - SlabParams.StrateBottomWorldZ <= 0.0f)
{
AddError(FString::Printf(
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f), which sends GetSlabDensity ")
TEXT("down its early-out. The op stack has no such early-out by design."),
TEXT("%s has degenerate Z bounds (top %.1f, bottom %.1f); GetDensityAt builds no ")
TEXT("stack for it. The op stack has no degenerate-strate early-out by design."),
SlotName, SlabParams.StrateTopWorldZ, SlabParams.StrateBottomWorldZ));
return;
}
@@ -114,90 +101,16 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
}
//=====================================================================
// 1. ÉQUIVALENCE — géométrie d'abord, bits ensuite.
// 1. DENSITÉ FINIE / FINITE DENSITY
//=====================================================================
// ─────────────────────────────────────────────────────────────────────
// LE BON MÈTRE — corrigé 2026-07-27 après que la passe `tuned` a crié au loup
// ─────────────────────────────────────────────────────────────────────
// Première version : `16 · max(|Old|, 1) · FLT_EPSILON`, c.-à-d. l'ULP mesuré sur la
// DENSITÉ DE SORTIE. C'est le mauvais mètre, et il se trompe exactement là où le test
// regarde le plus : la densité vaut `min(Z - Sol, Plafond - Z)`, donc PRÈS DE L'ISOSURFACE
// la sortie tend vers 0 pendant que les intermédiaires (surfaces, Z monde, amplitudes de
// bruit) valent des CENTAINES. Un arrondi né à l'échelle 400 était jugé contre un mètre
// à l'échelle 1 — 400× trop serré.
//
// Mesuré : la passe `tuned` (rugosités ×2.25 et ×3.33) a vu ses écarts croître ×4.5, et
// son pire écart valait **0.345 ULP de |Z|**. Sous-ULP à l'échelle où l'erreur naît.
// L'erreur est donc proportionnelle à l'AMPLITUDE, ce qui est la signature d'un arrondi
// ordinaire, pas d'une transcription fausse.
//
// Le mètre correct est la magnitude des quantités D'OÙ VIENT l'erreur. Le test reste
// discriminant : une vraie dérive de portage (offset de bruit faux, `abs()` manquant,
// clamp oublié) déplace la surface de plusieurs VOXELS — 4 ordres de grandeur au-dessus
// de ce seuil, pas 4 fois.
//
// The first yardstick measured ULPs on the OUTPUT density, which tends to 0 near the
// isosurface while the intermediates are in the hundreds. Rounding born at scale ~400 was
// judged against a yardstick of scale 1. Real port drift moves the surface by voxels —
// four orders of magnitude above this bound, so the test stays discriminating.
const float SurfaceScale = FMath::Max(FMath::Abs(SlabParams.StrateTopWorldZ),
FMath::Abs(SlabParams.StrateBottomWorldZ));
int32 NumDiff = 0, WorstIdx = -1, NumBeyondUlpNoise = 0, NumSolidDisagreements = 0;
int32 NumNonFinite = 0;
float WorstDelta = 0.0f, WorstOld = 0.0f, WorstUlpsOfScale = 0.0f;
// Le pire cas PARMI LES DÉPASSEMENTS — c'est lui qui dit si un WARN est du bruit ou une dérive.
float WorstOutlierDelta = 0.0f, WorstOutlierOld = 0.0f, WorstOutlierUlps = 0.0f;
for (int32 i = 0; i < NumSlabSamples; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float Old = Gen->GetSlabDensity(X, Y, Z, SlabParams); // MC : négatif = solide
const float New = Stack.EvalMC(X, Y, Z);
if (!FMath::IsFinite(New))
if (!FMath::IsFinite(Stack.EvalMC((float)Points[i].X, (float)Points[i].Y, (float)Points[i].Z)))
{
++NumNonFinite;
}
if (!BitEqual(Old, New))
{
++NumDiff;
const float Delta = FMath::Abs(Old - New);
// L'échelle à laquelle CET échantillon calcule : la sortie, sa propre altitude, et
// les bornes de la strate. C'est le plus grand des trois qui porte l'arrondi.
const float Scale = FMath::Max3(FMath::Abs(Old), FMath::Abs(Z),
FMath::Max(SurfaceScale, 1.0f));
const float Ulps = Delta / (Scale * FLT_EPSILON);
if (Delta > WorstDelta)
{
WorstDelta = Delta; WorstIdx = i; WorstOld = Old; WorstUlpsOfScale = Ulps;
}
if (Delta > 16.0f * Scale * FLT_EPSILON)
{
++NumBeyondUlpNoise;
if (Delta > WorstOutlierDelta)
{
WorstOutlierDelta = Delta; WorstOutlierOld = Old; WorstOutlierUlps = Ulps;
}
}
}
// Le mesher ne lit que le SIGNE. Un désaccord de CÔTÉ bouge la géométrie.
if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSolidDisagreements; }
}
AddInfo(FString::Printf(
TEXT("%s legacy slab diagnostic: %d of %d samples differ, %d exceed the old ULP "
"bound, largest |delta| %.9g at (%.0f,%.0f,%.0f), %d side disagreements; "
"current stack non-finite samples=%d."),
SlotName, NumDiff, NumSlabSamples, NumBeyondUlpNoise, WorstDelta,
WorstIdx >= 0 ? Points[WorstIdx].X : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Y : 0.0f,
WorstIdx >= 0 ? Points[WorstIdx].Z : 0.0f,
NumSolidDisagreements, NumNonFinite));
TestEqual(*FString::Printf(TEXT("%s: current stack has no non-finite density"), SlotName),
NumNonFinite, 0);
@@ -300,9 +213,9 @@ bool FVoxelForgeOpStackSlabTest::RunTest(const FString& Parameters)
AddInfo(FString::Printf(
TEXT("%s box verdicts over %d tiles (XY sampled from +/- %d voxels = %.1f x ")
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. Today's ")
TEXT("ClassifyTile proves ZERO of these. This number is the whole point of making ")
TEXT("the slab surfaces XY-pure (OPSTACK-DECOMPOSITION 3.1)."),
TEXT("ColumnSpacing %.0f): %d proved uniform, %d Mixed, %d voxels checked, %d violations. ")
TEXT("This number is the whole point of making the slab surfaces XY-pure ")
TEXT("(OPSTACK-DECOMPOSITION 3.1)."),
SlotName, NumSlabTiles, SpanVoxels,
(float)SpanVoxels / FMath::Max(SlabParams.ColumnSpacing, 1.0f), SlabParams.ColumnSpacing,
NumProved, NumMixed, NumBruteSamples, NumUnsound));
@@ -8,6 +8,7 @@
#include "Misc/AutomationTest.h"
#include "VoxelCaveMorphology.h"
#include "VoxelDensityOpStack.h"
#include "VoxelDensityPrimitives.h"
#include "VoxelForgeTestFixture.h"
#include "VoxelPassageGeometry.h"
@@ -900,11 +901,27 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
++NumFootingChecked;
// The live world's passage modifier is intentionally absent here. The endpoint is
// already known to be passage-carved, so this second generator checks the archetype
// source itself: air at the landing and solid immediately below it.
TStrongObjectPtr<UVoxelGenerator> SourceOnlyGenerator(
NewObject<UVoxelGenerator>(GetTransientPackage(), NAME_None, RF_Transient));
SourceOnlyGenerator->InitializeSettings(World.Settings.Get());
// already known to be passage-carved, so the archetype's own stack, built without a
// strate manager (hence without passage carving), checks the source itself: air at
// the landing and solid immediately below it.
const UVoxelGenerator& LiveGenerator = *World.Generator;
FVoxelOpStack SourceStack;
FVoxelOpContext SourceContext;
SourceContext.Seed = (uint32)LiveGenerator.Seed;
SourceContext.LayoutVersion = World.StrateManager->GetLayoutVersion();
SourceContext.StrateTopWorldZ = StrateTopZ;
SourceContext.StrateBottomWorldZ = StrateBottomZ;
SourceContext.WorldRadiusVoxels = LiveGenerator.WorldRadiusVoxels;
SourceContext.EdgeSealThickness = LiveGenerator.EdgeSealThickness;
// The generator resets the shaft-connector hand-off around every density query; a
// direct stack evaluation must model that boundary too.
auto EvalSource = [&SourceStack](float QueryX, float QueryY, float QueryZ)
{
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
const float SourceDensity = SourceStack.EvalMC(QueryX, QueryY, QueryZ);
VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker();
return SourceDensity;
};
float LandingDensity = 0.0f;
float FootingDensity = 0.0f;
@@ -915,13 +932,16 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FMazeGenerationParams P = Definition->MazeParams;
P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetMazeDensity(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
VoxelDensityOps::BuildMazeStack(SourceStack, P, LiveGenerator.Seed,
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
SourceStack.PrepareChunk(SourceContext);
LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
const float FloorProbeZ = SuggestedPoint.Z
- FMath::Max(P.CorridorRadius, 0.5f)
- P.SurfaceRoughness * VOXEL_NOISE_SCALE * 1.5f - 3.0f;
FootingDensity = SourceOnlyGenerator->GetMazeDensity(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P);
FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
break;
}
@@ -930,11 +950,14 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FVerticalShaftParams P = Definition->VerticalShaftParams;
P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetVerticalShaftDensity(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
VoxelDensityOps::BuildVerticalShaftStack(SourceStack, P, LiveGenerator.Seed,
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
SourceStack.PrepareChunk(SourceContext);
LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
const float FloorProbeZ = StrateBottomZ + P.BoundarySealThickness * 0.5f;
FootingDensity = SourceOnlyGenerator->GetVerticalShaftDensity(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ, P);
FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, FloorProbeZ);
break;
}
@@ -943,10 +966,13 @@ bool FVoxelForgePassageLandsInOpenSpaceTest::RunTest(const FString& Parameters)
FFloatingIslandParams P = Definition->FloatingIslandParams;
P.StrateTopWorldZ = StrateTopZ;
P.StrateBottomWorldZ = StrateBottomZ;
LandingDensity = SourceOnlyGenerator->GetFloatingIslandDensity(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z, P);
FootingDensity = SourceOnlyGenerator->GetFloatingIslandDensity(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f, P);
VoxelDensityOps::BuildFloatingIslandStack(SourceStack, P, LiveGenerator.Seed,
LiveGenerator.OriginSpineRadius, /*StrateManager*/nullptr);
SourceStack.PrepareChunk(SourceContext);
LandingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z);
FootingDensity = EvalSource(
SuggestedPoint.X, SuggestedPoint.Y, SuggestedPoint.Z - 1.0f);
break;
}
@@ -140,7 +140,6 @@ namespace
Definition->SurfaceParams = Candidate.ArchetypeParams.SurfaceParams;
Definition->VerticalShaftParams = Candidate.ArchetypeParams.VerticalShaftParams;
Definition->FloatingIslandParams = Candidate.ArchetypeParams.FloatingIslandParams;
Definition->bUseOperatorStack = true;
Definition->TransitionType = EVoxelStrateTransition::Hard;
World.Reinitialize();
}
@@ -869,7 +868,7 @@ bool FVoxelForgeScaleDiagnosisTest::RunTest(const FString& Parameters)
// Resolve every probe from the current corpus. This keeps the diagnosis attached to real
// single-region candidates without making a corpus asset edit look like a geometry failure.
FTestWorld World;
World.Build(DiagnosisSeed, 2, true, 8);
World.Build(DiagnosisSeed, 2, 8);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -64,8 +64,7 @@ bool FVoxelForgeSealedSolidProofTest::RunTest(const FString& Parameters)
// Forty voxels makes a complete 32-cell level-0 tile fit inside a bottom boundary seal. The
// production asset remains untouched; this is a transient proof-only fixture configuration.
FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false,
/*InStrateHeightInChunks*/8);
World.Build(/*Seed*/1337, /*GapChunks*/2, /*InStrateHeightInChunks*/8);
if (!World.IsValid())
{
AddError(World.WhyInvalid());
@@ -1325,20 +1325,12 @@ bool FVoxelForgeVerticalShaftSeamTest::RunTest(const FString& Parameters)
{
using namespace VoxelForgeTest;
bool bAllChecksPassed = true;
FString Summary = TEXT("VerticalShafts seam-freedom check (cell-boundary probes with two "
"neighbouring chunk-context orders):\n");
for (const bool bUseOperatorStack : { false, true })
{
FTestWorld World;
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2, bUseOperatorStack);
World.Build(/*InSeed=*/1337, /*InGapChunks=*/2);
if (!World.IsValid())
{
AddError(FString::Printf(
TEXT("HARD FAILURE: could not build the %s seam-test fixture."),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")));
bAllChecksPassed = false;
continue;
AddError(TEXT("HARD FAILURE: could not build the seam-test fixture."));
return false;
}
int32 Checked = 0;
@@ -1346,33 +1338,25 @@ bool FVoxelForgeVerticalShaftSeamTest::RunTest(const FString& Parameters)
FString FirstMismatch;
if (!CheckVerticalShaftSeams(World, Checked, Mismatches, FirstMismatch))
{
AddError(FString::Printf(
TEXT("HARD FAILURE: the %s seam test sampled no VerticalShafts positions."),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")));
bAllChecksPassed = false;
continue;
AddError(TEXT("HARD FAILURE: the seam test sampled no VerticalShafts positions."));
return false;
}
Summary += FString::Printf(
TEXT(" %s path: %d same-position re-evaluations, %d bit mismatches%s%s.\n"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
AddInfo(FString::Printf(
TEXT("VerticalShafts seam-freedom check (cell-boundary probes with two neighbouring "
"chunk-context orders): %d same-position re-evaluations, %d bit mismatches%s%s."),
Checked,
Mismatches,
Mismatches > 0 ? TEXT("; first ") : TEXT(""),
Mismatches > 0 ? *FirstMismatch : TEXT(""));
Mismatches > 0 ? *FirstMismatch : TEXT("")));
if (Mismatches != 0)
{
AddError(FString::Printf(
TEXT("SEAM: %s VerticalShafts density changed after different chunk-context "
"orders: %s"),
bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"),
TEXT("SEAM: VerticalShafts density changed after different chunk-context orders: %s"),
*FirstMismatch));
bAllChecksPassed = false;
return false;
}
}
AddInfo(Summary);
return bAllChecksPassed;
return true;
}
bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters)
@@ -6,9 +6,9 @@
// ---------------
// The interesting invariants (density purity across worker threads, ClassifyTile soundness)
// only fire on the REAL path — UVoxelGenerator::GetDensityAt — because that is where the
// thread_local per-chunk caches live (CP_*, GSurfColCache, the diff slots, the SDF cache).
// Calling GetSurfaceDensity / GetMazeDensity directly bypasses every one of them and would
// test almost nothing. GetDensityAt in turn needs a live UVoxelStrateManager, whose only
// thread_local per-chunk caches live (CP_*, the prepared stacks, the diff slots, the SDF cache).
// Evaluating a stack directly bypasses every one of them and would test almost nothing.
// GetDensityAt in turn needs a live UVoxelStrateManager, whose only
// entry point is Initialize(UVoxelSettings*, int32) reading TSoftObjectPtr pools.
//
// So the fixture builds a whole synthetic world in memory: transient strate definitions →
@@ -46,7 +46,7 @@ namespace VoxelForgeTest
* generator + diff layer. No AActor, no UWorld, no PIE.
*
* The default layout stacks one strate of EVERY archetype (in ECaveGeneratorType order),
* so a single fixture exercises all eight density functions and their per-chunk caches,
* so a single fixture exercises all eight archetype stacks and their per-chunk caches,
* plus the gap-bedrock path when InterStrateGapChunks > 0.
*/
struct FTestWorld
@@ -73,8 +73,7 @@ namespace VoxelForgeTest
* dangerous — and `VoxelForge.Determinism.LargeSeedSurvives` deliberately passes big ones
* (up to 2e9) to prove it stays that way.
*/
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, bool bUseOperatorStack = false,
int32 InStrateHeightInChunks = 4)
void Build(int32 InSeed = 1337, int32 InGapChunks = 2, int32 InStrateHeightInChunks = 4)
{
Settings = TStrongObjectPtr<UVoxelSettings>(
NewObject<UVoxelSettings>(GetTransientPackage(), NAME_None, RF_Transient));
@@ -111,10 +110,6 @@ namespace VoxelForgeTest
// refinement and roll tests use the smaller synthetic volume as a bounded memory
// control; the owner-facing showcase opts into 8 chunks below.
Def->StrateHeightInChunks = FMath::Max(1, InStrateHeightInChunks);
// L'OPT-IN de la pile d'opérateurs. Faux par défaut : les treize tests existants
// doivent continuer à exercer le `switch`, qui reste le comportement de référence.
// Seul le test de solidité de ClassifyTie côté pile le passe à vrai.
Def->bUseOperatorStack = bUseOperatorStack;
// Hard transitions: param blending across a boundary would make "which archetype
// owns this chunk" ambiguous, and these tests want an unambiguous mapping.
Def->TransitionType = EVoxelStrateTransition::Hard;
@@ -235,8 +230,8 @@ namespace VoxelForgeTest
/**
* A spread of world sample points that deliberately crosses chunk boundaries, strate
* boundaries and bedrock gaps — the exact conditions under which a per-chunk cache with a
* missing key input produces a wrong answer. Integer XY on purpose: that is the branch
* GetDensityAt's T1.a column cache actually takes (fractional XY bypasses the cache).
* missing key input produces a wrong answer. Integer XY on purpose: that is the lattice the
* mesher samples, and the one the per-column memos are keyed on.
*/
inline void BuildSamplePoints(const FTestWorld& World, int32 Count, int32 Seed,
TArray<FVector>& OutPoints)
@@ -154,72 +154,50 @@ bool FVoxelForgeTilePostReachProofTest::RunTest(const FString& Parameters)
return false;
}
FTestWorld NativeWorld;
NativeWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
FTestWorld FusedWorld;
FusedWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
if (!NativeWorld.IsValid() || !FusedWorld.IsValid())
FTestWorld World;
World.Build(/*Seed*/1337, /*GapChunks*/2);
if (!World.IsValid())
{
AddError(NativeWorld.IsValid() && FusedWorld.IsValid()
? TEXT("Unexpected invalid reach-proof fixture")
: (!NativeWorld.IsValid() ? NativeWorld.WhyInvalid() : FusedWorld.WhyInvalid()));
AddError(World.WhyInvalid());
return false;
}
TArray<FIntVector> NativeOrigins;
TArray<FIntVector> FusedOrigins;
AppendWorldReachTiles(NativeWorld, NativeOrigins);
AppendWorldReachTiles(FusedWorld, FusedOrigins);
TestTrue(TEXT("the representative native tile set is non-empty"), NativeOrigins.Num() > 0);
TestTrue(TEXT("the representative fused tile set is non-empty"), FusedOrigins.Num() > 0);
TArray<FIntVector> Origins;
AppendWorldReachTiles(World, Origins);
TestTrue(TEXT("the representative tile set is non-empty"), Origins.Num() > 0);
TStrongObjectPtr<UVoxelMarchingCubesMesher> NativeMesher(
TStrongObjectPtr<UVoxelMarchingCubesMesher> Mesher(
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
TStrongObjectPtr<UVoxelMarchingCubesMesher> FusedMesher(
NewObject<UVoxelMarchingCubesMesher>(GetTransientPackage(), NAME_None, RF_Transient));
NativeMesher->SetGenerator(NativeWorld.Generator.Get());
FusedMesher->SetGenerator(FusedWorld.Generator.Get());
Mesher->SetGenerator(World.Generator.Get());
VoxelGenLOD::ResetTilePostReachDiagnostics();
CVarGuard.SetScale(1.0f);
const FReachSnapshot NativeFull = RunReachTiles(*NativeMesher, NativeOrigins);
const FReachSnapshot FusedFull = RunReachTiles(*FusedMesher, FusedOrigins);
const uint64 FullFinalComparisons = NativeFull.FinalComparisons + FusedFull.FinalComparisons;
const uint64 FullFinalDifferences = NativeFull.FinalDifferences + FusedFull.FinalDifferences;
const FReachSnapshot Full = RunReachTiles(*Mesher, Origins);
TestTrue(TEXT("normal reach compares final fields on the representative tiles"),
FullFinalComparisons > 0);
TestEqual(TEXT("normal reach has no final-field differences"), FullFinalDifferences, 0ull);
TestEqual(TEXT("normal reach has no skipped-post differences"),
NativeFull.AllDifferences + FusedFull.AllDifferences, 0ull);
Full.FinalComparisons > 0);
TestEqual(TEXT("normal reach has no final-field differences"), Full.FinalDifferences, 0ull);
TestEqual(TEXT("normal reach has no skipped-post differences"), Full.AllDifferences, 0ull);
VoxelGenLOD::ResetTilePostReachDiagnostics();
// Use a deliberately tiny but valid scale so at least one envelope is rejected while the same
// final-field samples remain exercised; this is the negative control, not a supported setting.
CVarGuard.SetScale(0.01f);
const FReachSnapshot NativeShrunk = RunReachTiles(*NativeMesher, NativeOrigins);
const FReachSnapshot FusedShrunk = RunReachTiles(*FusedMesher, FusedOrigins);
const uint64 ShrunkFinalComparisons =
NativeShrunk.FinalComparisons + FusedShrunk.FinalComparisons;
const uint64 ShrunkFinalDifferences =
NativeShrunk.FinalDifferences + FusedShrunk.FinalDifferences;
const FReachSnapshot Shrunk = RunReachTiles(*Mesher, Origins);
TestTrue(TEXT("shrunk reach still executes final-field comparisons"),
ShrunkFinalComparisons > 0);
Shrunk.FinalComparisons > 0);
TestTrue(TEXT("shrunk reach demonstrably fails the final-field proof"),
ShrunkFinalDifferences > 0);
Shrunk.FinalDifferences > 0);
AddInfo(FString::Printf(
TEXT("tile_post_reach_proof: native_tiles=%llu fused_tiles=%llu full_final=%llu/%llu "
"shrunk_final=%llu/%llu native_samples=%lld/%lld fused_samples=%lld/%lld."),
static_cast<unsigned long long>(NativeFull.Tiles),
static_cast<unsigned long long>(FusedFull.Tiles),
static_cast<unsigned long long>(FullFinalComparisons),
static_cast<unsigned long long>(FullFinalDifferences),
static_cast<unsigned long long>(ShrunkFinalComparisons),
static_cast<unsigned long long>(ShrunkFinalDifferences),
static_cast<long long>(NativeFull.DensitySamples),
static_cast<long long>(NativeShrunk.DensitySamples),
static_cast<long long>(FusedFull.DensitySamples),
static_cast<long long>(FusedShrunk.DensitySamples)));
TEXT("tile_post_reach_proof: tiles=%llu full_final=%llu/%llu shrunk_final=%llu/%llu "
"samples=%lld/%lld."),
static_cast<unsigned long long>(Full.Tiles),
static_cast<unsigned long long>(Full.FinalComparisons),
static_cast<unsigned long long>(Full.FinalDifferences),
static_cast<unsigned long long>(Shrunk.FinalComparisons),
static_cast<unsigned long long>(Shrunk.FinalDifferences),
static_cast<long long>(Full.DensitySamples),
static_cast<long long>(Shrunk.DensitySamples)));
return true;
}
@@ -29,15 +29,13 @@ bool FVoxelForgeVerticalStrateReachTest::RunTest(const FString& Parameters)
Clipmap.ClipRadius = 3;
Clipmap.MaxClipLevel = 3;
Clipmap.RenderDistanceChunks = 768;
Clipmap.bFarSheetRing = true;
Clipmap.FarSheetSpanLevels = 2;
TArray<FVoxelTileKey> Desired;
VoxelClipmapDesiredTiles::Build(
FIntVector(0, 0, PlayerChunkZ), MinChunkZ, MaxChunkZ, Clipmap, Desired);
TArray<int32> CountsByLevel;
CountsByLevel.Init(0, 6);
CountsByLevel.Init(0, 4);
for (const FVoxelTileKey& Tile : Desired)
{
if (CountsByLevel.IsValidIndex(Tile.Level))
@@ -46,7 +44,9 @@ bool FVoxelForgeVerticalStrateReachTest::RunTest(const FString& Parameters)
}
}
const int32 ExpectedCounts[] = {343, 218, 178, 178, 178, 9586};
// Aligned rings (child-pair boundaries) below MaxClipLevel; the MaxClipLevel ring is stretched
// to the 768-chunk render distance (radius 96 at level 3) and clamped to the strate's Z band.
const int32 ExpectedCounts[] = {512, 320, 272, 372442};
for (int32 Level = 0; Level < static_cast<int32>(UE_ARRAY_COUNT(ExpectedCounts)); ++Level)
{
TestEqual(FString::Printf(TEXT("desired tile count at level %d"), Level),
@@ -63,26 +63,28 @@ bool FVoxelForgeVerticalStrateReachTest::RunTest(const FString& Parameters)
TestTrue(TEXT("level 0 tile around the high pawn is present"),
HasTile(0, FIntVector(0, 0, 0)));
TestTrue(TEXT("level 4 bridge tile covering the ground below the pawn is present"),
HasTile(4, FIntVector(0, 0, -3)));
TestTrue(TEXT("adjacent level 4 ground tile is present"),
HasTile(4, FIntVector(1, 0, -3)));
TestTrue(TEXT("outer level 5 sheet reaches the 768-chunk horizon"),
HasTile(5, FIntVector(24, 0, 0)));
TestTrue(TEXT("level 3 tile covering the ground below the pawn is present"),
HasTile(3, FIntVector(0, 0, -8)));
TestTrue(TEXT("adjacent level 3 ground tile is present"),
HasTile(3, FIntVector(1, 0, -8)));
TestTrue(TEXT("the outer level 3 ring reaches the 768-chunk horizon"),
HasTile(3, FIntVector(96, 0, 0)));
TestFalse(TEXT("the outer ring stops at the 768-chunk horizon"),
HasTile(3, FIntVector(97, 0, 0)));
TestFalse(TEXT("clamping does not request a full hidden LOD0 column"),
HasTile(0, FIntVector(0, 0, -47)));
for (int32 Level = 0; Level <= 5; ++Level)
for (int32 Level = 0; Level <= 3; ++Level)
{
TestTrue(FString::Printf(TEXT("clip level %d is non-empty"), Level),
CountsByLevel[Level] > 0);
}
AddInfo(FString::Printf(
TEXT("center_z=%d strate=[%d..%d] view=[%d..%d] levels=[%d,%d,%d,%d,%d,%d] total=%d"),
TEXT("center_z=%d strate=[%d..%d] view=[%d..%d] levels=[%d,%d,%d,%d] total=%d"),
PlayerChunkZ, StrateBottomZ, StrateTopZ, MinChunkZ, MaxChunkZ,
CountsByLevel[0], CountsByLevel[1], CountsByLevel[2], CountsByLevel[3],
CountsByLevel[4], CountsByLevel[5], Desired.Num()));
Desired.Num()));
return true;
}
@@ -67,26 +67,20 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
FailureKinds += Kind;
};
// The ordinary fixture deliberately uses the requested default, so existing op-stack and
// legacy tests exercise the setting without having to know about the new post.
FTestWorld Legacy;
Legacy.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
FTestWorld StackWorld;
StackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
// The ordinary fixture deliberately uses the requested default, so the other tests exercise
// the setting without having to know about the new post.
FTestWorld Bounded;
Bounded.Build(/*Seed*/1337, /*GapChunks*/2);
FTestWorld Unbounded;
Unbounded.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
Unbounded.Build(/*Seed*/1337, /*GapChunks*/2);
if (!Legacy.IsValid() || !StackWorld.IsValid() || !Unbounded.IsValid())
if (!Bounded.IsValid() || !Unbounded.IsValid())
{
AddError(Legacy.IsValid() && StackWorld.IsValid() && Unbounded.IsValid()
? TEXT("Unexpected edge-seal fixture state")
: (!Legacy.IsValid() ? Legacy.WhyInvalid()
: (!StackWorld.IsValid() ? StackWorld.WhyInvalid() : Unbounded.WhyInvalid())));
AddError(!Bounded.IsValid() ? Bounded.WhyInvalid() : Unbounded.WhyInvalid());
return false;
}
SetEdgeSettings(Legacy, TestWorldRadius);
SetEdgeSettings(StackWorld, TestWorldRadius);
SetEdgeSettings(Bounded, TestWorldRadius);
SetEdgeSettings(Unbounded, 0.0f);
//==========================================================================
@@ -103,12 +97,12 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (int32 Slot = 0; Slot < 8; ++Slot)
{
int32 TopZ = 0, BottomZ = 0;
if (Legacy.GetSlotVoxelZRange(Slot, TopZ, BottomZ))
if (Bounded.GetSlotVoxelZRange(Slot, TopZ, BottomZ))
{
StrateZ.Add((float)((TopZ + BottomZ) / 2));
}
}
StrateZ.Add(Legacy.MidVoxelZ());
StrateZ.Add(Bounded.MidVoxelZ());
int32 NumPositiveSamples = 0;
for (const FVector& XY : FarXY)
@@ -117,8 +111,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
{
const FVector P(XY.X, XY.Y, Z);
++NumPositiveSamples;
if (!IsSolidMC(*Legacy.Generator, P)) Fail(TEXT("positive-legacy"));
if (!IsSolidMC(*StackWorld.Generator, P)) Fail(TEXT("positive-opstack"));
if (!IsSolidMC(*Bounded.Generator, P)) Fail(TEXT("positive"));
}
}
@@ -138,7 +131,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (const float Z : StrateZ)
{
const FVector P(XY.X, XY.Y, Z);
const float BoundedD = Legacy.Generator->GetDensityAt(P.X, P.Y, P.Z);
const float BoundedD = Bounded.Generator->GetDensityAt(P.X, P.Y, P.Z);
const float UnboundedD = Unbounded.Generator->GetDensityAt(P.X, P.Y, P.Z);
++NumInteriorComparisons;
if (!BitEqual(BoundedD, UnboundedD)) Fail(TEXT("interior-not-no-op"));
@@ -193,7 +186,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
// CLASSIFYBOX — force boxes in the ramp and beyond the radius, then brute-force every voxel.
//==========================================================================
int32 TopShaftZ = 0, BottomShaftZ = 0;
if (!StackWorld.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ))
if (!Bounded.GetSlotVoxelZRange(FTestWorld::SlotVerticalShafts, TopShaftZ, BottomShaftZ))
{
Fail(TEXT("missing-shaft-slot"));
}
@@ -218,17 +211,17 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
};
const int32 MidChunkZ = FMath::FloorToInt((BoxZ0 + BoxZ1) * 0.5f / CHUNK_SIZE);
const FVerticalShaftParams ShaftParams = StackWorld.StrateManager->GetVerticalShaftParamsForChunk(
const FVerticalShaftParams ShaftParams = Bounded.StrateManager->GetVerticalShaftParamsForChunk(
FIntVector(0, 0, MidChunkZ));
FVoxelOpStack Stack;
VoxelDensityOps::BuildVerticalShaftStack(
Stack, ShaftParams, StackWorld.Settings->Seed,
StackWorld.Generator->OriginSpineRadius, StackWorld.StrateManager.Get());
Stack, ShaftParams, Bounded.Settings->Seed,
Bounded.Generator->OriginSpineRadius, Bounded.StrateManager.Get());
FVoxelOpContext Ctx;
Ctx.ChunkCoord = FIntVector(0, 0, MidChunkZ);
Ctx.Seed = (uint32)StackWorld.Settings->Seed;
Ctx.LayoutVersion = StackWorld.StrateManager->GetLayoutVersion();
Ctx.Seed = (uint32)Bounded.Settings->Seed;
Ctx.LayoutVersion = Bounded.StrateManager->GetLayoutVersion();
Ctx.StrateTopWorldZ = ShaftParams.StrateTopWorldZ;
Ctx.StrateBottomWorldZ = ShaftParams.StrateBottomWorldZ;
Ctx.WorldRadiusVoxels = TestWorldRadius;
@@ -255,14 +248,14 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
for (int32 X = X0; X <= X1; ++X)
{
++NumVoxelsChecked;
if (!IsSolidMC(*StackWorld.Generator, FVector((float)X, (float)Y, (float)Z)))
if (!IsSolidMC(*Bounded.Generator, FVector((float)X, (float)Y, (float)Z)))
Fail(TEXT("classifybox-false-allsolid"));
}
}
}
// The T1.d path must be able to skip an edge tile without knowing which Z category it hits.
const EVoxelTileClass EdgeTile = StackWorld.Generator->ClassifyTile(
const EVoxelTileClass EdgeTile = Bounded.Generator->ClassifyTile(
FIntVector(320, 0, BottomShaftZ + 8), 1, 8);
int32 NumTilesProved = (EdgeTile == EVoxelTileClass::AllSolid) ? 1 : 0;
if (EdgeTile != EVoxelTileClass::AllSolid) Fail(TEXT("classifytile-missed-edge"));
@@ -283,9 +276,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
// coverage only: the synthetic helper case above is the hard assertion even if landing-site
// adjustment moves every generated mouth out of this small test rim.
FTestWorld GeneratedPassageWorld;
GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/false);
FTestWorld GeneratedPassageStackWorld;
GeneratedPassageStackWorld.Build(/*Seed*/1337, /*GapChunks*/2, /*bUseOperatorStack*/true);
GeneratedPassageWorld.Build(/*Seed*/1337, /*GapChunks*/2);
auto CheckGeneratedPassages = [&](FTestWorld& World, const TCHAR* FailureKind)
{
if (!World.IsValid())
@@ -310,7 +301,6 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
}
};
CheckGeneratedPassages(GeneratedPassageWorld, TEXT("generated-passage-breaches-rim"));
CheckGeneratedPassages(GeneratedPassageStackWorld, TEXT("generated-opstack-passage-breaches-rim"));
// One sorted summary block; individual samples intentionally never log.
AddInfo(FString::Printf(
@@ -319,7 +309,7 @@ bool FVoxelForgeWorldEdgeSealTest::RunTest(const FString& Parameters)
TEXT(" classify_tile_proved=%d\n")
TEXT(" interior_comparisons=%d\n")
TEXT(" near_rim_passage_checks=%d\n")
TEXT(" positive_sample_points=%d (checked in both paths)\n")
TEXT(" positive_sample_points=%d\n")
TEXT(" rim_directions=%d\n")
TEXT(" rim_max_adjacent_solidness_jump=%.6f (threshold=%.6f)\n")
TEXT(" voxels_checked=%lld\n")
@@ -43,11 +43,11 @@ void UVoxelAtmosphereManager::UpdateForPlayer(const FVector& PlayerWorldPos)
if (!O) return;
// Le champ de densite est en espace ACTEUR : les requetes de strate et de biome veulent des
// coordonnees LOCALES. Le placement d acteurs, lui, veut du MONDE. Les deux cohabitent ici, ce
// qui est exactement pourquoi ce fichier supposait "acteur a l origine" avant le 2026-08-17.
// coordonnees LOCALES. Le placement d acteurs, lui, veut du MONDE. Les deux cohabitent ici : ne
// pas supposer "acteur a l origine".
// The density field is in ACTOR space: strate and biome queries want LOCAL coordinates, while
// actor placement wants WORLD. Both live in this function - which is exactly why it used to
// assume "actor at origin / identity".
// actor placement wants WORLD. Both live in this function - do not assume "actor at origin /
// identity".
const FTransform Xf = O->GetActorTransform();
const FVector LocalPos = Xf.InverseTransformPosition(PlayerWorldPos);
+20 -444
View File
@@ -51,7 +51,7 @@ namespace
// Density coordinates and cached cull radii are float-valued. FVector is double-valued in
// UE5, so using FVector::DistSquared here needlessly widens every candidate reject. Keep the
// cull in the same build-fixed float domain as the density query; the exact field output is
// intentionally no longer required to match the pre-optimization double round-trip.
// intentionally not required to match a double-precision round-trip.
FORCEINLINE float VF_FloatDistSquared(
float X, float Y, float Z, const FVector& Center)
{
@@ -81,7 +81,6 @@ namespace
struct FVFTunnelShapeEvaluation
{
float SDF = FLT_MAX;
bool bSupportFloor = false;
bool bHasSweptFloor = false;
float SweptFloorZ = -FLT_MAX;
float SweptFloorRadius = 0.0f;
@@ -548,7 +547,6 @@ namespace
Profile.NaturalStartFloorZ = FloorA;
Profile.NaturalEndFloorZ = FloorB;
Profile.bLedgeTransition = false;
Profile.NumSteps = 0;
if (bHasLedgeLevels)
{
@@ -934,27 +932,6 @@ namespace
return FloorZ;
}
FORCEINLINE float VF_TunnelFloorAtSegment(
const FVector& A, float RadiusA,
const FVector& B, float RadiusB,
float T, int32 SegmentIndex, int32 NumSegments,
const FVector& Position,
const FCachedTunnel& Tunnel,
const void* CacheIdentity, int32 TunnelIndex,
bool bEvaluateRelief,
const FVFFloorReliefColumnKey& Column,
const FTunnelFloorSegmentProfile* FloorProfile,
const FVFRoomFloorOwnership* RoomFloorOwnership)
{
const FVFTunnelFloorTerms Terms = VF_TunnelFloorTerms(
A, RadiusA, B, RadiusB,
T, SegmentIndex, NumSegments, Tunnel, FloorProfile,
RoomFloorOwnership);
return VF_TunnelFloorFromTerms(
Position, Tunnel, Terms,
CacheIdentity, TunnelIndex, bEvaluateRelief, Column);
}
FORCEINLINE bool VF_ProjectTunnelSegmentXY(
const FVector& Position, const FVector& A, const FVector& B,
float& OutT, float& OutDistanceSquared)
@@ -1229,76 +1206,6 @@ namespace
RoomFloorOwnership);
}
static bool VF_ProjectSweptTunnelFloor(
const FVector& Position,
const FCachedTunnel& Tunnel,
bool bWorldChain,
float& OutFloorZ,
float& OutSupportRadius,
const void* CacheIdentity, int32 TunnelIndex,
const FVFRoomFloorOwnership* RoomFloorOwnership = nullptr)
{
const FVFFloorReliefColumnKey ReliefColumn =
VF_MakeFloorReliefColumnKey(
static_cast<float>(Position.X), static_cast<float>(Position.Y));
const TArray<FVector>& ControlPoints = bWorldChain
? Tunnel.WorldControlPoints : Tunnel.ControlPoints;
const TArray<float>& ControlRadii = bWorldChain
? Tunnel.WorldControlRadii : Tunnel.ControlRadii;
if (ControlPoints.Num() < 2
|| ControlRadii.Num() != ControlPoints.Num())
{
return false;
}
const int32 ControlPointCount = ControlPoints.Num();
const int32 NumSegments = ControlPointCount - 1;
const FVector* ControlPointData = ControlPoints.GetData();
const float* ControlRadiusData = ControlRadii.GetData();
const TArray<FTunnelFloorSegmentProfile>* FloorProfiles = bWorldChain
? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles;
const FTunnelFloorSegmentProfile* FloorProfileData =
FloorProfiles->Num() == NumSegments ? FloorProfiles->GetData() : nullptr;
float BestDistanceSquared = FLT_MAX;
bool bFound = false;
for (int32 SegmentIndex = 0;
SegmentIndex < NumSegments;
++SegmentIndex)
{
float T = 0.0f;
float DistanceSquared = FLT_MAX;
if (!VF_ProjectTunnelSegmentXY(
Position,
ControlPointData[SegmentIndex], ControlPointData[SegmentIndex + 1],
T, DistanceSquared)
|| DistanceSquared >= BestDistanceSquared)
{
continue;
}
const float RadiusA = ControlRadiusData[SegmentIndex];
const float RadiusB = ControlRadiusData[SegmentIndex + 1];
OutFloorZ = VF_TunnelFloorAtSegment(
ControlPointData[SegmentIndex], RadiusA,
ControlPointData[SegmentIndex + 1], RadiusB,
T, SegmentIndex, NumSegments, Position, Tunnel,
CacheIdentity, TunnelIndex, /*bEvaluateRelief=*/true,
ReliefColumn,
FloorProfileData != nullptr ? FloorProfileData + SegmentIndex : nullptr,
RoomFloorOwnership);
OutSupportRadius = FMath::Max(
FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f,
VoxelPassageGeometry::PlayerRadiusVoxels);
BestDistanceSquared = DistanceSquared;
bFound = true;
}
return bFound
&& VoxelMath::IsFinite(OutFloorZ)
&& VoxelMath::IsFinite(OutSupportRadius)
&& BestDistanceSquared <= FMath::Square(OutSupportRadius);
}
}
void FChunkSDFSpatialIndex::Reset()
@@ -1449,7 +1356,7 @@ namespace
}
// Never turn a malformed authored bound into a giant allocation. A failed build is
// deliberately indistinguishable from the old path to the density evaluator.
// deliberately indistinguishable from the unindexed linear scan to the density evaluator.
constexpr int64 MaxBuckets = 1024 * 1024;
constexpr int64 MaxReferences = 4 * 1024 * 1024;
TArray<int32> ItemMinX;
@@ -3719,11 +3626,11 @@ namespace
// Once the lower and upper ends bracket the room, the authored room/floor field is
// monotone through this narrow vertical landing band for the finite parameter envelope
// used by the generator. Find the one-voxel bucket with a logarithmic search, then run
// the exact legacy bisection in that bucket. The latter is kept byte-for-byte in its
// arithmetic and iteration count so the endpoint remains identical to the old scan.
// the exact bisection in that bucket. The latter is kept byte-for-byte in its arithmetic
// and iteration count so the endpoint is identical to the one-voxel walk below.
// If the bracket is not monotone (or the local bucket check cannot reproduce the crossing),
// fall back to the original one-voxel walk below; malformed/unsupported inputs therefore
// retain the old behavior.
// fall back to that one-voxel walk; malformed/unsupported inputs therefore keep its
// result.
auto BisectBracket = [&](float BracketLow, float BracketHigh,
float BracketLowValue) -> bool
{
@@ -5396,9 +5303,9 @@ namespace
}
// The slab source is an XY height band. Evaluate the same two pure height fields as
// GetSlabDensity, then choose their midpoint after intersecting the seal-free interior.
// FSlabVoidSource, then choose their midpoint after intersecting the seal-free interior.
// La source slab est une bande de hauteurs XY : on recalcule les deux champs purs comme
// GetSlabDensity, puis on prend leur milieu après intersection avec l'intérieur sans seal.
// FSlabVoidSource, puis on prend leur milieu après intersection avec l'intérieur sans seal.
bool VF_SuggestSlabLandingPoint(
const FSlabGenerationParams& Params,
int32 Seed,
@@ -5537,7 +5444,8 @@ namespace
// Maze corridors are lattice edges. Only a horizontal edge is a useful landing source: a
// vertical edge can prove air, but cannot prove a place to stand. The edge set and Z levels
// below use the same origin-directed tree + loop contract as GetMazeDensity, with no cache.
// below use the same origin-directed tree + loop contract as FLatticeCorridorSource, with no
// cache.
bool VF_SuggestMazeLandingPoint(
const FMazeGenerationParams& Params,
int32 Seed,
@@ -5958,14 +5866,6 @@ namespace
SampleShaft,
OutPoint))
{
if (Seed == 14 && FMath::IsNearlyEqual(StrateBottomZ, -736.0f))
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeExplore][TemporaryVerticalLanding] axis=(%.3f,%.3f) "
"safeRadius=%.3f candidate=(%.3f,%.3f,%.3f)"),
BestAxisX, BestAxisY, BestSafeRadius,
CandidateFeet.X, CandidateFeet.Y, CandidateFeet.Z);
}
return true;
}
}
@@ -5980,7 +5880,7 @@ namespace
};
// Evaluate only the island source, in the same internal-density convention as
// GetFloatingIslandDensity (positive = solid, negative = void). This is intentionally a
// FIslandBlobSource (positive = solid, negative = void). This is intentionally a
// local pure evaluator: it lets the query bracket the actual blob top instead of treating a
// nominal island centre as footing. Full-octave noise is used because this query has no LOD
// state by design and GeneratePassages runs before a tile's LOD is selected.
@@ -6017,7 +5917,7 @@ namespace
for (int32 IslandIndex = 0; IslandIndex < NumIslands; ++IslandIndex)
{
const FVFIslandSite& Island = Islands[IslandIndex];
// Production GetFloatingIslandDensity evaluates exactly this 3x3 cell neighbourhood
// Production FIslandBlobSource evaluates exactly this 3x3 cell neighbourhood
// for the queried XY. The outer search may collect more candidates so a lateral snap
// can be found, but those distant blobs must not influence the returned footing.
if (FMath::Abs(Island.CellX - BaseCellX) > 1
@@ -7221,14 +7121,14 @@ void VoxelCaveMorphology::BuildChunkCache(
// (Frontier rooms with no closer candidate in reach fall back to plain NN — a far
// cluster stays internally chained even when it can't bridge to the origin side.)
//
// bTunnelsFlowTowardOrigin = false (legacy): plain nearest-neighbor pairing. NOTE:
// despite what this comment used to claim, an NN-graph is a FOREST of small clusters,
// not a connected tree — isolated cave pockets are expected in this mode.
// bTunnelsFlowTowardOrigin = false (legacy): plain nearest-neighbor pairing. NOTE: an
// NN-graph is a FOREST of small clusters, not a connected tree — isolated cave pockets
// are expected in this mode.
//
// Selection metric (not the reach filter) penalizes vertical separation via
// TunnelHorizontalBias, so the GUARANTEED links also prefer walkable slopes —
// previously only the random TunnelDensity extras were biased, which is why
// backbone tunnels could come out absurdly steep.
// biasing only the random TunnelDensity extras lets backbone tunnels come out
// absurdly steep.
TArray<int32, TInlineAllocator<64>> NearestNeighbor;
NearestNeighbor.SetNumUninitialized(NumRooms);
@@ -8367,7 +8267,7 @@ void VoxelCaveMorphology::BuildChunkCache(
MaxExtent += FloorReliefEnvelope + BlendEnvelope * 3.0f;
// --- PRE-BAKED SHAPE ---
// Same hash roll + thresholds + capsule trig the evaluator used to redo PER VOXEL;
// Same hash roll + thresholds + capsule trig the evaluator would otherwise redo PER VOXEL;
// done once here → EvaluateSDFCached just switches on ShapeType. Bit-identical output.
{
const uint32 ShapeHash = VoxelHash::Mix(CR.Hash ^ 0xDEADBEEFu);
@@ -8859,7 +8759,7 @@ float VoxelCaveMorphology::EvaluateSDFCached(
// A room owns every point inside its authored body. The short mouth ownership fade below
// handles the fillet outside the wall; this broader test is the hard A > B rule for the room
// interior. The exact raw shape sign was captured during the room SDF pass above, so this
// point no longer performs a second spatial-index traversal.
// point does not perform a second spatial-index traversal.
auto EvaluateTunnel = [&](int32 TunnelIdx)
{
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
@@ -8960,50 +8860,6 @@ float VoxelCaveMorphology::EvaluateSDFCached(
return MinSDF;
}
float VoxelCaveMorphology::EvaluateTunnelCoreSDF(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
bool bUseSpatialIndex)
{
if (VoxelDensityAblation::IsTunnelCoreOff())
{
return FLT_MAX;
}
const FVector Pos(WorldX, WorldY, WorldZ);
float MinSDF = FLT_MAX;
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
auto EvaluateTunnel = [&](int32 TunnelIdx)
{
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
if (Tunnel.SDFInfluenceRadius > 0.0f
&& VF_DistanceSquaredToAabb(
WorldX, WorldY, WorldZ,
Tunnel.SDFCenterlineMin, Tunnel.SDFCenterlineMax)
> FMath::Square(Tunnel.SDFInfluenceRadius))
{
return;
}
if (VF_FloatDistSquared(WorldX, WorldY, WorldZ, Tunnel.BoundCenter)
> Tunnel.BoundRadiusSq)
{
return;
}
MinSDF = FMath::Min(
MinSDF,
VF_EvaluateSweptTunnel(
Pos, Tunnel, /*bWorldChain=*/false, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/false, &Cache, TunnelIdx).SDF);
};
VF_ForEachSpatialCandidate(
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel,
bUseSpatialIndex);
return MinSDF;
}
FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
@@ -9454,7 +9310,7 @@ bool VoxelCaveMorphology::AnyTunnelCoreWorldNearLattice(
// The broad sphere already encloses the swept radius and finite floor band. Add the
// vertical relief and room-mouth ownership envelopes before asking whether the lattice
// box can touch it. A miss is therefore an identity proof for the core evaluator and
// its support/room-floor tail; its only field writes are bSupportFloor, bRoomFloor, or
// its support/room-floor tail; its only field writes are bRoomFloor or
// CoreSDF < -inset, all of which are inside this envelope.
const float Reach = (
FMath::Sqrt(FMath::Max(BoundRadiusSq, 0.0f))
@@ -9477,254 +9333,6 @@ float VoxelCaveMorphology::EvaluateTunnelCoreWorldSDF(
WorldX, WorldY, WorldZ, Cache, nullptr, bUseSpatialIndex).SDF;
}
bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
bool bUseSpatialIndex)
{
if (VoxelDensityAblation::IsTunnelCoreOff())
{
return false;
}
const FVector Pos(WorldX, WorldY, WorldZ);
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
auto TestTunnel = [&](int32 TunnelIdx) -> bool
{
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2
&& Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num();
const FVector& CenterlineMin = bHasWorldChain
? Tunnel.WorldCenterlineMin : Tunnel.SDFCenterlineMin;
const FVector& CenterlineMax = bHasWorldChain
? Tunnel.WorldCenterlineMax : Tunnel.SDFCenterlineMax;
const float InfluenceRadius = bHasWorldChain
? Tunnel.WorldInfluenceRadius : Tunnel.SDFInfluenceRadius;
if (InfluenceRadius > 0.0f
&& VF_DistanceSquaredToAabb(
WorldX, WorldY, WorldZ, CenterlineMin, CenterlineMax)
> FMath::Square(InfluenceRadius))
{
return false;
}
const FVector& BoundCenter = bHasWorldChain
? Tunnel.WorldBoundCenter : Tunnel.BoundCenter;
const float BoundRadiusSq = bHasWorldChain
? Tunnel.WorldBoundRadiusSq : Tunnel.BoundRadiusSq;
if (BoundRadiusSq > 0.0f
&& VF_FloatDistSquared(WorldX, WorldY, WorldZ, BoundCenter) > BoundRadiusSq)
{
return false;
}
const FVFRoomFloorOwnership RoomMouthOwnership =
VF_FindWorldTunnelMouthOwnership(Pos, Tunnel, Cache.SDFBlendRadius);
if (RoomMouthOwnership.bValid)
{
float ProjectedFloorZ = 0.0f;
float SupportRadius = 0.0f;
if (VF_ProjectSweptTunnelFloor(
Pos, Tunnel, bHasWorldChain, ProjectedFloorZ, SupportRadius,
&Cache, TunnelIdx, &RoomMouthOwnership)
&& ProjectedFloorZ > RoomMouthOwnership.FloorZ + KINDA_SMALL_NUMBER)
{
// Only a raised tunnel apron is clipped. If the tunnel is lower, the room's
// SmoothMax floor already wins naturally and retaining this support band is
// capability-safe for legacy callers.
return false;
}
}
return VF_EvaluateSweptTunnel(
Pos, Tunnel, bHasWorldChain, Cache.SDFBlendRadius,
/*bApplyFloorCut=*/true, &Cache, TunnelIdx,
&RoomMouthOwnership).bSupportFloor;
};
bool bSupport = false;
int32 CandidateCount = 0;
auto TestRange = [&](int32 Begin, int32 End)
{
const int32* ItemIndexData = Cache.TunnelSpatialIndex.ItemIndices.GetData();
for (int32 Cursor = Begin; Cursor < End; ++Cursor)
{
const int32 TunnelIdx = ItemIndexData[Cursor];
if (!Cache.Tunnels.IsValidIndex(TunnelIdx))
{
continue;
}
++CandidateCount;
if (TestTunnel(TunnelIdx))
{
bSupport = true;
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
}
break;
}
}
};
int32 Begin = 0;
int32 End = 0;
if (bUseSpatialIndex && Cache.TunnelSpatialIndex.GetRange(WorldX, WorldY, Begin, End))
{
TestRange(Begin, End);
}
else
{
for (int32 TunnelIdx = 0; TunnelIdx < Cache.Tunnels.Num() && !bSupport; ++TunnelIdx)
{
++CandidateCount;
if (TestTunnel(TunnelIdx))
{
bSupport = true;
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreEvaluated);
}
}
}
}
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelCoreCandidates,
static_cast<uint64>(CandidateCount));
}
return bSupport;
}
void VoxelCaveMorphology::BuildTunnelSupportFloorColumn(
float WorldX, float WorldY,
const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn,
bool bUseSpatialIndex)
{
if (VoxelDensityAblation::IsTunnelCoreOff())
{
OutColumn.Reset();
return;
}
VoxelDensityProfile::FScopedTimer ProfileTimer(
VoxelDensityProfile::EBucket::TunnelCoreSupport);
OutColumn.Reset();
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportColumnBuilds);
}
const FVector ColumnPosition(WorldX, WorldY, 0.0f);
const FCachedTunnel* TunnelData = Cache.Tunnels.GetData();
auto EvaluateTunnel = [&](int32 TunnelIdx)
{
const FCachedTunnel& Tunnel = TunnelData[TunnelIdx];
const FVFRoomFloorOwnership RoomMouthOwnership =
VF_FindWorldTunnelMouthOwnership(ColumnPosition, Tunnel, Cache.SDFBlendRadius);
float FloorZ = 0.0f;
float SupportRadius = 0.0f;
if (!VF_ProjectSweptTunnelFloor(
ColumnPosition, Tunnel, /*bWorldChain=*/true,
FloorZ, SupportRadius, &Cache, TunnelIdx, &RoomMouthOwnership))
{
return;
}
float MinZ = FloorZ - VoxelPassageGeometry::LandingFloorThicknessVoxels;
float MaxZ = FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels;
// Preserve the public point predicate's world bounding sphere exactly, while resolving
// its Z interval once. The XY projection already proves the support-radius condition.
if (Tunnel.WorldBoundRadiusSq > 0.0f)
{
const float DX = WorldX - static_cast<float>(Tunnel.WorldBoundCenter.X);
const float DY = WorldY - static_cast<float>(Tunnel.WorldBoundCenter.Y);
const float Remaining = Tunnel.WorldBoundRadiusSq - (DX * DX + DY * DY);
if (Remaining < 0.0f)
{
return;
}
const float HalfZ = FMath::Sqrt(Remaining);
MinZ = FMath::Max(MinZ, static_cast<float>(Tunnel.WorldBoundCenter.Z) - HalfZ);
MaxZ = FMath::Min(MaxZ, static_cast<float>(Tunnel.WorldBoundCenter.Z) + HalfZ);
}
if (MinZ <= MaxZ)
{
FTunnelSupportFloorInterval& Interval =
OutColumn.Intervals.Emplace_GetRef();
Interval.TunnelIndex = TunnelIdx;
Interval.FloorZ = FloorZ;
Interval.MinZ = MinZ;
Interval.MaxZ = MaxZ;
}
};
const int32 CandidateCount = VF_ForEachSpatialCandidate(
Cache.TunnelSpatialIndex, Cache.Tunnels.Num(), WorldX, WorldY, EvaluateTunnel,
bUseSpatialIndex);
if (VoxelDensityProfile::AreCountersEnabled())
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates,
static_cast<uint64>(CandidateCount));
}
// Candidate IDs are deterministic and allow the world evaluator to find a
// tunnel's own band without re-running the XY projection. The public union
// query below simply tests every band; graph columns normally contain only a
// handful of candidates.
OutColumn.Intervals.Sort(
[](const FTunnelSupportFloorInterval& A,
const FTunnelSupportFloorInterval& B)
{
return A.TunnelIndex < B.TunnelIndex;
});
}
bool VoxelCaveMorphology::IsTunnelSupportFloorColumnZ(
float WorldZ,
const FTunnelSupportFloorColumn& Column)
{
for (const FTunnelSupportFloorInterval& Interval : Column.Intervals)
{
if (WorldZ <= Interval.MaxZ)
{
if (WorldZ >= Interval.MinZ)
{
return true;
}
}
}
return false;
}
bool VoxelCaveMorphology::GetTunnelSupportFloorColumnBand(
int32 TunnelIndex,
const FTunnelSupportFloorColumn& Column,
float& OutFloorZ,
float& OutMinZ,
float& OutMaxZ)
{
for (const FTunnelSupportFloorInterval& Interval : Column.Intervals)
{
if (Interval.TunnelIndex == TunnelIndex)
{
OutFloorZ = Interval.FloorZ;
OutMinZ = Interval.MinZ;
OutMaxZ = Interval.MaxZ;
return true;
}
}
return false;
}
//=============================================================================
// CONVENIENCE WRAPPER (backward compatible)
//=============================================================================
@@ -9734,35 +9342,3 @@ bool VoxelCaveMorphology::GetTunnelSupportFloorColumnBand(
// internally widens the COLLECT region to (2*MaxTunnelLength + MaxInfluence) so the
// graph it builds is the same one the chunk path would build at this point.
float VoxelCaveMorphology::EvaluateSDF(
float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 Seed, int32 StrateIndex)
{
const float RoomRadiusEnvelope = FMath::Max(
FMath::Abs(Params.MinRoomRadius), FMath::Abs(Params.MaxRoomRadius));
const float TunnelRadiusEnvelope = FMath::Max(
0.5f,
FMath::Max(FMath::Abs(Params.TunnelMinRadius), FMath::Abs(Params.TunnelMaxRadius))
* 1.18f);
const float FloorReliefEnvelope = FMath::Abs(Params.FloorReliefStrength)
* VOXEL_NOISE_SCALE * 1.5f;
const float BlendEnvelope = FMath::Max(Params.SDFBlendRadius, 0.0f);
const float Margin = FMath::Max(
RoomRadiusEnvelope + FloorReliefEnvelope,
FMath::Abs(Params.TunnelWarpStrength) + TunnelRadiusEnvelope
) + BlendEnvelope * 3.0f;
FChunkSDFCache TempCache;
BuildChunkCache(
TempCache,
WorldX - Margin, WorldY - Margin,
WorldX + Margin, WorldY + Margin,
Params, Seed, StrateIndex
);
return EvaluateSDFCached(
WorldX, WorldY, WorldZ,
TempCache, Params.SDFBlendRadius
);
}
@@ -9,8 +9,6 @@ namespace VoxelClipmapDesiredTiles
int32 ClipRadius = 3;
int32 MaxClipLevel = 4;
int32 RenderDistanceChunks = 0;
bool bFarSheetRing = true;
int32 FarSheetSpanLevels = 2;
};
FORCEINLINE int32 FloorDiv(int32 Value, int32 Divisor)
@@ -60,12 +58,6 @@ namespace VoxelClipmapDesiredTiles
{
OutLevel = MaxLevel;
OutRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
if (Parameters.bFarSheetRing && OutRadius > Radius)
{
OutLevel = MaxLevel + FMath::Clamp(Parameters.FarSheetSpanLevels, 1, 4);
OutRadius = FMath::Max(1,
(Parameters.RenderDistanceChunks + (1 << OutLevel) - 1) >> OutLevel);
}
}
FORCEINLINE void ResolveVerticalBounds(
@@ -97,37 +89,21 @@ namespace VoxelClipmapDesiredTiles
const int32 Radius = FMath::Max(1, Parameters.ClipRadius);
const int32 MaxLevel = FMath::Clamp(Parameters.MaxClipLevel, 0, 8);
const int32 OuterRadius = OuterShellRadius(Parameters, Radius, MaxLevel);
int32 SheetLevel = MaxLevel;
int32 SheetRadius = OuterRadius;
OuterShell(Parameters, Radius, MaxLevel, SheetLevel, SheetRadius);
const bool bSheetRing = SheetLevel > MaxLevel;
const int32 MaxMCRingLevel = bSheetRing ? SheetLevel - 1 : MaxLevel;
FIntVector PreviousRingMin = FIntVector::ZeroValue;
FIntVector PreviousRingMax = FIntVector::ZeroValue;
FIntVector MCRingMin = FIntVector::ZeroValue;
FIntVector MCRingMax = FIntVector::ZeroValue;
for (int32 Level = 0; Level <= MaxMCRingLevel; ++Level)
for (int32 Level = 0; Level <= MaxLevel; ++Level)
{
const int32 Pow = 1 << Level;
const FIntVector CenterLevel = FloorDiv(Center, Pow);
const int32 LevelRadius = (Level == MaxLevel && !bSheetRing)
? OuterRadius : Radius;
const int32 LevelRadius = (Level == MaxLevel) ? OuterRadius : Radius;
// Every finer shell must end on a complete child-pair boundary so a coarser tile is
// either wholly inside it or wholly outside it. The final MC ring uses the sheet's
// grouping width when a sheet ring follows it; otherwise the ordinary LOD boundary
// only needs pair alignment.
const int32 RingAlignment = Level < MaxMCRingLevel
? 2
: (bSheetRing ? (1 << (SheetLevel - MaxMCRingLevel)) : 1);
// either wholly inside it or wholly outside it. The outermost ring has no coarser
// neighbour and needs no alignment.
const int32 RingAlignment = Level < MaxLevel ? 2 : 1;
FIntVector RingMin, RingMax;
AlignedRingBounds(CenterLevel, LevelRadius, RingAlignment, RingMin, RingMax);
if (Level == MaxMCRingLevel)
{
MCRingMin = RingMin;
MCRingMax = RingMax;
}
int32 DeltaZMin = RingMin.Z - CenterLevel.Z;
int32 DeltaZMax = RingMax.Z - CenterLevel.Z;
@@ -172,48 +148,5 @@ namespace VoxelClipmapDesiredTiles
PreviousRingMin = RingMin;
PreviousRingMax = RingMax;
}
if (bSheetRing)
{
const int32 SheetPow = 1 << SheetLevel;
const FIntVector CenterSheet = FloorDiv(Center, SheetPow);
const int32 LevelDelta = SheetLevel - MaxMCRingLevel;
const int32 MCPerSheet = 1 << LevelDelta;
int32 DeltaZMin = -SheetRadius;
int32 DeltaZMax = SheetRadius;
if (MinChunkZ != MIN_int32)
{
DeltaZMin = FMath::Max(DeltaZMin,
FloorDiv(MinChunkZ, SheetPow) - CenterSheet.Z);
DeltaZMax = FMath::Min(DeltaZMax,
FloorDiv(MaxChunkZ, SheetPow) - CenterSheet.Z);
}
for (int32 DeltaZ = DeltaZMin; DeltaZ <= DeltaZMax; ++DeltaZ)
for (int32 DeltaY = -SheetRadius; DeltaY <= SheetRadius; ++DeltaY)
for (int32 DeltaX = -SheetRadius; DeltaX <= SheetRadius; ++DeltaX)
{
const FIntVector Tile = CenterSheet + FIntVector(DeltaX, DeltaY, DeltaZ);
const int32 TileMinZ = Tile.Z << SheetLevel;
const int32 TileMaxZ = ((Tile.Z + 1) << SheetLevel) - 1;
if (TileMaxZ < MinChunkZ || TileMinZ > MaxChunkZ)
{
continue;
}
const bool bCovered =
(Tile.X * MCPerSheet >= MCRingMin.X)
&& ((Tile.X + 1) * MCPerSheet - 1 <= MCRingMax.X)
&& (Tile.Y * MCPerSheet >= MCRingMin.Y)
&& ((Tile.Y + 1) * MCPerSheet - 1 <= MCRingMax.Y)
&& (Tile.Z * MCPerSheet >= MCRingMin.Z)
&& ((Tile.Z + 1) * MCPerSheet - 1 <= MCRingMax.Z);
if (!bCovered)
{
OutTiles.Emplace(Tile, SheetLevel);
}
}
}
}
}
+104 -251
View File
@@ -1,22 +1,12 @@
// VoxelDensityOpStack.cpp
// Les opérateurs concrets de la Phase 1 : la décomposition de Maze + le post-traitement structurel.
// The concrete Phase 1 operators: the Maze decomposition + the structural post-process.
// Les opérateurs concrets : les décompositions d'archétypes + le post-traitement structurel.
// The concrete operators: the archetype decompositions + the structural post-process.
//
// Ces opérateurs alimentent le jeu derrière l'opt-in décrit dans l'en-tête de VoxelDensityOpStack.h.
// Ces opérateurs génèrent chaque strate du jeu (voir l'en-tête de VoxelDensityOpStack.h).
// These operators generate every strate in the game (see the VoxelDensityOpStack.h header).
//
// FIDÉLITÉ / FIDELITY
// Chaque corps ci-dessous est une transcription LITTÉRALE du bloc correspondant de
// `UVoxelGenerator::GetMazeDensity` — mêmes hashes, mêmes constantes, même ordre d'opérations
// flottantes, même convention de signe (INTERNE : positif = solide). L'objectif est
// l'égalité BIT à BIT, vérifiée par `VoxelForge.OpStack.MazeEquivalence`.
//
// `OPSTACK-PLAN §2.6` n'EXIGE pas l'identité binaire avec l'ancien système — mais Maze se
// décompose si proprement qu'on peut l'obtenir, et quand on peut l'obtenir il faut la prendre :
// une égalité binaire transforme « je crois que la décomposition est correcte » en preuve.
//
// §2.6 does not REQUIRE bit-identity with the old system — but Maze decomposes cleanly enough that
// it is achievable, and when it is achievable it should be taken: bit-equality turns "I believe the
// decomposition is right" into a proof.
// Convention de signe INTERNE : positif = solide ; la pile négative une fois pour le MC.
// Internal sign convention: positive = solid; the stack negates once for MC.
#include "VoxelDensityOpStack.h"
#include "VoxelStrateComposer.h"
@@ -92,23 +82,20 @@ namespace
* gradients.) Se tromper ici coûte une boîte de recherche un peu plus large, jamais un
* verdict faux : plus large ⇒ SUR-ensemble de primitives ⇒ `Identity` plus rare.
*
* ⚠️⚠️ **CORRIGÉ DE 2.0 À 1.5 LE 2026-07-28, ET CETTE CONSTANTE ÉTAIT LE TERME DOMINANT DE
* TOUTE LA FONCTION PENDANT TROIS BUILDS.** À lire avant d'y retoucher.
* ⚠️⚠️ **CETTE CONSTANTE EST LE TERME DOMINANT DE TOUTE LA FONCTION.** À lire avant d'y
* retoucher.
*
* La dilatation vaut `CaveWarpStrength · VOXEL_NOISE_SCALE · CETTE BORNE`. Avec les défauts
* (`CaveWarpStrength = 8`, `SCALE = 1.25`) elle valait **20 voxels** — appliquée des deux
* côtés de chaque axe d'une tuile de **10 voxels**, soit une boîte de requête de 50 voxels,
* **125× le volume de la tuile**. Trois passes de resserrement (le ver, les colonnes,
* l'échantillonneur, la disjonction des tunnels) ont été faites AUTOUR de ce terme sans que
* personne ne le mesure. Le test des tunnels, annoncé « un ordre de grandeur plus serré », ne
* gagnait en pratique que 25 % — exactement parce que `BoxHalfDiag` était dominé par cette
* dilatation et non par la géométrie.
* (`CaveWarpStrength = 8`, `SCALE = 1.25`), une borne de 2.0 vaut **20 voxels** — appliquée
* des deux côtés de chaque axe d'une tuile de **10 voxels**, soit une boîte de requête de
* 50 voxels, **125× le volume de la tuile** : `BoxHalfDiag` est alors dominé par cette
* dilatation et non par la géométrie, et tout resserrement ailleurs ne gagne presque rien.
*
* ⚠️ ET LE RESTE DU PLUGIN N'A JAMAIS ÉTÉ AUSSI PRUDENT : `BuildChunkCache` est appelée avec
* ⚠️ LE RESTE DU PLUGIN EST MOINS PRUDENT : `BuildChunkCache` est appelée avec
* `Expansion = CaveWarpStrength + 2` (ici comme dans `GetDensityWithParams`), ce qui suppose
* `|Perlin3D| · SCALE ≤ CaveWarpStrength`, donc `|Perlin3D| ≤ 0.8`. Le code qui tourne en
* production depuis toujours parie déjà là-dessus. Prendre 2.0 était 2,5× plus conservateur
* que l'hypothèse dont dépend déjà la correction du cache.
* `|Perlin3D| · SCALE ≤ CaveWarpStrength`, donc `|Perlin3D| ≤ 0.8`. Le code de production
* parie déjà là-dessus. 2.0 serait 2,5× plus conservateur que l'hypothèse dont dépend déjà la
* correction du cache.
*
* LA BORNE 1.5, DÉMONTRÉE (et non observée) :
* 1. `GradDot` rend `±u ± v` où `u` et `v` sont deux composantes **distinctes** de l'offset
@@ -122,11 +109,10 @@ namespace
* est la borne qui se démontre sans analyse de cas sur les hash. `√3/2 ≈ 0.87`, la borne
* classique de Perlin 3D, dépend du jeu de gradients : on ne s'appuie pas dessus.)
*
* Was 2.0, and that constant was the dominant term of this whole function for three builds:
* it inflated a 10-voxel tile into a 50-voxel query box (125x the volume), which is why the
* "order of magnitude tighter" tunnel test only won 25%. The rest of the plugin has always
* assumed |Perlin3D| <= 0.8 (BuildChunkCache's Expansion = CaveWarpStrength + 2). 1.5 is
* PROVED above from GradDot's two-distinct-axes form and the per-axis weighted bound of 0.5.
* This bound is the dominant term of the whole function: 2.0 would inflate a 10-voxel tile
* into a 50-voxel query box (125x the volume). The rest of the plugin already assumes
* |Perlin3D| <= 0.8 (BuildChunkCache's Expansion = CaveWarpStrength + 2). 1.5 is PROVED
* above from GradDot's two-distinct-axes form and the per-axis weighted bound of 0.5.
*/
static constexpr float VF_PerlinAbsBound = 1.5f;
@@ -356,38 +342,11 @@ namespace
// ⚠️ CE QUE ÇA COÛTE, dit franchement : l'original teste UNE fois et saute les douze ; la pile
// teste douze fois. Douze comparaisons flottantes parfaitement prédites par voxel de roc profond
// — mesurable, mais c'est exactement le genre de chose que `AUDIT §C10` dit de MESURER avant
// d'optimiser. Noté dans OPSTACK-PROGRESS comme poste de perf, pas « corrigé » à l'aveugle.
// d'optimiser. C'est un poste de perf connu, pas à « corriger » à l'aveugle.
//
// STAGE B5 DECISION: repeated early-out in each op, NOT a scoping container — the stack is a flat
// list that ClassifyBox folds op by op, and an op that only exists inside a container is not
// composable. Cost stated honestly: twelve predictable compares instead of one branch.
/** Distance d'un point à un SEGMENT (pas à une droite). Écrite ici plutôt que prise dans
* `FMath` : cinq lignes, aucune ambiguïté d'API, et elle sert une borne de correction — le
* genre d'endroit où « je crois que cette fonction fait ça » n'est pas suffisant. */
FORCEINLINE float VF_DistPointSegment(const FVector& P, const FVector& A, const FVector& B)
{
const FVector AB = B - A;
const double LenSq = FVector::DotProduct(AB, AB);
const double T = (LenSq > KINDA_SMALL_NUMBER)
? FMath::Clamp(FVector::DotProduct(P - A, AB) / LenSq, 0.0, 1.0)
: 0.0;
return (float)FVector::Dist(P, A + AB * T);
}
/** La même chose en 2D, pour les connecteurs de puits : ce sont des capsules HORIZONTALES, donc
* Z se teste exactement et seul XY demande une distance point-segment. */
FORCEINLINE float VF_DistPointSegment2D(const FVector2D& P, const FVector2D& A, const FVector2D& B)
{
const FVector2D AB = B - A;
const double LenSq = (double)AB.X * AB.X + (double)AB.Y * AB.Y;
const double T = (LenSq > KINDA_SMALL_NUMBER)
? FMath::Clamp(((double)(P.X - A.X) * AB.X + (double)(P.Y - A.Y) * AB.Y) / LenSq, 0.0, 1.0)
: 0.0;
const double DX = (double)P.X - ((double)A.X + AB.X * T);
const double DY = (double)P.Y - ((double)A.Y + AB.Y * T);
return (float)FMath::Sqrt(DX * DX + DY * DY);
}
FORCEINLINE bool VF_NearCaveSurface(float Sdf, float SDFBlendRadius)
{
// Transcrit tel quel, ordre des comparaisons compris :
@@ -705,7 +664,7 @@ namespace
, CorridorRadius(FMath::Max(P.CorridorRadius, 0.5f))
, BranchProbability(P.BranchProbability)
, Verticality(P.Verticality)
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze' — identique à GetMazeDensity
, Salt((uint32)Seed ^ 0x4D617A65u) // 'Maze'
{}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
@@ -795,18 +754,8 @@ namespace
return FVector((X + 0.5f) * CellSize, (Y + 0.5f) * CellSize, (Z + 0.5f) * CellSize);
}
// Sur-approximation volontaire : boîte englobante du segment contre la boîte élargie.
// Un test capsule/AABB exact serait plus serré ; il coûterait plus cher pour un gain nul
// ici, car la réponse ne sert qu'à un rejet grossier par tuile.
static bool SegmentHitsBox(const FVector& A, const FVector& B, const FVector& Min, const FVector& Max)
{
return FMath::Min(A.X, B.X) <= Max.X && FMath::Max(A.X, B.X) >= Min.X
&& FMath::Min(A.Y, B.Y) <= Max.Y && FMath::Max(A.Y, B.Y) >= Min.Y
&& FMath::Min(A.Z, B.Z) <= Max.Z && FMath::Max(A.Z, B.Z) >= Min.Z;
}
/**
* Le cache par CELLULE, partagé avec le même contrat que GetMazeDensity. Il est `thread_local` et non
* Le cache par CELLULE. Il est `thread_local` et non
* membre parce que la pile est PARTAGÉE entre workers en lecture — un membre mutable serait
* une course. La reconstruction seule inspecte le petit voisinage ; Eval ne refait aucune
* décision de parent ou de boucle.
@@ -870,16 +819,13 @@ namespace
//=========================================================================
// RÔLE 1 — SOURCE : DALLE / SLAB VOID (FlatPlain ET CrystalChamber)
//=========================================================================
// Transcription littérale des ÉTAPES 1-3 de `GetSlabDensity` : surface de sol, surface de
// plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
// Surface de sol, surface de plafond, puis `Density = -min(distAuSol, distAuPlafond)`.
//
// DEUX archétypes, UN opérateur. `GetSlabDensity` est appelé pour FlatPlain et
// CrystalChamber sans le moindre branchement sur le type — CrystalChamber n'est rien d'autre
// que FlatPlain avec un `CeilingRoughness` plus grand. C'est le premier vrai gain du refactor
// (OPSTACK-PLAN §4) : deux des huit archétypes disparaissent dans un seul opérateur, et la
// différence entre eux redevient ce qu'elle a toujours été — un jeu de valeurs par défaut.
// DEUX archétypes, UN opérateur, sans le moindre branchement sur le type — CrystalChamber
// n'est rien d'autre que FlatPlain avec un `CeilingRoughness` plus grand (OPSTACK-PLAN §4) :
// la différence entre eux est un jeu de valeurs par défaut.
//
// Two archetypes, ONE op: GetSlabDensity is called for both with no branch on the type.
// Two archetypes, ONE op, with no branch on the type.
// CrystalChamber IS FlatPlain with a bigger CeilingRoughness.
//
// XY-PUR depuis §3.1 (le terme en Z des deux bruits est parti). C'est ce qui rend
@@ -911,25 +857,22 @@ namespace
bool IsAdditive() const override { return false; }
void PrepareChunk(const FVoxelOpContext&) override {}
// ⚠️⚠️ CORRIGÉ 2026-07-27 : c'était `true`, ET C'ÉTAIT FAUX.
// ⚠️⚠️ `false`, ET NE PAS LE PASSER À `true`.
//
// Le contrat de `IsXYPure` est « **`Eval`** ne dépend pas de Z » — pas « les surfaces ne
// dépendent pas de Z ». Or `Eval` calcule `min(Z - sol, plafond - Z)` : il dépend de Z de
// la façon la plus directe qui soit. §3.1 a rendu les SURFACES pures en XY ; la DENSITÉ,
// elle, ne l'a jamais été et ne peut pas l'être — c'est une distance à une surface.
//
// Latent seulement parce que personne ne lit encore ce drapeau. Le jour où le cache de
// colonnes T1.a devient générique (l'étape suivante), un `true` ici ferait partager UNE
// valeur de densité sur TOUTE la pile verticale de chunks — un monde silencieusement faux,
// que `ValidateDeterminism` ne verrait pas parce qu'il échantillonne le long d'un bord X.
// C'est exactement le piège que l'avertissement de `VoxelDensityOp.h` décrit, et je suis
// tombé dedans en écrivant l'opérateur qui le cite.
// Le jour où un cache de colonnes générique lit ce drapeau, un `true` ici ferait partager
// UNE valeur de densité sur TOUTE la pile verticale de chunks — un monde silencieusement
// faux, que `ValidateDeterminism` ne verrait pas parce qu'il échantillonne le long d'un
// bord X. C'est exactement le piège que l'avertissement de `VoxelDensityOp.h` décrit.
//
// ⚠️ FIXED: this said `true` and was WRONG. The contract is "**Eval** does not depend on Z",
// and Eval computes min(Z - floor, ceil - Z). §3.1 made the SURFACES XY-pure; the DENSITY
// never was and cannot be — it is a distance to a surface. Latent only because nothing reads
// the flag yet; a generic T1.a column cache would have shared one density down the whole
// vertical chunk stack.
// ⚠️ Must stay `false`. The contract is "**Eval** does not depend on Z", and Eval computes
// min(Z - floor, ceil - Z). §3.1 made the SURFACES XY-pure; the DENSITY never was and
// cannot be — it is a distance to a surface. A generic column cache reading `true` here
// would share one density down the whole vertical chunk stack.
//
// C'est précisément cette distinction qui justifie l'espace-hauteur (`VoxelHeightOp.h`) :
// ce qui est pur en XY, ce sont les HAUTEURS, et elles y sont dans un type qui n'a pas de Z.
@@ -993,8 +936,8 @@ namespace
}
private:
// The surface coordinates stay in float; the old FVector round-trip only preserved
// legacy rounding and is no longer part of the output contract.
// The surface coordinates stay in float; an FVector (double) round-trip is not part of
// the output contract.
float SurfaceFloor(float WorldX, float WorldY) const
{
if (FloorRoughness <= 0.0f) { return FloorZ; }
@@ -1037,7 +980,7 @@ namespace
// RÔLE 1 — SOURCE : COLONNE DE SURFACE / SURFACE COLUMN (SurfaceWorld)
//=========================================================================
// Le pont entre les deux espaces : consomme DEUX piles de hauteur (sol et voûte) et en fait une
// densité. C'est tout le combine de `SurfaceDensityFromColumn` :
// densité :
//
// Density = TerrainZ - Z ← solide sous le sol
// Density = max(Density, Z - CeilSurf) ← solide au-dessus de la voûte
@@ -1120,12 +1063,6 @@ namespace
}
}
/** Sans biomes — délègue, pour qu'il n'existe qu'UN corps de construction et UN compteur
* d'identité. Deux constructeurs qui s'initialisent chacun de leur côté, c'est deux
* endroits où oublier un membre. */
FSurfaceColumnSource(const FSurfaceGenerationParams& InP, int32 Seed)
: FSurfaceColumnSource(InP, Seed, TArray<FSurfaceGenerationParams>(), nullptr) {}
void BuildSingleBiome(const FSurfaceGenerationParams& InP, int32 Seed)
{
// Construite à la main (pas via BuildSurfaceHeightStack) pour GARDER le pointeur vers la
@@ -1342,8 +1279,6 @@ namespace
return S.Height;
}
const FSurfaceGenerationParams& GetParams() const { return P; }
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
@@ -1352,29 +1287,29 @@ namespace
bool IsXYPure() const override { return false; } // voir le bloc ci-dessus
/**
* ⚠️ C'EST ICI QUE SE JOUE LA PERF DE CET ARCHÉTYPE — corrigé 2026-07-27 après mesure.
* ⚠️ C'EST ICI QUE SE JOUE LA PERF DE CET ARCHÉTYPE.
*
* Le mémo de colonne était clé sur `InstanceId`, qui change à CHAQUE reconstruction de pile,
* 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 ». 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.
* Ne PAS clé ce mémo sur `InstanceId` : il change à CHAQUE reconstruction de pile,
* c'est-à-dire à chaque chunk, et une strate haute de 4 chunks recalculerait ses colonnes
* **4 fois**, resamples du cliff compris. `GSurfColCache` est clé sur
* `(boîte XY, StrateKey, Seed, LayoutVersion)` **SANS ChunkZ**, 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 un LRU spatial de six boîtes.
*
* 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.
* `thread_local`, il SURVIT à la reconstruction de la pile — seule la clé l'invalide.
*
* 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é
* XY-pur — « ZERO Z dependence: the climate/Voronoi fields are pure-XY ». C'est exactement
* la justification sur laquelle `GSurfColCache` repose déjà.
*
* 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 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
* Do not key the memo on InstanceId: it changes every chunk, so a 4-chunk strate would
* recompute every column 4x. GSurfColCache deliberately omits ChunkZ and shares down the whole
* vertical stack; this 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
@@ -1616,13 +1551,13 @@ namespace
//=========================================================================
// RÔLE 3 — MODIFIER : COLONNES SUR GRILLE MONDE / WORLD-GRID COLUMNS
//=========================================================================
// ÉTAPE 4 de `GetSlabDensity`. Des cylindres de hauteur infinie posés sur une grille de
// Des cylindres de hauteur infinie posés sur une grille de
// `ColumnSpacing`, un tirage d'existence et un jitter par cellule. Le champ de vide décide déjà
// où est le solide, donc la colonne n'a qu'à AJOUTER de la densité le long de son XY — elle
// n'est visible que là où le vide avait creusé autour d'elle.
//
// Le cache 3×3 par cellule est repris tel quel (il était déjà `thread_local` dans l'original,
// et c'est exactement ce que la note de threading de VoxelDensityOp.h autorise). Sa clé
// Le cache 3×3 par cellule est `thread_local`, ce que la note de threading de VoxelDensityOp.h
// autorise. Sa clé
// contient tous les paramètres qui influent sur le résultat + le seed, donc un changement de
// layout qui change un param invalide bien ; un changement qui n'en touche aucun produirait
// des colonnes identiques (cf. AUDIT C2 — la clé est complète, pas seulement le coord).
@@ -1709,7 +1644,7 @@ namespace
private:
struct FSlabColumn { float X, Y, R; };
static constexpr float ColBlend = 2.0f; // identique à GetSlabDensity
static constexpr float ColBlend = 2.0f;
/** Le tirage d'une cellule : existence, jitter, rayon. Fonction PURE de (cellule, seed,
* params) — donc `Eval` et `EffectOverBox` voient forcément la même colonne. */
@@ -1766,8 +1701,7 @@ namespace
// SEUL changement est `Density += Fill·Base·2` au lieu de `Density -= Carve·Base·2`.
//
// Un archétype qui CREUSE dans du roc et un archétype qui REMPLIT du vide sont donc le même
// opérateur au signe près, exactement comme la source constante au-dessus. C'est la symétrie
// que le `switch` ne pouvait pas montrer : les deux blocs y sont à 900 lignes l'un de l'autre.
// opérateur au signe près, exactement comme la source constante au-dessus.
//
// ⚠️ `Sign` vaut ±1.0f et rien d'autre. La multiplication par ±1 est EXACTE en IEEE-754, donc
// `D += (-1·F)·B·2` rend bit pour bit ce que `D -= F·B·2` rendait — l'égalité binaire des trois
@@ -2018,8 +1952,7 @@ namespace
*
* (Le `ClassifyTile` actuel n'a pas cette marge — il exclut simplement ces z du test de
* colonne. La fenêtre est infime et demande que l'archétype produise de l'air pile à ce z,
* mais elle est réelle ; notée plutôt que corrigée en douce, puisque le chemin d'aujourd'hui
* n'est pas touché par cette Phase 1.)
* mais elle est réelle.)
*/
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext&) const override
{
@@ -2320,7 +2253,7 @@ namespace
{
public:
FShaftFieldSource(const FVerticalShaftParams& InP, int32 Seed, float InSpineRadius)
: P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft' — identique à GetVerticalShaftDensity
: P(InP), Salt((uint32)Seed ^ 0x53686674u) // 'Shft'
, SpineRadius(InSpineRadius) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
@@ -2480,9 +2413,9 @@ namespace
}
/** Le voisinage inner 3×3 + ses connecteurs, mémoïsés par worker. Le rebuild collecte un
* halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback fixe ±3. Clé = cellule + tous les params
* qui influent (comme l'original) : stable à travers les reconstructions de pile, ce qui
* est la leçon retenue du mémo de colonne de SurfaceWorld. */
* halo 9×9 émetteur / 15×15 total pour résoudre les fenêtres parent 5×5 et le fallback
* fixe ±3. Clé = cellule + tous les params qui influent : stable à travers les
* reconstructions de pile, ce qui est la leçon retenue du mémo de colonne de SurfaceWorld. */
const FCells& GetCells(float WorldX, float WorldY) const
{
const float Spacing = FMath::Max(P.ShaftSpacing, 1.0f);
@@ -2776,9 +2709,9 @@ namespace
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
{
if (P.LedgeSpacing <= 0.0f || P.LedgeDepth <= 0.0f || Field == nullptr) { return; }
// ⚠️ La garde de l'original est `CaveSDF < 0` — le SDF APRÈS rugosité, tel que la pile
// l'a laissé. C'est bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait
// le SDF SANS rugosité et déplacerait les étagères.
// ⚠️ La garde est `Sdf < 0` — le SDF APRÈS rugosité, tel que la pile l'a laissé. C'est
// bien `InOut.Sdf` ici, pas une re-évaluation : re-calculer donnerait le SDF SANS
// rugosité et déplacerait les étagères.
if (InOut.Sdf >= 0.0f) { return; }
const float Phase = FMath::Frac((WorldZ - P.StrateBottomWorldZ) / P.LedgeSpacing);
@@ -2851,27 +2784,6 @@ namespace
{
if (Field == nullptr) return;
const FShaftFieldSource::FCells& Cells = Field->GetCellsAt(WorldX, WorldY);
static thread_local bool bLoggedTemporaryShaftConnectors = false;
if (!bLoggedTemporaryShaftConnectors
&& FMath::IsNearlyEqual(WorldZ, -700.0f)
&& WorldX > -180.0f && WorldX < -170.0f
&& WorldY > -48.0f && WorldY < -38.0f)
{
bLoggedTemporaryShaftConnectors = true;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeExplore][TemporaryShaftConnectors] query=(%.3f,%.3f,%.3f) count=%d"),
WorldX, WorldY, WorldZ, Cells.Conns.Num());
for (const FShaftFieldSource::FConn& DebugConn : Cells.Conns)
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeExplore][TemporaryShaftConnectors] A=(%.3f,%.3f,%.3f) "
"B=(%.3f,%.3f,%.3f) radius=%.3f floor=%.3f walk=%d"),
DebugConn.A.X, DebugConn.A.Y, DebugConn.A.Z,
DebugConn.B.X, DebugConn.B.Y, DebugConn.B.Z,
DebugConn.Radius, DebugConn.FloorZ,
DebugConn.bWalkableFloor ? 1 : 0);
}
}
for (const FShaftFieldSource::FConn& Conn : Cells.Conns)
{
if (!Conn.bWalkableFloor) continue;
@@ -2945,26 +2857,22 @@ namespace
//
// ⚠️ ÉCART ASSUMÉ AVEC `OPSTACK-DECOMPOSITION §7`, qui décrivait un `FRAME IslandWarp` enveloppant
// la source. Le warp reste À L'INTÉRIEUR de l'opérateur, et c'est délibéré : `§7` compte trois
// usages de frames (îles, caves de TunnelNetwork, tunnels), mais **deux d'entre eux ne sont pas
// encore portés**. Inventer l'infrastructure de frame pour son unique utilisateur actuel, c'est
// la concevoir contre un seul exemple — précisément ce que ce refactor a évité jusqu'ici en
// n'abstrayant qu'à la deuxième occurrence (cf. `IVoxelBiomeField`, né d'un besoin réel).
// ✅ RÉPONDU (2026-08-16) : TunnelNetwork EST porté, et le deuxième usage réel a dissous la
// question au lieu de la trancher. Voir `BuildTunnelNetworkStack` : `CaveWarp` n'enveloppe
// qu'UN opérateur (donc c'est une variable locale, pas un frame) et `VerticalScale` est une
// fonction pure d'un scalaire. **Zéro frame sur trois candidats.** Ne pas rouvrir : le warp
// reste local ICI pour la même raison qu'il est resté local là-bas.
// usages de frames (îles, caves de TunnelNetwork, tunnels), et aucun n'en a besoin. Voir
// `BuildTunnelNetworkStack` : `CaveWarp` n'enveloppe qu'UN opérateur (donc c'est une variable
// locale, pas un frame) et `VerticalScale` est une fonction pure d'un scalaire. **Zéro frame
// sur trois candidats.** Ne pas rouvrir : le warp reste local ICI pour la même raison qu'il
// est resté local là-bas. On n'abstrait qu'à la deuxième occurrence réelle (cf.
// `IVoxelBiomeField`, né d'un besoin réel).
//
// The warp stays INSIDE the op against §7's FRAME suggestion. ✅ ANSWERED 2026-08-16: this said
// "revisit when TunnelNetwork brings the second real use" — TunnelNetwork is ported, and the
// second use dissolved the question rather than settling it. See BuildTunnelNetworkStack:
// The warp stays INSIDE the op against §7's FRAME suggestion. TunnelNetwork is ported, and the
// second real use dissolved the question rather than settling it. See BuildTunnelNetworkStack:
// CaveWarp wraps exactly ONE operator (a local variable, not a frame) and VerticalScale is a
// pure function of a scalar. ZERO frames out of three candidates. Do not reopen.
class FIslandBlobSource final : public IVoxelDensityOp
{
public:
FIslandBlobSource(const FFloatingIslandParams& InP, int32 Seed)
: P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld' — identique à GetFloatingIslandDensity
: P(InP), Salt((uint32)Seed ^ 0x49736C64u) // 'Isld'
{}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
@@ -3040,8 +2948,8 @@ namespace
* • en Z, `Sdf ≥ WorldZ − TopSurf ≥ WorldZ − TopZ`, donc au-dessus du sommet + marge il
* n'y a plus rien à faire. **En dessous, il n'y a PAS de borne** : sous une île, le SDF
* vaut ≈ `DistXY` à toute profondeur, donc un mince fil de matière descend le long de
* l'axe. C'est le comportement de l'original ; le confondre avec « rien en dessous »
* serait un TROU, et c'est pourquoi seule la borne HAUTE est testée.
* l'axe. Le confondre avec « rien en dessous » serait un TROU, et c'est pourquoi seule
* la borne HAUTE est testée.
* • cette source ne couvre pas l'aval : elle publie seulement son propre intervalle SDF.
* La rugosité, le blend du fill et le creux du SmoothMin sont bornés par leurs propres
* opérateurs dans le pliage, chacun avec son contrat isolé.
@@ -3137,8 +3045,7 @@ namespace
private:
/** Tirage d'une cellule. PURE en (cellule, seed, params) ⇒ `Eval` et `EffectOverBox` ne
* peuvent pas voir des îles différentes. Transcription littérale du bloc de cuisson de
* `GetFloatingIslandDensity`. */
* peuvent pas voir des îles différentes. */
bool RollIsland(int32 nx, int32 ny, FIsland& Out) const
{
const float H = P.StrateTopWorldZ - P.StrateBottomWorldZ;
@@ -3172,17 +3079,15 @@ namespace
}
/**
* Le voisinage 3×3, mémoïsé par worker — la même cuisson `thread_local` que l'original.
* Le voisinage 3×3, mémoïsé par worker (`thread_local`).
*
* ⚠️ LA CLÉ INCLUT `BoundarySealThickness`, QUE L'ORIGINAL OMET. `SpreadZ` s'en sert
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc dans `GetFloatingIslandDensity` une
* édition à chaud qui ne change QUE l'épaisseur de seal sert des îles périmées. Même famille
* que `AUDIT §C2` et que la régression d'overhang du 2026-07-27 : une clé de cache
* incomplète ne se voit pas, elle produit du terrain plausible. Ajouter le champ ne coûte
* qu'un recalcul, jamais une valeur différente — donc l'égalité binaire tient.
* ⚠️ LA CLÉ INCLUT `BoundarySealThickness` : `SpreadZ` s'en sert
* (`H·0.5 − max(TopHalf, UnderDepth) − Seal`), donc sans lui une édition à chaud qui ne
* change QUE l'épaisseur de seal servirait des îles périmées. Même famille que `AUDIT §C2` :
* une clé de cache incomplète ne se voit pas, elle produit du terrain plausible.
*
* The key includes BoundarySealThickness, which the original omits although SpreadZ reads it.
* Adding it can only cost a recompute, never change a value — bit-equality is unaffected.
* The key includes BoundarySealThickness because SpreadZ reads it; without it a live edit
* of the seal thickness alone would serve stale islands.
*/
const FCells& GetCells(float WorldX, float WorldY) const
{
@@ -4019,10 +3924,9 @@ namespace
// clé « égalité de chunk » se retournait à chaque cellule de bord et reconstruisait le
// cache (coûteux) en boucle. Comme le cache couvre la boîte + MaxInfluence, toute requête
// DANS la boîte est correcte. Ne pas « simplifier » en clé de chunk.
// (Les champs de cache vivent maintenant dans `FState`, au-dessus, pour que les
// modificateurs de détail de l'étape B puissent les lire. Même durée de vie, même
// partage entre instances qu'avant : ce sont les mêmes `thread_local`, déménagés.)
// ⚠️ AJOUTÉ PAR RAPPORT À L'ORIGINAL — la leçon du 2026-07-27 (régression d'overhang).
// (Les champs de cache vivent dans `FState`, au-dessus, pour que les modificateurs de
// détail puissent les lire ; ce sont des `thread_local` partagés entre instances.)
// ⚠️ LA CLÉ INCLUT LES PARAMS, CONTRAIREMENT À L'ORIGINAL.
// L'original ne clé QUE sur (boîte, strate, seed) : deux jeux de params différents dans
// la MÊME strate au MÊME seed se servent mutuellement leur cache. En production
// `RebuildStrates` masque le trou en bougeant la strate ; en test, deux piles construites
@@ -4313,7 +4217,6 @@ namespace
// room's own volume.
InOut.bHasTunnelCoreWorldEvaluation = true;
InOut.TunnelCoreWorldSDF = S.LastTunnelCoreWorldEvaluation.SDF;
InOut.bTunnelCoreSupportFloor = false;
InOut.bTunnelCoreRoomFloor =
S.LastTunnelCoreWorldEvaluation.bRoomFloor;
InOut.bHasTunnelCoreSweptFloor =
@@ -4322,8 +4225,6 @@ namespace
S.LastTunnelCoreWorldEvaluation.SweptFloorZ;
InOut.TunnelCoreSweptFloorRadius =
S.LastTunnelCoreWorldEvaluation.SweptFloorRadius;
InOut.bProtectedStructuralFloor = false;
InOut.StructuralFloorMinimumDensity = 0.0f;
}
// These cache hand-offs remain valid when tunnel core is off; detail operators still
@@ -4445,7 +4346,6 @@ namespace
bool bMayHaveTunnelCoreTail = false;
uint32 ExactTailQueries = 0;
uint32 ExactTailEvaluated = 0;
uint32 ExactTailCacheHits = 0;
uint64 ExactTailCycles = 0;
uint64 PropagateCycles = 0;
uint64 ExactPrimitiveCycles = 0;
@@ -4534,12 +4434,6 @@ namespace
return false;
}
bool HasRoomColumnsForLastBox() const
{
const FBoxState& B = BoxState();
return !B.bValid || GetBoxCache().Columns.Num() > 0;
}
float RoomColumnFillSupremumForLastBox() const
{
const FBoxState& B = BoxState();
@@ -5623,18 +5517,6 @@ namespace
}
};
auto EffectiveZRangeToWorld = [&](float MinEffectiveZ,
float MaxEffectiveZ) -> FVector2D
{
if (P.VerticalScale > 0.0f && P.VerticalScale != 1.0f)
{
const float WorldA = MinEffectiveZ * P.VerticalScale;
const float WorldB = MaxEffectiveZ * P.VerticalScale;
return FVector2D(FMath::Min(WorldA, WorldB), FMath::Max(WorldA, WorldB));
}
return FVector2D(MinEffectiveZ, MaxEffectiveZ);
};
auto DistanceBlockToInfiniteLineLower = [&](const FExactLatticeBlock& Block,
const FVector& A,
const FVector& BPoint) -> float
@@ -8331,7 +8213,7 @@ namespace
// sortie, ce qui est la raison pour laquelle l'ordre de `BuildTunnelNetworkStack` reste celui
// de l'original, ligne pour ligne.
//
// TIER 3b AUDIT (2026-09-04): this op is intentionally still separate. Ten operators lie
// This op is intentionally separate from 4b. Ten operators lie
// between 4b and this phase (4c through 4h), and every one reads+writes Density. Moving this
// phase next to 4b would reorder those density updates, so a bit-identical fuse is not legal.
class FFloorBiasMod final : public IVoxelDensityOp
@@ -9427,8 +9309,8 @@ namespace VoxelDensityOps
TUniquePtr<IVoxelDensityOp> MakeConstantVoidSource(float BaseDensity)
{
// `float Density = -Params.BaseDensity; // start as open air (void)` — la négation unaire
// est exacte, donc c'est littéralement la première ligne de GetFloatingIslandDensity.
// `Density = -BaseDensity` : on part du vide. La négation unaire est exacte.
// Start as open air; unary negation is exact.
return MakeUnique<FConstantFieldSource>(-BaseDensity);
}
@@ -9463,18 +9345,13 @@ namespace VoxelDensityOps
return MakeUnique<FGridColumnMod>(P, Seed);
}
TUniquePtr<IVoxelDensityOp> MakeSurfaceColumnSource(const FSurfaceGenerationParams& P, int32 Seed)
{
return MakeUnique<FSurfaceColumnSource>(P, Seed);
}
void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P,
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
const TArray<FSurfaceGenerationParams>& PerBiomeParams,
TUniquePtr<IVoxelBiomeField> BiomeField,
bool bAppendStructuralPosts)
{
// ARCHÉTYPE COMPLET depuis l'étape 2c : vide + overhang + mélange de biomes.
// ARCHÉTYPE COMPLET : vide + overhang + mélange de biomes.
// `PerBiomeParams` vide ⇒ chemin sans biomes, strictement inchangé.
TUniquePtr<FSurfaceColumnSource> ColumnSource =
MakeUnique<FSurfaceColumnSource>(P, Seed, PerBiomeParams, MoveTemp(BiomeField));
@@ -9497,10 +9374,8 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// DEUX archétypes entrent ici, aucun branchement ne les distingue — parce que
// `GetSlabDensity` n'en fait aucun non plus. FlatPlain et CrystalChamber ne diffèrent que
// par leurs valeurs par défaut, et c'est maintenant visible dans le code plutôt que dans
// un commentaire. 8 archétypes → 7.
// DEUX archétypes entrent ici, aucun branchement ne les distingue : FlatPlain et
// CrystalChamber ne diffèrent que par leurs valeurs par défaut.
OutStack.Add(MakeSlabVoidSource(P, Seed));
OutStack.Add(MakeGridColumnMod(P, Seed));
@@ -9516,16 +9391,10 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// ⚠️ LA PREUVE QUE L'ABSTRACTION EST RÉELLE, et elle vaut d'être dite : TROIS des cinq
// opérateurs ci-dessous sont ceux de Maze, **repris sans une ligne de changement** —
// `ConstantRock`, `SdfRoughness`, `SdfCarve`. Dans le `switch`, Maze et VerticalShafts sont
// deux fonctions de ~100 lignes qui n'ont rien en commun à l'œil ; en opérateurs, ce sont
// les MÊMES trois ops avec une source différente. C'est exactement ce que `§2.5` prédisait
// et ce que la Phase 1 avait parié.
// TROIS des cinq opérateurs ci-dessous sont ceux de Maze, repris tels quels —
// `ConstantRock`, `SdfRoughness`, `SdfCarve` — avec une source différente (`§2.5`).
//
// THREE of the five ops below are Maze's, reused without a line changed. In the switch,
// Maze and VerticalShafts are two unrelated ~100-line functions; as operators they are the
// same three ops with a different source.
// THREE of the five ops below are Maze's, reused unchanged, with a different source.
constexpr float CarveBlend = 2.0f;
TUniquePtr<FShaftFieldSource> ShaftSource =
@@ -9534,9 +9403,8 @@ namespace VoxelDensityOps
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
OutStack.Add(MoveTemp(ShaftSource));
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — les constantes de
// `GetVerticalShaftDensity`, PAS celles de Maze (0.12 / `R + rough + 2`). Même opérateur,
// réglages différents : c'est le point.
// Fréquence 0.1 et fenêtre `SurfaceRoughness + 4` — PAS celles de Maze
// (0.12 / `R + rough + 2`). Même opérateur, réglages différents.
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.1f, 3, P.SurfaceRoughness + 4.0f));
OutStack.Add(MakeSdfCarve(CarveBlend, P.BaseDensity));
OutStack.Add(MakeUnique<FShaftLedgeMod>(P, ShaftPtr));
@@ -9565,20 +9433,6 @@ namespace VoxelDensityOps
// plus proche appliqué ; les onze modificateurs concernés y lisent leurs champs au lieu des
// leurs. La rugosité (4b) NON — dans l'original elle précède la déclaration du shadow.
//
// ⚠️ CE PARAGRAPHE ÉTAIT PÉRIMÉ ET DISAIT LE CONTRAIRE DU CODE (corrigé 2026-08-16).
// Il annonçait « étape A sur trois, les douze modificateurs et l'override par salle ne sont
// pas encore portés, c'est pour ça que `UsesOperatorStackForChunk` rend **false** pour
// TunnelNetwork ». Les trois étapes sont terminées : les douze modificateurs sont ajoutés
// douze lignes plus bas, l'override C1 est en place, et `UsesOperatorStackForChunk` rend
// **true** pour TunnelNetwork. Un commentaire qui contredit le code sous lui est
// exactement le piège « lire le code, pas le commentaire » à l'envers.
//
// STALE PARAGRAPH REMOVED 2026-08-16. It claimed "stage A of three, the detail modifiers and
// the per-room override are not ported yet, which is why the archetype is still off in
// UsesOperatorStackForChunk". All three stages are done, the twelve modifiers are added a
// dozen lines below, and that function returns TRUE for TunnelNetwork. A comment that
// contradicts the code beneath it is the "read the code, not the comment" trap in reverse.
//
//---------------------------------------------------------------------
// ⚠️ CE PORTAGE RETIRE L'IDÉE DE « FRAME OPS » (OPSTACK-DECOMPOSITION §1)
//---------------------------------------------------------------------
@@ -9592,7 +9446,7 @@ namespace VoxelDensityOps
// qu'ajouter un canal pour éviter une division.
// Il restait le warp d'îles (§7), déjà gardé local pour la même raison. **Zéro frame sur
// trois candidats** : ce n'était pas une infrastructure manquante, c'était trois fois la
// même chose vue de loin. Noté ici plutôt que laissé en TODO permanent.
// même chose vue de loin.
constexpr float CarveMinDivisor = 1.0f; // TunnelNetwork plancher son diviseur, cf. FSdfConvertOp
// La source de salles est retenue par pointeur non possédant : les terrasses re-interrogent
@@ -9665,8 +9519,8 @@ namespace VoxelDensityOps
OutStack.Add(MakeConstantVoidSource(P.BaseDensity));
OutStack.Add(MakeUnique<FIslandBlobSource>(P, Seed));
// Fréquence 0.08 et 4 octaves — les constantes de `GetFloatingIslandDensity`. Quatrième
// archétype à réutiliser cet opérateur (Maze 0.12/3, VerticalShafts 0.1/3).
// Fréquence 0.08 et 4 octaves — les constantes de FloatingIslands (Maze 0.12/3,
// VerticalShafts 0.1/3).
OutStack.Add(MakeSdfRoughnessMod(P.SurfaceRoughness, 0.08f, 4,
P.SurfaceRoughness + BlendK + 2.0f));
OutStack.Add(MakeSdfFill(BlendK, P.BaseDensity));
@@ -9683,15 +9537,14 @@ namespace VoxelDensityOps
int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager,
bool bAppendStructuralPosts)
{
// Les constantes viennent telles quelles de GetMazeDensity — elles y étaient codées en dur.
// Constantes Maze, codées en dur. / Maze's hard-coded constants.
constexpr float CarveBlend = 2.0f;
constexpr float RoughFrequency = 0.12f;
constexpr int32 RoughOctaves = 3;
const float R = FMath::Max(P.CorridorRadius, 0.5f);
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f` dans
// l'original. Reproduite à l'identique pour que l'égalité binaire tienne.
// Fenêtre d'application de la rugosité : `MazeSDF < R + SurfaceRoughness + 2.0f`.
const float RoughApplyWithin = R + P.SurfaceRoughness + 2.0f;
OutStack.Add(MakeConstantRockSource(P.BaseDensity));
@@ -218,11 +218,6 @@ namespace VoxelDensityProfile
}
}
void SetEnabled(bool bEnabled)
{
SetMode(bEnabled ? EMode::Sampled : EMode::Disabled, DefaultSampleInterval);
}
void SetMode(EMode Mode, uint32 SampleInterval)
{
if (SampleInterval == 0)
@@ -39,8 +39,8 @@ void FVoxelForgeModule::StartupModule()
RequestedFma3,
Fma3After);
#endif
// These parsers used to be entered lazily by worker-side density/mesher calls. Resolve the
// command line once while the module is starting, before any generation task is submitted.
// Resolve the command line once while the module is starting, before any generation task is
// submitted, instead of lazily from worker-side density/mesher calls.
VoxelGenLOD::InitializeConsoleSwitches();
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
UE_LOG(LogTemp, Log, TEXT("VoxelForge module started!"));
File diff suppressed because it is too large. Load diff
@@ -91,8 +91,7 @@ void UVoxelMarchingCubesMesher::InitializeConsoleSwitches()
//=============================================================================
// MAIN ALGORITHM
//=============================================================================
// (L'ancien trio GetDensity / InterpolateEdge / ComputeGradientNormal a été retiré :
// mort depuis T1.b — la grille pré-échantillonnée fournit positions ET gradients.)
// La grille pré-échantillonnée fournit positions ET gradients.
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, int32 Step, int32 InCellsPerAxis,
TArray<uint8>* OutCaptureGrid,
@@ -141,8 +140,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
//=========================================================================
// Le discriminant est SÉMANTIQUE, pas géométrique (fable-idea F17) : un vertex orienté vers
// le bas est un sky-cap seulement s'il est proche de CeilSurf ; proche de TerrainZ c'est un
// surplomb de terrain (reste "sol" — l'ancien vote par tuile mettait le matériau ciel sous
// les surplombs des tuiles majoritairement plafond). Un futur toit de grotte (aussi down-
// surplomb de terrain (reste "sol" — un vote par tuile mettrait le matériau ciel sous les
// surplombs des tuiles majoritairement plafond). Un futur toit de grotte (aussi down-
// facing, mais SOUS TerrainZ) tombera correctement côté "sol/roche" par la même règle.
// Coût : GetSurfaceHeightAt (pile XY complète) UNIQUEMENT pour les vertex down-facing,
// mémoïsé par colonne quantifiée au pas de la grille → ≤ (colonnes touchées) appels ;
@@ -1126,218 +1125,3 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
return MeshData;
}
//=============================================================================
// F18 — FEUILLE DE CHAMP LOINTAIN (voir le doc de la déclaration dans le .h)
//=============================================================================
// Deux grilles déplacées (sol + cap) au lieu d'un marching cubes 3D. Conventions IDENTIQUES à
// GenerateMesh : positions monde cm, UVs planaires (voxels), masques F6 par GetBiomeMaterialAt,
// jupes double-face extrudées le long de −N par seau, indices sol‖cap + NumCeilingTriangles.
// Winding : le chemin MC émet (0,2,1) des tables Bourke ⇒ une surface orientée +Z s'écrit
// (x,y) → (x,y+1) → (x+1,y) ; le cap (orienté −Z) inverse.
FVoxelMeshData UVoxelMarchingCubesMesher::GenerateSheetMesh(FIntVector OriginVoxels, int32 StepXY,
int32 CellsXY, int32 StrateChunkZ,
int32 HoleMinXVox, int32 HoleMinYVox,
int32 HoleMaxXVox, int32 HoleMaxYVox)
{
FVoxelMeshData MeshData;
if (!Generator) return MeshData;
StepXY = FMath::Max(1, StepXY);
CellsXY = FMath::Clamp(CellsXY, 2, 512);
// TROU XY — cellule [x0, x0+Step]×[y0, y0+Step] entièrement dans le rectangle (Max exclusif)
// ⇒ sautée : la zone est couverte par les coquilles MC (voir VoxelWorld.h SheetHole*).
auto CellInHole = [&](int32 cx, int32 cy) -> bool
{
const int32 X0 = OriginVoxels.X + cx * StepXY;
const int32 Y0 = OriginVoxels.Y + cy * StepXY;
return X0 >= HoleMinXVox && X0 + StepXY <= HoleMaxXVox
&& Y0 >= HoleMinYVox && Y0 + StepXY <= HoleMaxYVox;
};
const int32 GridDim = CellsXY + 1; // points de grille par axe
const int32 MDim = GridDim + 2; // + anneau de marge ±1 (normales continues entre feuilles)
// Early-out : la validité SurfaceWorld ne dépend que de ChunkZ (les strates sont des couches
// horizontales) — une sonde suffit. La sentinelle par colonne plus bas reste par prudence.
{
float T0 = 0.0f, C0 = 0.0f;
if (!Generator->GetSurfaceHeightAt((float)OriginVoxels.X, (float)OriginVoxels.Y,
StrateChunkZ, T0, C0))
{
return MeshData; // pas une strate SurfaceWorld → feuille vide
}
}
// Colonnes (TerrainZ, CeilSurf) en Z-voxel monde absolu, marge incluse. X==FLT_MAX ⇒ invalide.
static thread_local TArray<FVector2f> SheetCols;
SheetCols.SetNumUninitialized(MDim * MDim);
for (int32 gy = -1; gy <= GridDim; ++gy)
{
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 gx = -1; gx <= GridDim; ++gx)
{
float Tz = 0.0f, Cz = 0.0f;
const bool bOk = Generator->GetSurfaceHeightAt(
(float)(OriginVoxels.X + gx * StepXY), (float)(OriginVoxels.Y + gy * StepXY),
StrateChunkZ, Tz, Cz);
SheetCols[(gy + 1) * MDim + (gx + 1)] = bOk ? FVector2f(Tz, Cz)
: FVector2f(FLT_MAX, -FLT_MAX);
}
}
auto Col = [&](int32 gx, int32 gy) -> const FVector2f&
{
return SheetCols[(gy + 1) * MDim + (gx + 1)];
};
// Index de vertex par point de grille (−1 = pas créé), sol et cap séparés — le partage de
// vertex est structurel (grille régulière), pas besoin de map de déduplication.
static thread_local TArray<int32> GroundIdx;
static thread_local TArray<int32> CapIdx;
GroundIdx.Init(-1, GridDim * GridDim);
CapIdx.Init(-1, GridDim * GridDim);
auto MakeVert = [&](int32 gx, int32 gy, bool bCap) -> int32
{
int32& Slot = (bCap ? CapIdx : GroundIdx)[gy * GridDim + gx];
if (Slot >= 0) return Slot;
const FVector2f C = Col(gx, gy);
const float H = bCap ? C.Y : C.X;
// Pente par différences centrales sur la grille de hauteurs (voxels/voxels — l'échelle cm
// se simplifie). Voisin invalide ⇒ composante plate (ne devrait pas arriver : cf. early-out).
auto HAt = [&](int32 x, int32 y) -> float
{
const FVector2f& N = Col(x, y);
return (N.X == FLT_MAX) ? H : (bCap ? N.Y : N.X);
};
const float Sx = (HAt(gx + 1, gy) - HAt(gx - 1, gy)) / (2.0f * (float)StepXY);
const float Sy = (HAt(gx, gy + 1) - HAt(gx, gy - 1)) / (2.0f * (float)StepXY);
// Sol : air au-dessus ⇒ normale vers le haut. Cap : l'air (l'intérieur de la strate) est
// EN-DESSOUS ⇒ normale vers le bas (−∇(z − C) = (Cx, Cy, −1)).
FVector Normal = bCap ? FVector(Sx, Sy, -1.0f) : FVector(-Sx, -Sy, 1.0f);
if (!Normal.Normalize())
{
Normal = FVector(0.0f, 0.0f, bCap ? -1.0f : 1.0f);
}
const float Xv = (float)(OriginVoxels.X + gx * StepXY);
const float Yv = (float)(OriginVoxels.Y + gy * StepXY);
const FVector WorldPos(Xv * VOXEL_SIZE, Yv * VOXEL_SIZE, H * VOXEL_SIZE);
Slot = MeshData.Vertices.Num();
MeshData.Vertices.Add(WorldPos);
MeshData.Normals.Add(Normal);
MeshData.UVs.Add(FVector2D(Xv, Yv)); // == WorldPos.XY / VOXEL_SIZE (convention MC)
// Mêmes masques F6 que GenerateMesh (palette biome dominant/voisin, pente, fondu).
int32 PalD = 0, PalN = 0; float BlendW = 0.0f;
Generator->GetBiomeMaterialAt(Xv, Yv, H, PalD, PalN, BlendW);
const uint8 Rr = (uint8)FMath::Clamp(PalD, 0, 255);
const uint8 Aa = (uint8)FMath::Clamp(PalN, 0, 255);
const uint8 Gg = (uint8)FMath::Clamp(FMath::RoundToInt((1.0f - FMath::Abs((float)Normal.Z)) * 255.0f), 0, 255);
const uint8 Bb = (uint8)FMath::Clamp(FMath::RoundToInt(BlendW * 255.0f), 0, 255);
MeshData.Colors.Add(FColor(Rr, Gg, Bb, Aa));
return Slot;
};
static thread_local TArray<int32> SheetGroundTris;
static thread_local TArray<int32> SheetCapTris;
SheetGroundTris.Reset();
SheetCapTris.Reset();
for (int32 cy = 0; cy < CellsXY; ++cy)
{
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 cx = 0; cx < CellsXY; ++cx)
{
if (CellInHole(cx, cy)) continue; // couverte par les coquilles MC proches
if (Col(cx, cy).X == FLT_MAX || Col(cx + 1, cy).X == FLT_MAX ||
Col(cx, cy + 1).X == FLT_MAX || Col(cx + 1, cy + 1).X == FLT_MAX)
{
continue;
}
// SOL (polygroup 0) — orienté +Z.
const int32 g00 = MakeVert(cx, cy, false);
const int32 g10 = MakeVert(cx + 1, cy, false);
const int32 g01 = MakeVert(cx, cy + 1, false);
const int32 g11 = MakeVert(cx + 1, cy + 1, false);
SheetGroundTris.Add(g00); SheetGroundTris.Add(g01); SheetGroundTris.Add(g10);
SheetGroundTris.Add(g10); SheetGroundTris.Add(g01); SheetGroundTris.Add(g11);
// CAP (polygroup 1) — orienté −Z ⇒ winding inversé.
const int32 c00 = MakeVert(cx, cy, true);
const int32 c10 = MakeVert(cx + 1, cy, true);
const int32 c01 = MakeVert(cx, cy + 1, true);
const int32 c11 = MakeVert(cx + 1, cy + 1, true);
SheetCapTris.Add(c00); SheetCapTris.Add(c10); SheetCapTris.Add(c01);
SheetCapTris.Add(c10); SheetCapTris.Add(c11); SheetCapTris.Add(c01);
}
}
// JUPES périmètre — mêmes règles que le chemin MC : extrusion le long de −N (vers le solide :
// bas pour le sol, HAUT pour le cap), double face, émises PAR SEAU (héritent le polygroup).
// Entre feuilles voisines les coins coïncident (mêmes échantillons monde) → pas de fissure ;
// la jupe couvre surtout la couture feuille ↔ anneau MC et les décalages de niveau.
if (bGenerateSkirts && (SheetGroundTris.Num() + SheetCapTris.Num()) > 0)
{
const float SkirtDepth = FMath::Max(1.0f, SkirtCells) * (float)StepXY * VOXEL_SIZE;
auto EmitEdgeSkirt = [&](int32 ax, int32 ay, int32 bx, int32 by, bool bCap, TArray<int32>& Tris)
{
if (Col(ax, ay).X == FLT_MAX || Col(bx, by).X == FLT_MAX) return;
const int32 iA = MakeVert(ax, ay, bCap);
const int32 iB = MakeVert(bx, by, bCap);
// COPIES par valeur — les Add ci-dessous réallouent Vertices/Normals/Colors.
const FVector PA = MeshData.Vertices[iA], PB = MeshData.Vertices[iB];
const FVector NA = MeshData.Normals[iA], NB = MeshData.Normals[iB];
const FColor CA = MeshData.Colors[iA], CB = MeshData.Colors[iB];
const int32 iA2 = MeshData.Vertices.Num();
MeshData.Vertices.Add(PA - NA * SkirtDepth);
MeshData.Normals.Add(NA);
MeshData.UVs.Add(FVector2D(PA.X / VOXEL_SIZE, PA.Y / VOXEL_SIZE));
MeshData.Colors.Add(CA);
const int32 iB2 = MeshData.Vertices.Num();
MeshData.Vertices.Add(PB - NB * SkirtDepth);
MeshData.Normals.Add(NB);
MeshData.UVs.Add(FVector2D(PB.X / VOXEL_SIZE, PB.Y / VOXEL_SIZE));
MeshData.Colors.Add(CB);
Tris.Add(iA); Tris.Add(iB); Tris.Add(iB2);
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iA2);
Tris.Add(iA); Tris.Add(iB2); Tris.Add(iB);
Tris.Add(iA); Tris.Add(iA2); Tris.Add(iB2);
};
// Une jupe n'est émise que si la cellule de bord adjacente a réellement été maillée
// (pas dans le trou XY) — sinon mur flottant sans surface.
for (int32 c = 0; c < CellsXY; ++c)
{
if (ShouldAbortWork()) return FVoxelMeshData();
for (int32 Pass = 0; Pass < 2; ++Pass)
{
const bool bCap = (Pass == 1);
TArray<int32>& Tris = bCap ? SheetCapTris : SheetGroundTris;
if (!CellInHole(c, 0))
EmitEdgeSkirt(c, 0, c + 1, 0, bCap, Tris); // bord Y-min
if (!CellInHole(c, CellsXY - 1))
EmitEdgeSkirt(c, CellsXY, c + 1, CellsXY, bCap, Tris); // bord Y-max
if (!CellInHole(0, c))
EmitEdgeSkirt(0, c, 0, c + 1, bCap, Tris); // bord X-min
if (!CellInHole(CellsXY - 1, c))
EmitEdgeSkirt(CellsXY, c, CellsXY, c + 1, bCap, Tris); // bord X-max
}
}
}
// Concatène sol PUIS cap : un run d'indices contigu par polygroup (contrat F17/BuildTileStreamSet).
MeshData.Triangles.Reserve(SheetGroundTris.Num() + SheetCapTris.Num());
MeshData.Triangles.Append(SheetGroundTris);
MeshData.Triangles.Append(SheetCapTris);
MeshData.NumCeilingTriangles = SheetCapTris.Num() / 3;
return MeshData;
}
@@ -1410,59 +1410,6 @@ namespace VoxelSeasonManifestPrivate
return bValid;
}
bool SaveJson(const FString& FilePath, const TSharedPtr<FJsonObject>& Root,
FString& OutReport)
{
const FString Directory = FPaths::GetPath(FilePath);
if (!Directory.IsEmpty() && !IFileManager::Get().MakeDirectory(*Directory, true))
{
OutReport = FString::Printf(TEXT("could not create manifest directory %s"), *Directory);
return false;
}
FString JsonText;
TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonText);
if (!FJsonSerializer::Serialize(Root.ToSharedRef(), Writer) || !Writer->Close()
|| !FFileHelper::SaveStringToFile(JsonText, *FilePath))
{
OutReport = FString::Printf(TEXT("could not write season manifest %s"), *FilePath);
return false;
}
return true;
}
bool LoadJson(const FString& FilePath, TSharedPtr<FJsonObject>& OutRoot,
FString& OutReport, FString* OutJsonText = nullptr)
{
FString JsonText;
if (!FFileHelper::LoadFileToString(JsonText, *FilePath))
{
OutReport = FString::Printf(TEXT("could not read season manifest %s"), *FilePath);
return false;
}
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonText);
if (!FJsonSerializer::Deserialize(Reader, OutRoot) || !OutRoot.IsValid())
{
OutReport = FString::Printf(TEXT("invalid JSON in season manifest %s"), *FilePath);
return false;
}
if (OutJsonText != nullptr)
{
*OutJsonText = MoveTemp(JsonText);
}
return true;
}
}
const TCHAR* VF_GetVoxelSeasonSelectionReasonName(EVoxelSeasonSelectionReason Reason)
{
switch (Reason)
{
case EVoxelSeasonSelectionReason::Grounded: return TEXT("grounded");
case EVoxelSeasonSelectionReason::Outlier: return TEXT("outlier");
case EVoxelSeasonSelectionReason::Fixed: return TEXT("fixed");
}
return TEXT("unknown");
}
namespace
@@ -863,12 +863,6 @@ void FVoxelStackSampler::Shutdown()
StopAndWrite();
}
bool FVoxelStackSampler::IsRunning() const
{
FScopeLock LifecycleLock(&LifecycleMutex);
return Impl != nullptr;
}
bool FVoxelStackSampler::RegisterCurrentThread(int32 LODLevel)
{
// Keep this as the first and only operation in the disabled fast path. In particular, do not
-4
View File
@@ -10,10 +10,6 @@ DEFINE_STAT(STAT_VoxelForgeTilesSkippedAllAir);
DEFINE_STAT(STAT_VoxelForgeTilesMeshed);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackSolid);
DEFINE_STAT(STAT_VoxelForgeTilesOpStackAir);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout);
DEFINE_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck);
DEFINE_STAT(STAT_VoxelForgeCaveMixOutOfLayout);
DEFINE_STAT(STAT_VoxelForgeCaveMixGap);
DEFINE_STAT(STAT_VoxelForgeCaveMixSurfaceWorld);
@@ -3800,55 +3800,6 @@ bool FVoxelStrateCorpus::LoadFromAssetRegistry(
return bLoaded && NumUnresolved == 0 && bPromotedLoaded;
}
bool FVoxelStrateCorpus::LoadFromSettings(const UVoxelSettings* Settings, FString& OutReport)
{
// Compatibility/audit entry point. It intentionally does not load any soft reference: the
// Asset Registry is the corpus source, while the settings pool is reported as the reason the
// first Tier 4a run saw only one vector.
TArray<FString> SettingsPaths;
if (Settings != nullptr)
{
TSet<FString> UniquePaths;
for (const TPair<int32, TSoftObjectPtr<UVoxelStrateDefinition>>& Pair : Settings->FixedStrates)
{
const FString Path = Pair.Value.ToSoftObjectPath().ToString();
if (!Path.IsEmpty())
{
UniquePaths.Add(Path);
}
}
for (const TSoftObjectPtr<UVoxelStrateDefinition>& Reference : Settings->StratePool)
{
const FString Path = Reference.ToSoftObjectPath().ToString();
if (!Path.IsEmpty())
{
UniquePaths.Add(Path);
}
}
for (const FString& Path : UniquePaths)
{
SettingsPaths.Add(Path);
}
SettingsPaths.Sort();
}
FString RegistryReport;
const bool bLoaded = LoadFromAssetRegistry(RegistryReport);
OutReport = FString::Printf(
TEXT("Settings audit: %d unique fixed/pool soft paths"), SettingsPaths.Num());
for (const FString& Path : SettingsPaths)
{
OutReport += FString::Printf(TEXT(" [%s]"), *Path);
}
if (Settings == nullptr)
{
OutReport += TEXT(" [settings object was null]");
}
OutReport += TEXT("; settings references are diagnostic only. ");
OutReport += RegistryReport;
return bLoaded;
}
int32 FVoxelStrateCorpus::NumForArchetype(ECaveGeneratorType Archetype) const
{
int32 Count = 0;
@@ -4921,7 +4872,7 @@ FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpu
Repeat.ArchetypeParams, Result.ArchetypeParams, Result.Archetype)
&& Repeat.ParentEntryIndices == Result.ParentEntryIndices
&& Repeat.ParentWeights == Result.ParentWeights,
TEXT("VF_RollStrateParams lost determinism for the same corpus/seed/index."));
TEXT("VF_RollStrateParamsDetailed lost determinism for the same corpus/seed/index."));
}
#endif
return Result;
@@ -5301,12 +5252,6 @@ bool VF_ValidateStrateCorpusFreeConstraints(
return false;
}
FStrateGenerationParams VF_RollStrateParams(const FVoxelStrateCorpus& Corpus,
int32 Seed, int32 Index)
{
return VF_RollStrateParamsDetailed(Corpus, Seed, Index).Params;
}
namespace
{
const TCHAR* VF_RecipeOpCode(EVoxelStrateOpClass OpClass)
@@ -5532,15 +5477,6 @@ FVoxelStrateRegionManifest VF_RollStrateRegionManifest(
ECaveGeneratorType::VerticalShafts,
ECaveGeneratorType::FloatingIslands,
};
static const EVoxelStrateParamBlock BlockIds[] =
{
EVoxelStrateParamBlock::TunnelNetwork,
EVoxelStrateParamBlock::Slab,
EVoxelStrateParamBlock::Maze,
EVoxelStrateParamBlock::Surface,
EVoxelStrateParamBlock::VerticalShaft,
EVoxelStrateParamBlock::FloatingIsland,
};
static const uint32 BlockSalts[] = { 0x1001u, 0x1003u, 0x1005u,
0x1007u, 0x1009u, 0x100Bu };
+37 -252
View File
@@ -2,7 +2,6 @@
// 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 "VoxelSeasonAsset.h"
#include "VoxelTypes.h" // For CHUNK_SIZE, VOXEL_SIZE, WorldToChunkCoord
@@ -234,8 +233,8 @@ namespace
{
// One projection result per passage is enough for the current voxel sample. The surrounding
// passage cache already moves with (manager, layout, chunk); a generation invalidates old flags
// before any caller can observe them. Keeping the entries indexed by passage avoids the TMap
// rehash path that this hot loop used to carry, while a collision-free hit remains exact.
// before any caller can observe them. Keeping the entries indexed by passage avoids a TMap
// rehash path in this hot loop, while a collision-free hit remains exact.
struct FPassageFloorProjectionCacheEntry
{
// The sample key lives once in FPassageEvaluationCache. A monotonically increasing
@@ -1334,7 +1333,6 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
}
Slot.Definition->GeneratorType = SeasonStrate.Archetype;
Slot.Definition->bUseOperatorStack = SeasonStrate.bUsesRecipe;
Slot.Definition->StrateHeightInChunks = SeasonStrate.HeightInChunks;
Slot.Definition->GenerationParams = SeasonStrate.Params.TunnelNetworkParams;
Slot.Definition->SlabParams = SeasonStrate.Params.SlabParams;
@@ -1401,10 +1399,9 @@ bool UVoxelStrateManager::Initialize(UVoxelSettings* Settings, int32 WorldSeed)
: FString::Printf(TEXT("Value_%d"), static_cast<int32>(Slot.Definition->GeneratorType));
VoxelForgeStartupTrace::RecordEvent(TEXT("strate_slot"), FString::Printf(
TEXT("\"index\":%d,\"definition\":\"%s\",\"archetype\":\"%s\",\"top_chunk_z\":%d,"
"\"bottom_chunk_z\":%d,\"height_chunks\":%d,\"operator_stack\":%d"),
"\"bottom_chunk_z\":%d,\"height_chunks\":%d"),
Slot.StrateIndex, *DefinitionPath, *ArchetypeName,
Slot.TopChunkZ, Slot.BottomChunkZ, Slot.HeightInChunks,
Slot.Definition->bUseOperatorStack ? 1 : 0));
Slot.TopChunkZ, Slot.BottomChunkZ, Slot.HeightInChunks));
}
UE_LOG(LogTemp, Log, TEXT("[StrateManager] Strate %d: '%s' | Z chunks [%d to %d] | %d chunks tall"),
@@ -1415,79 +1412,6 @@ bool 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)
{
const ECaveGeneratorType SlotArchetype = bUseSeason
? SeasonStrates[Slot.StrateIndex].Archetype : Slot.Definition->GeneratorType;
if (!Slot.Definition || SlotArchetype == ECaveGeneratorType::SurfaceWorld)
{
continue;
}
++NumCaveSlots;
const bool bUsesStack = bUseSeason
? SeasonStrates[Slot.StrateIndex].bUsesRecipe : Slot.Definition->bUseOperatorStack;
if (!bUsesStack)
{
++NumOperatorStackDisabledCaves;
}
}
// ⚠️ 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)
{
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
{
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)
{
const bool bSeasonStack = bUseSeason
&& SeasonStrates[Slot.StrateIndex].bUsesRecipe;
const ECaveGeneratorType SlotArchetype = bUseSeason
? SeasonStrates[Slot.StrateIndex].Archetype : Slot.Definition->GeneratorType;
if (!Slot.Definition
|| SlotArchetype == ECaveGeneratorType::SurfaceWorld
|| (bUseSeason ? bSeasonStack : Slot.Definition->bUseOperatorStack))
{
continue;
}
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;
bOpenSurfaceEntry = Settings->bOpenSurfaceEntry;
OriginSpineRadius = bUseSeason
@@ -1804,13 +1728,13 @@ void UVoxelStrateManager::GeneratePassages()
case ECaveGeneratorType::TunnelNetwork:
case ECaveGeneratorType::Underwater:
// Une salle est une CIBLE ÉPARSE en XY, exactement comme un couloir de labyrinthe : le
// budget doit donc être réel. Zéro ici était un bug — la requête trouvait la salle la
// plus proche puis atterrissait à côté d'elle dans 92,8 % des cas (balayage d'un million
// de seeds). Un espacement de salles est le voisinage local naturel.
// budget doit donc être réel. Zéro ici est un bug — la requête trouve la salle la plus
// proche puis atterrit à côté d'elle dans 92,8 % des cas (balayage d'un million de
// seeds). Un espacement de salles est le voisinage local naturel.
//
// A room is an XY-SPARSE target, exactly like a maze corridor, so the budget must be
// real. Zero here was the bug: the query found the nearest room and then landed beside
// it 92.8% of the time (million-seed sweep). One room spacing is the natural local
// real. Zero here is a bug: the query finds the nearest room and then lands beside it
// 92.8% of the time (million-seed sweep). One room spacing is the natural local
// neighbourhood, and it protects the configured spine-distance distribution.
return FMath::Max(Definition.GenerationParams.RoomSpacing, 1.0f);
@@ -2771,9 +2695,9 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float
{
const FVoxelPassage& P = PassageData[PIdx];
// BOUNDING-SPHERE REJECT: skip passages this voxel can't possibly be inside.
// EvaluateModifierSDF runs PER VOXEL and used to evaluate every passage's full
// capsule chain unconditionally — the dominant lag source once passages became
// 12-segment worms. Now far passages cost a single squared-distance compare.
// EvaluateModifierSDF runs PER VOXEL; evaluating every passage's full capsule chain
// unconditionally is the dominant lag source with 12-segment worm passages. Far
// passages cost a single squared-distance compare.
if (FVector::DistSquared(Pos, P.BoundCenter) > P.BoundRadiusSq) continue;
if (VoxelDensityProfile::AreCountersEnabled())
{
@@ -3023,20 +2947,6 @@ void UVoxelStrateManager::ApplyPassageLandingAir(
VF_ApplyPassageLandingCarving(Density, MinLandingSDF, BaseDensity, SealThickness);
}
void UVoxelStrateManager::ApplyPassageLandingAirMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
// ApplyDisturbances is deliberately an MC-space post-process and can add a bridge or ridge
// on top of the structural passage. Reassert only landing air here; the support slab is
// restored by ApplyPassageLandingFloorMC immediately afterwards. This stays on the same
// thread-local passage shortlist as the hot voxel path and performs no source/topology work.
float InternalDensity = -Density;
ApplyPassageLandingAir(
InternalDensity, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
Density = -InternalDensity;
}
void UVoxelStrateManager::ApplyPassageTunnelAir(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
@@ -3070,18 +2980,6 @@ void UVoxelStrateManager::ApplyPassageTunnelAir(
}
}
void UVoxelStrateManager::ApplyPassageTunnelAirMC(
float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const
{
// The disturbance layer uses MC polarity (negative = solid), so reuse the exact internal
// tunnel-air operation rather than maintaining a second polarity-specific formula.
float InternalDensity = -Density;
ApplyPassageTunnelAir(
InternalDensity, WorldX, WorldY, WorldZ, BaseDensity, SealThickness);
Density = -InternalDensity;
}
void UVoxelStrateManager::ApplyPassageLandingFloorMC(
float& Density, float WorldX, float WorldY, float WorldZ, float BaseDensity) const
{
@@ -3280,8 +3178,8 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC(
}
}
// The graph tunnel no longer exposes a separate support slab. Inter-strate landing carving
// remains independent and is still composed here when its own reach proof allows it.
// The graph tunnel does not expose a separate support slab. Inter-strate landing carving
// is independent and is composed here when its own reach proof allows it.
if (!bDisableLandingPosts
&& !bAnyLandingFloor && !bLegacyTunnelSupportFloor && MinLandingSDF < FLT_MAX)
{
@@ -4799,7 +4697,7 @@ bool UVoxelStrateManager::AnyPassageAirPostNearLattice(
// The union above is conservative but can be much smaller than the queried box. Restrict the
// exact walk to its lattice intersection; a false positive still pays the exact predicates,
// while a far-away long-passage sphere no longer does.
// while a far-away long-passage sphere does not.
if (!VF_LatticeAxisRange(
FMath::Max((float)VoxelBox.Min.X, PotentialMinX),
FMath::Min((float)VoxelBox.Max.X, PotentialMaxX),
@@ -6307,65 +6205,42 @@ bool UVoxelStrateManager::UsesOperatorStackForChunk(const FIntVector& ChunkCoord
const int32 SlotIdx = FindSlotIndexForChunkZ(ChunkCoord.Z);
if (SlotIdx < 0 || !StrateLayout[SlotIdx].Definition) { return false; }
if (SeasonStrates.IsValidIndex(SlotIdx))
{
// A recipe is an explicit stack opt-in. Native fixed entries retain the authored/switch
// route while still reading their season parameter vector.
return SeasonStrates[SlotIdx].bUsesRecipe;
}
#if WITH_EDITOR
if (FindComposerOverride(StrateLayout[SlotIdx].StrateIndex) != nullptr)
{
// Temporary composer candidates are measured through the stack path. Do not inherit an
// unrelated asset flag from the slot being replaced.
return true;
}
#endif
// Every strate is generated by the operator stack: authored definitions, cooked-season entries
// (recipe or native) and editor composer candidates alike. The slot definition carries the
// archetype in every case.
const UVoxelStrateDefinition* Def = StrateLayout[SlotIdx].Definition;
if (!Def->bUseOperatorStack) { return false; }
// LA LISTE DES ARCHÉTYPES PORTÉS — le seul endroit où elle est écrite. Un archétype non porté
// ignore le drapeau et retombe sur le `switch`, pour qu'on puisse cocher la case sur n'importe
// quelle strate sans rien casser en attendant son portage.
// THE PORTED-ARCHETYPE LIST, written down exactly once. An unported archetype ignores the flag
// and falls back to the switch, so the box can be ticked anywhere without breaking anything.
// LA LISTE DES ARCHÉTYPES — le seul endroit où elle est écrite.
// THE ARCHETYPE LIST, written down exactly once.
switch (Def->GeneratorType)
{
case ECaveGeneratorType::Maze: return true; // Phase 1
case ECaveGeneratorType::FlatPlain: // Phase 2 — les deux partagent
case ECaveGeneratorType::Maze: return true;
case ECaveGeneratorType::FlatPlain: // les deux partagent
case ECaveGeneratorType::CrystalChamber: return true; // UNE seule pile (BuildSlabStack)
case ECaveGeneratorType::SurfaceWorld:
// ✅ La garde « pas de biomes » est TOMBÉE (étape 2c) : le combiner `Mask` existe, donc une
// strate à biomes mélange bien ses hauteurs comme le chemin d'origine. Les trois archétypes
// du dessus plus celui-ci font 5 des 8 portés.
// The no-biome guard is GONE: the Mask combiner exists, so a biome strate blends its heights
// Pas de garde « pas de biomes » : le combiner `Mask` existe, donc une strate à biomes
// mélange bien ses hauteurs comme le chemin d'origine.
// No no-biome guard: the Mask combiner exists, so a biome strate blends its heights
// exactly as the original path does.
return true;
case ECaveGeneratorType::VerticalShafts: return true; // Phase 2 — 3 ops repris de Maze tels quels
case ECaveGeneratorType::VerticalShafts: return true; // 3 ops repris de Maze tels quels
case ECaveGeneratorType::FloatingIslands:
// Phase 2 — la pile qui tourne à l'ENVERS : source de VIDE + fill, au lieu de source de ROC
// La pile qui tourne à l'ENVERS : source de VIDE + fill, au lieu de source de ROC
// + carve, avec les MÊMES opérateurs au signe près.
return true;
case ECaveGeneratorType::Underwater:
// ⚠️ AUCUNE PILE À ELLE : `Underwater` EST `TunnelNetwork` plus un drapeau d'eau consommé
// côté rendu. `GetDensityAt` les met dans le même `case`, et `WaterLevelRelative` n'est lu
// côté rendu. `VF_BuildOpStackForChunk` les met dans le même `case`, et `WaterLevelRelative` n'est lu
// que par `GetWaterLevel*` de ce manager — jamais par la densité (vérifié, pas supposé).
case ECaveGeneratorType::TunnelNetwork:
// Phase 2, LE DERNIER, et le plus gros : ~1080 lignes portées en trois étapes (squelette
// SDF → douze modificateurs de détail → override d'op par salle), 19 opérateurs, dont
// `FRoomGraphSource` qui **APPELLE** `BuildChunkCache`/`EvaluateSDFCached` au lieu de les
// transcrire — c'est là que vit la discipline d'invariance de fenêtre d'ARCHITECTURE §8.4,
// et en forker une copie aurait été le pire résultat possible de ce refactor.
//
// **8 SUR 8.** Le `switch` d'archétypes a désormais un jumeau en pile d'opérateurs, opt-in
// par strate, chacun vérifié par un test d'équivalence bit à bit contre sa fonction
// d'origine. Ce qui n'est PAS fait : `ClassifyTile` n'utilise toujours pas `ClassifyBox`.
// Le plus gros : squelette SDF → douze modificateurs de détail → override d'op par salle,
// 19 opérateurs, dont `FRoomGraphSource` qui **APPELLE** `BuildChunkCache`/`EvaluateSDFCached`
// au lieu de les transcrire — c'est là que vit la discipline d'invariance de fenêtre
// d'ARCHITECTURE §8.4 ; en forker une copie la casserait.
return true;
default: return false;
@@ -6712,7 +6587,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
// Three transition styles:
//
// GRADIENT (default):
// Classic linear lerp of all params across BlendChunks. Smooth,
// Classic linear lerp of all params across TransitionBlendChunks. Smooth,
// invisible boundary. Cave shape morphs gradually from one strate
// to the next over several chunks.
//
@@ -6720,7 +6595,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
// No blending at all — params switch instantly at the boundary.
// The abrupt change in density, room size, roughness, etc. creates
// a natural cliff, ledge, or visible material discontinuity.
// BlendChunks is ignored (effectively 0).
// TransitionBlendChunks is ignored (effectively 0).
//
// INTERLEAVED:
// 3D Perlin noise warps the effective boundary Z position per XY column.
@@ -6740,7 +6615,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
// CHECK BOTTOM BOUNDARY (transitioning to strate below)
//---------------------------------------------------------------------
// DistFromBottom = how many chunks above the bottom edge of this strate.
// When 0, we're right at the boundary. When == BlendChunks, we're at
// When 0, we're right at the boundary. When == TransitionBlendChunks, we're at
// the outer edge of the transition zone.
int32 DistFromBottom = ChunkCoord.Z - Slot.BottomChunkZ;
@@ -6749,7 +6624,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
// The upper strate (this one) controls the transition type at its lower edge
const EVoxelStrateTransition TransType = Slot.Definition->TransitionType;
// Per-definition blend distance (overrides the manager's default BlendChunks)
// Per-definition blend distance.
const int32 EffectiveBlend = Slot.Definition->TransitionBlendChunks;
// Prepare the neighbor's params (only used for Gradient and Interleaved)
@@ -6929,7 +6804,7 @@ FStrateGenerationParams UVoxelStrateManager::GetGenerationParams(const FIntVecto
//=============================================================================
// Returns the definition's base GenerationParams (cave shape, SDF, roughness, etc.).
//
// NOTE: Terrain op fields (TerraceStepHeight, ColumnDensity, etc.) are no longer
// NOTE: Terrain op fields (TerraceStepHeight, ColumnDensity, etc.) are not
// merged here. They default to 0 (disabled) in the base params, and are applied
// per-room during BuildChunkCache() via FCachedRoom::RoomOp — each room hash-rolls
// one op from the strate's probability pool (FStrateTerrainOpEntry::Probability).
@@ -6952,93 +6827,3 @@ FStrateGenerationParams UVoxelStrateManager::BuildParamsFromDefinition(const UVo
return Result;
}
uint64 UVoxelStrateManager::GetGenerationParamsFingerprint() const
{
// The diagnostic command-line override changes density inputs without changing an asset. Do
// not let a verdict from the unoverridden layout survive into an overridden session (or vice
// versa); a zero fingerprint conservatively disables persistence for that session.
const FRuntimeRoughnessOverrides& RoughnessOverrides = VF_GetRuntimeRoughnessOverrides();
if (RoughnessOverrides.bSurfaceRoughness
|| RoughnessOverrides.bFrequency
|| RoughnessOverrides.bNoiseType)
{
return 0;
}
// The session cache is deliberately conservative. These inputs are valid density inputs but
// are not represented by the fixed-size hash below, so returning zero disables verdict reuse
// instead of pretending that a partial key is complete.
if (IsUsingSeason())
{
return 0;
}
#if WITH_EDITOR
if (ComposerOverrides.Num() > 0)
{
return 0;
}
#endif
uint32 A = 0x9E3779B9u;
uint32 B = 0x85EBCA6Bu;
auto HashBytes = [&A, &B](const void* Data, int32 Size)
{
A = FCrc::MemCrc32(Data, Size, A);
B = FCrc::MemCrc32(Data, Size, B ^ 0xA511E9B3u);
};
auto HashValue = [&HashBytes](const auto& Value)
{
HashBytes(&Value, sizeof(Value));
};
HashValue(CachedSeed);
HashValue(bOpenSurfaceEntry);
HashValue(OriginSpineRadius);
HashValue(InterStrateGapChunks);
const int32 LayoutCount = StrateLayout.Num();
HashValue(LayoutCount);
for (const FStrateSlot& Slot : StrateLayout)
{
if (Slot.Definition == nullptr)
{
return 0;
}
// BuildChunkCache resolves these arrays into the room cache. Hashing UObject pointers or
// asset names would not prove the asset contents, so dynamic operation/biome layouts
// conservatively opt out. The normal synthetic/runtime profile has neither array.
if (Slot.Definition->TerrainOperations.Num() > 0
|| Slot.Definition->Biomes.Num() > 0)
{
return 0;
}
HashValue(Slot.StrateIndex);
HashValue(Slot.TopChunkZ);
HashValue(Slot.BottomChunkZ);
HashValue(Slot.HeightInChunks);
HashValue(Slot.Definition->GeneratorType);
HashValue(Slot.Definition->bUseOperatorStack);
HashValue(Slot.Definition->bEnableWorms);
HashValue(VF_WormsForceOff());
// Development-only stage ablations change the field and therefore belong in every
// persistent/verdict key even though they are not authored generation parameters.
HashValue(VoxelDensityAblation::GetResolvedMask());
HashValue(Slot.Definition->StrateHeightInChunks);
HashValue(Slot.Definition->TransitionType);
HashValue(Slot.Definition->TransitionBlendChunks);
HashValue(Slot.Definition->GenerationParams);
HashValue(Slot.Definition->SlabParams);
HashValue(Slot.Definition->MazeParams);
HashValue(Slot.Definition->SurfaceParams);
HashValue(Slot.Definition->VerticalShaftParams);
HashValue(Slot.Definition->FloatingIslandParams);
HashValue(Slot.Definition->PassageConfig);
HashValue(Slot.Definition->bHasWater);
HashValue(Slot.Definition->BiomeMapParams);
}
const uint64 Result = (static_cast<uint64>(B) << 32) | static_cast<uint64>(A);
return Result != 0 ? Result : 1;
}
@@ -2973,24 +2973,6 @@ namespace VoxelStrateMeasurePrivate
return Result;
}
int32 GetSixNeighbour(const FSampleGrid& Grid, int32 Cell, int32 Direction)
{
int32 X = 0;
int32 Y = 0;
int32 Z = 0;
DecodeIndex(Grid, Cell, X, Y, Z);
switch (Direction)
{
case 0: return X + 1 < Grid.NumX ? Grid.Index(X + 1, Y, Z) : INDEX_NONE;
case 1: return X > 0 ? Grid.Index(X - 1, Y, Z) : INDEX_NONE;
case 2: return Y + 1 < Grid.NumY ? Grid.Index(X, Y + 1, Z) : INDEX_NONE;
case 3: return Y > 0 ? Grid.Index(X, Y - 1, Z) : INDEX_NONE;
case 4: return Z + 1 < Grid.NumZ ? Grid.Index(X, Y, Z + 1) : INDEX_NONE;
case 5: return Z > 0 ? Grid.Index(X, Y, Z - 1) : INDEX_NONE;
default: return INDEX_NONE;
}
}
int32 CountPlayerFitNeighbours(
const FSampleGrid& Grid,
const TArray<uint8>& PlayerFitMask,
@@ -3458,26 +3440,6 @@ FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
return Result;
}
EVoxelConnectivityResult VF_ArePlayerFitConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics)
{
int32 NumRouteRetries = 0;
return VoxelStrateMeasurePrivate::QueryConnectivity(
nullptr, nullptr, &Sampler, INDEX_NONE,
StrateBottomWorldZ, StrateTopWorldZ, BoundarySealThickness, true,
AVoxel, BVoxel, Settings, bOutStartSnapped, bOutGoalSnapped,
NumRouteRetries, OutDiagnostics, true, nullptr);
}
FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
+49 -179
View File
@@ -23,11 +23,10 @@
#include "VoxelTilePostReach.h"
#include "VoxelClipmapDesiredTiles.h"
// IWYU (FPSemantics = Precise ⇒ plus de PCH partagé) : GetPlayerPosition déréférence le pawn, donc
// APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController était
// complet par transitivité seulement : on l'inclut explicitement, c'est exactement la fragilité
// qu'on est en train de retirer.
// APawn doit être COMPLET — `Casts.h` n'en donne qu'une déclaration avant. APlayerController n'est
// complet que par transitivité : on l'inclut explicitement.
// GetPlayerPosition dereferences the pawn, so APawn must be COMPLETE — Casts.h only forward-declares
// it. APlayerController was complete transitively only; include it explicitly.
// it. APlayerController is complete only transitively; include it explicitly.
#include "GameFramework/Pawn.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PawnMovementComponent.h"
@@ -59,7 +58,6 @@
#include "Serialization/JsonWriter.h"
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings);
static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile,
const FVector& BoundsMin, const FVector& BoundsMax);
static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B);
@@ -186,11 +184,11 @@ AVoxelWorld::AVoxelWorld()
//=============================================================================
// T1.f — build the RMC geometry buffers OFF the game thread.
//=============================================================================
// FRealtimeMeshStreamSet is plain CPU data; the per-vertex/per-triangle builder loop used to run
// in ApplyMeshToTile ON THE GAME THREAD, where it was the dominant streaming cost (game thread
// >6 ms while moving, GPU/draw idle). It touches ONLY the POD MeshData arrays — no UObject, no
// generator — so it's safe on the gen worker. The game thread then just uploads the finished
// streams (CreateSectionGroup). Byte-identical geometry; the only thing that moved is WHERE it runs.
// FRealtimeMeshStreamSet is plain CPU data; on the game thread (in ApplyMeshToTile) the
// per-vertex/per-triangle builder loop is the dominant streaming cost (game thread >6 ms while
// moving, GPU/draw idle). It touches ONLY the POD MeshData arrays — no UObject, no generator — so
// it's safe on the gen worker. The game thread then just uploads the finished streams
// (CreateSectionGroup). Byte-identical geometry; only WHERE it runs differs.
static void BuildTileStreamSet(RealtimeMesh::FRealtimeMeshStreamSet& Streams, const FVoxelMeshData& MeshData)
{
RealtimeMesh::TRealtimeMeshBuilderLocal<uint32, FPackedNormal, FVector2DHalf, 1> Builder(Streams);
@@ -1586,16 +1584,12 @@ void AVoxelWorld::BeginPlay()
int32 TestMaxClipLevel = -1;
const bool bTestMaxClipLevel =
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestMaxClipLevel="), TestMaxClipLevel);
int32 TestFarSheetRing = -1;
const bool bTestFarSheetRing =
FParse::Value(FCommandLine::Get(), TEXT("voxel.TestFarSheetRing="), TestFarSheetRing);
if (bTestRenderDistance || bTestMaxClipLevel || bTestFarSheetRing)
if (bTestRenderDistance || bTestMaxClipLevel)
{
if (UVoxelSettings* TestSettings = DuplicateObject<UVoxelSettings>(Settings, this))
{
const int32 AuthoredRenderDistance = Settings->RenderDistanceChunks;
const int32 AuthoredMaxClipLevel = Settings->MaxClipLevel;
const bool bAuthoredFarSheetRing = Settings->bFarSheetRing;
if (bTestRenderDistance)
{
TestSettings->RenderDistanceChunks = TestRenderDistanceChunks;
@@ -1604,18 +1598,12 @@ void AVoxelWorld::BeginPlay()
{
TestSettings->MaxClipLevel = FMath::Clamp(TestMaxClipLevel, 0, 8);
}
if (bTestFarSheetRing)
{
TestSettings->bFarSheetRing = TestFarSheetRing != 0;
}
Settings = TestSettings;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeHorizonMeasure] transient_settings=1 "
"render_distance_chunks=%d authored=%d max_clip_level=%d authored=%d "
"far_sheet_ring=%d authored=%d"),
"render_distance_chunks=%d authored=%d max_clip_level=%d authored=%d"),
Settings->RenderDistanceChunks, AuthoredRenderDistance,
Settings->MaxClipLevel, AuthoredMaxClipLevel,
Settings->bFarSheetRing ? 1 : 0, bAuthoredFarSheetRing ? 1 : 0);
Settings->MaxClipLevel, AuthoredMaxClipLevel);
}
}
@@ -1707,18 +1695,17 @@ void AVoxelWorld::BeginPlay()
TEXT("\"asset\":\"%s\",\"seed\":%d,\"total_strates\":%d,\"inter_strate_gap_chunks\":%d,"
"\"strate_pool\":%d,\"strate_content_cut_min_level\":%d,"
"\"effective_strate_content_cut_min_level\":%d,\"max_clip_level\":%d,\"clip_radius\":%d,"
"\"render_distance_chunks\":%d,\"far_sheet_ring\":%d,\"far_sheet_span_levels\":%d,"
"\"render_distance_chunks\":%d,"
"\"enable_density_volume\":%d,\"density_volume_resolution\":%d,\"density_volume_levels\":%d,"
"\"density_volume_max_tasks\":%d,\"density_volume_gpu_upload\":%d"),
"\"density_volume_gpu_upload\":%d"),
*SettingsPath,
Settings->GetEffectiveWorldSeed(), Settings->TotalStrates,
Settings->InterStrateGapChunks, Settings->StratePool.Num(),
Settings->StrateContentCutMinLevel,
Settings->GetEffectiveStrateContentCutMinLevel(), Settings->MaxClipLevel,
Settings->ClipRadius, Settings->RenderDistanceChunks,
Settings->bFarSheetRing ? 1 : 0, Settings->FarSheetSpanLevels,
Settings->bEnableDensityVolume ? 1 : 0, Settings->DensityVolumeResolution,
Settings->DensityVolumeLevels, Settings->DensityVolumeMaxTasks,
Settings->DensityVolumeLevels,
Settings->bDensityVolumeGPUUpload ? 1 : 0));
}
@@ -2232,7 +2219,7 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, const FVoxelMeshData& MeshData)
const FVoxelMeshData& MeshData)
{
if (!bTileHashDumpEnabled)
{
@@ -2246,7 +2233,6 @@ void AVoxelWorld::RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& Or
Record.Cells = Cells;
Record.BandChunkLo = BandChunkLo;
Record.BandChunkHi = BandChunkHi;
Record.bSheetTile = bSheetTile;
Record.bEmpty = MeshData.IsEmpty();
Record.NumTriangles = MeshData.Triangles.Num() / 3;
Record.GeometryHash = VoxelForgeGeometryHash::Compute(MeshData);
@@ -2294,7 +2280,6 @@ void AVoxelWorld::WriteTileHashDump()
Writer->WriteValue(TEXT("coarse_tile_cells"), Settings->CoarseTileCells);
Writer->WriteValue(TEXT("max_clip_level"), Settings->MaxClipLevel);
Writer->WriteValue(TEXT("render_distance_chunks"), Settings->RenderDistanceChunks);
Writer->WriteValue(TEXT("far_sheet_ring"), Settings->bFarSheetRing);
Writer->WriteValue(TEXT("strate_content_cut_min_level"),
Settings->GetEffectiveStrateContentCutMinLevel());
Writer->WriteObjectEnd();
@@ -2315,7 +2300,8 @@ void AVoxelWorld::WriteTileHashDump()
Writer->WriteValue(TEXT("cells"), Record.Cells);
Writer->WriteValue(TEXT("band_chunk_lo"), Record.BandChunkLo);
Writer->WriteValue(TEXT("band_chunk_hi"), Record.BandChunkHi);
Writer->WriteValue(TEXT("sheet_tile"), Record.bSheetTile);
// The parity harness still compares this field; the runtime has no sheet tiles.
Writer->WriteValue(TEXT("sheet_tile"), false);
Writer->WriteValue(TEXT("empty"), Record.bEmpty);
Writer->WriteValue(TEXT("triangle_count"), Record.NumTriangles);
Writer->WriteValue(TEXT("geometry_hash"), Record.GeometryHash);
@@ -3018,11 +3004,10 @@ void AVoxelWorld::AdvanceHeadlessCollisionGateStressTest(
{
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeCollisionGateStressTest] owner_settings clip_radius=%d "
"max_clip_level=%d render_distance_chunks=%d far_sheet_ring=%d"),
"max_clip_level=%d render_distance_chunks=%d"),
Settings != nullptr ? Settings->ClipRadius : -1,
Settings != nullptr ? Settings->MaxClipLevel : -1,
Settings != nullptr ? Settings->RenderDistanceChunks : -1,
Settings != nullptr && Settings->bFarSheetRing ? 1 : 0);
Settings != nullptr ? Settings->RenderDistanceChunks : -1);
float CapsuleHalfHeightCm = 0.0f;
if (const ACharacter* Character = Cast<ACharacter>(PlayerPawn))
{
@@ -4577,12 +4562,11 @@ void AVoxelWorld::Tick(float DeltaTime)
const double SpanZ = static_cast<double>(Max.Z - Min.Z + 1) * TileExtentMetres;
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_desired_level"), FString::Printf(
TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d],"
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s],\"sheet\":%d"),
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s]"),
Level, SteadyLevelCounts.FindChecked(Level),
Min.X, Min.Y, Min.Z, Max.X, Max.Y, Max.Z,
*FString::SanitizeFloat(TileExtentMetres),
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ),
Settings && Level > VF_MaxMarchingCubesLevel(Settings) ? 1 : 0));
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ)));
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_loaded_level"), FString::Printf(
TEXT("\"level\":%d,\"tiles\":%d,\"vertices\":%llu,\"triangles\":%llu,\"geometry_bytes\":%llu"),
Level, LoadedLevelCounts.FindRef(Level),
@@ -6217,8 +6201,7 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
Result.GenerationStartCycles = GenerateStartCycles;
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
/*bSheetTile*/ false, /*SheetChunkZ*/ 0,
/*Hole*/ 0, 0, 0, 0, Result, nullptr); // hole unused (not a sheet tile)
Result, nullptr);
Result.GenerationEndCycles = GenerateStartCycles != 0
? FPlatformTime::Cycles64() : 0;
@@ -6471,30 +6454,16 @@ static FORCEINLINE FIntVector VF_FloorDiv(const FIntVector& V, int32 D)
return FIntVector(VF_FloorDiv(V.X, D), VF_FloorDiv(V.Y, D), VF_FloorDiv(V.Z, D));
}
// F18 — the outer shell is either the MC render-distance level or the larger sheet ring. When
// the sheet span skips levels, VF_MaxMarchingCubesLevel fills them with MC bridge shells.
// Shared by desired selection and range culling so both use the same outer horizon.
// The outer shell is the MaxClipLevel ring stretched to RenderDistanceChunks. Shared by desired
// selection and range culling so both use the same outer horizon.
static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, int32 MaxLevel,
int32& OutLevel, int32& OutRadius)
{
VoxelClipmapDesiredTiles::FParameters Parameters;
Parameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0;
Parameters.bFarSheetRing = Settings && Settings->bFarSheetRing;
Parameters.FarSheetSpanLevels = Settings ? Settings->FarSheetSpanLevels : 2;
VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevel, OutLevel, OutRadius);
}
// When an expanded sheet ring skips LOD levels, keep an MC bridge at every intervening level.
static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings)
{
const int32 Radius = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
int32 OuterLevel = MaxLevel, OuterRadius = Radius;
VF_OuterShell(Settings, Radius, MaxLevel, OuterLevel, OuterRadius);
(void)OuterRadius;
return OuterLevel > MaxLevel ? OuterLevel - 1 : MaxLevel;
}
void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition,
APawn* PlayerPawn, const FVector& PlayerHeading,
TArray<FVoxelTileKey>& OutLeavers)
@@ -6512,8 +6481,7 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
// Render-distance tiles expand horizontally. The sheet ring retains its configured span;
// intermediate levels are emitted as marching-cubes bridge shells below it.
// Render-distance tiles expand horizontally.
// IsTileInClipRange shares the outer-shell calculation so culling sees this same horizon.
int32 StrateTopZ = 0, StrateBottomZ = 0;
bool bHasStrate = false;
@@ -6537,8 +6505,6 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
TileParameters.ClipRadius = R;
TileParameters.MaxClipLevel = MaxLevel;
TileParameters.RenderDistanceChunks = Settings ? Settings->RenderDistanceChunks : 0;
TileParameters.bFarSheetRing = Settings && Settings->bFarSheetRing;
TileParameters.FarSheetSpanLevels = Settings ? Settings->FarSheetSpanLevels : 2;
VoxelClipmapDesiredTiles::Build(Center, ZLo, ZHi, TileParameters, DesiredSorted);
for (const FVoxelTileKey& Key : DesiredSorted)
{
@@ -6681,10 +6647,10 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
DesiredTransitionMaxLevel = FMath::Max(DesiredTransitionMaxLevel, Pair.Key.Level);
}
// A worker from the previous desired epoch can finish late with a component at a level no
// longer present in TileComponents when this index is built. Index through the supported
// maximum sheet level (MaxClipLevel 8 + FarSheetSpanLevels 4) so that such a retired tile still
// longer present in TileComponents when this index is built. Index through the highest
// possible tile level (MaxClipLevel is clamped to 8) so that such a retired tile still
// finds all of its desired descendants when the result is reconciled.
constexpr int32 MaxSupportedTileLevel = 12;
constexpr int32 MaxSupportedTileLevel = 8;
const int32 DescendantIndexMaxLevel = FMath::Max(
DesiredTransitionMaxLevel, MaxSupportedTileLevel);
for (const FVoxelTileKey& Key : DesiredSorted)
@@ -6727,7 +6693,7 @@ void AVoxelWorld::CancelObsoleteTileWork()
}
// Fold every registered anchor's small level-0 box into the current desired set (§9.3). Runs inside
// BuildDesiredTiles after the player clipmap + sheet ring, keyed on the same DesiredStamp so the leaver
// BuildDesiredTiles after the player clipmap, keyed on the same DesiredStamp so the leaver
// sweep + delta cull handle anchor tiles leaving. Level-0 only (collision lives on level-0 tiles); empty
// tiles in the box are ~free (the trivial-tile reject skips gen). Dedup vs the player clipmap by stamp.
// §9.4: a tile the player clipmap did NOT stamp (bNew) that only a CollisionOnly anchor wants goes into
@@ -6839,7 +6805,7 @@ void AVoxelWorld::UnregisterStreamingAnchor(AActor* Actor)
bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector& Center) const
{
// In range = the tile's centre falls within the OUTERMOST shell (VF_OuterShell — the
// render-distance/sheet-ring level+radius, same as BuildDesiredTiles). A loaded-but-not-
// render-distance level+radius, same as BuildDesiredTiles). A loaded-but-not-
// desired tile in range is mid-LOD-transition (wait for its replacement); one out of range
// has left the view entirely (cull immediately).
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
@@ -7021,13 +6987,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
const double SpanZ = static_cast<double>(Summary.Max.Z - Summary.Min.Z + 1) * TileExtentMetres;
VoxelForgeStartupTrace::RecordEvent(TEXT("desired_level"), FString::Printf(
TEXT("\"level\":%d,\"tiles\":%d,\"min_coord\":[%d,%d,%d],\"max_coord\":[%d,%d,%d],"
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s],\"sheet\":%d"),
"\"tile_extent_m\":%s,\"coverage_extent_m\":[%s,%s,%s]"),
Summary.Level, Summary.Count,
Summary.Min.X, Summary.Min.Y, Summary.Min.Z,
Summary.Max.X, Summary.Max.Y, Summary.Max.Z,
*FString::SanitizeFloat(TileExtentMetres),
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY), *FString::SanitizeFloat(SpanZ),
Settings && Summary.Level > VF_MaxMarchingCubesLevel(Settings) ? 1 : 0));
*FString::SanitizeFloat(SpanX), *FString::SanitizeFloat(SpanY),
*FString::SanitizeFloat(SpanZ)));
}
bStartupTraceDesiredRecorded = true;
}
@@ -7044,13 +7010,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// either way; everything else re-gens in place via BandRemeshQueue (no visual pop).
{
const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9;
// F18 — l'anneau feuille dépend aussi de la bande (sa strate de référence) : on l'arme
// dès que les feuilles sont actives, même si la coupe de contenu MC est désactivée.
const bool bSheetsWantBand = Settings && Settings->bFarSheetRing
&& Settings->RenderDistanceChunks > 0;
const int32 TopMC = VF_MaxMarchingCubesLevel(Settings);
int32 NewLo = MIN_int32, NewHi = MAX_int32;
if ((CutMin <= 8 || bSheetsWantBand) && StrateManager)
if (CutMin <= 8 && StrateManager)
{
int32 StrTopZ = 0, StrBotZ = 0;
if (StrateManager->GetStrateChunkZBounds(CenterChunk.Z, StrTopZ, StrBotZ))
@@ -7069,8 +7030,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
NewLo, NewHi, MeshBandChunkLo, MeshBandChunkHi, CutMin, CenterChunk.Z);
for (const FVoxelTileKey& T : LoadedTiles)
{
// Sheets (above the last MC bridge level) always depend on the strate band.
if (T.Level < CutMin && T.Level <= TopMC) continue;
if (T.Level < CutMin) continue;
const int32 CLo = T.Coord.Z << T.Level;
const int32 CHi = ((T.Coord.Z + 1) << T.Level) - 1;
const bool bInOld = CLo >= MeshBandChunkLo && CHi <= MeshBandChunkHi;
@@ -7087,44 +7047,6 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
}
}
// The sheet XY hole follows the outermost MC bridge level and shrinks by one tile, leaving
// an overlap at the mesh transition. Requeue sheets overlapping either the old or new hole
// when the player crosses a bridge-level tile.
{
int32 NewMinX = MAX_int32, NewMinY = MAX_int32;
int32 NewMaxX = MIN_int32, NewMaxY = MIN_int32;
const int32 TopMCLvl = VF_MaxMarchingCubesLevel(Settings);
if (Settings && Settings->bFarSheetRing && Settings->RenderDistanceChunks > 0)
{
const int32 RClip = FMath::Max(1, Settings->ClipRadius);
const int32 Shrink = FMath::Max(0, RClip - 1);
const int32 ExtM = CHUNK_SIZE << TopMCLvl; // tuile MaxLevel en voxels
const FIntVector CM = VF_FloorDiv(CenterChunk, 1 << TopMCLvl); // tuile MaxLevel du joueur
NewMinX = (CM.X - Shrink) * ExtM;
NewMinY = (CM.Y - Shrink) * ExtM;
NewMaxX = (CM.X + Shrink + 1) * ExtM; // EXCLUSIF
NewMaxY = (CM.Y + Shrink + 1) * ExtM;
}
if (NewMinX != SheetHoleMinXVox || NewMinY != SheetHoleMinYVox
|| NewMaxX != SheetHoleMaxXVox || NewMaxY != SheetHoleMaxYVox)
{
for (const FVoxelTileKey& T : LoadedTiles)
{
if (T.Level <= TopMCLvl) continue; // seules les feuilles portent le trou
const int32 Ext = CHUNK_SIZE << T.Level;
const int32 TMinX = T.Coord.X * Ext, TMinY = T.Coord.Y * Ext;
const bool bOldOv = TMinX < SheetHoleMaxXVox && TMinX + Ext > SheetHoleMinXVox
&& TMinY < SheetHoleMaxYVox && TMinY + Ext > SheetHoleMinYVox;
const bool bNewOv = TMinX < NewMaxX && TMinX + Ext > NewMinX
&& TMinY < NewMaxY && TMinY + Ext > NewMinY;
if ((bOldOv || bNewOv) && IsDesired(T)) BandRemeshQueue.Add(T);
}
SheetHoleMinXVox = NewMinX; SheetHoleMinYVox = NewMinY;
SheetHoleMaxXVox = NewMaxX; SheetHoleMaxYVox = NewMaxY;
bAllChunksLoaded = false;
}
}
// Cull rules — STRICT LOAD-BEFORE-UNLOAD so crossing a shell boundary NEVER leaves a hole:
// - out of clip range → left the view entirely, no replacement coming → cull now.
// - in range (mid-LOD-transition) → cull ONLY once EVERY desired tile that overlaps its
@@ -7391,20 +7313,9 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
const int32 FullRes = Settings ? FMath::Max(1, Settings->FullResClipLevels) : 2;
const int32 Extent = CHUNK_SIZE << Tile.Level;
// F18 — sheet-ring keys follow the last MC bridge level and mesh as two heightfields
// (GenerateSheetMesh). Their sampling step matches that MC level; cells grow with sheet extent
// up to 128 per axis, after which the step increases.
const int32 TopMC = VF_MaxMarchingCubesLevel(Settings);
const bool bSheetTile = Tile.Level > TopMC;
int32 Cells = (Tile.Level < FullRes)
const int32 Cells = (Tile.Level < FullRes)
? CHUNK_SIZE
: (Settings ? FMath::Clamp(Settings->CoarseTileCells, 4, CHUNK_SIZE) : 16);
if (bSheetTile)
{
const int32 StepMC = FMath::Max(1, (CHUNK_SIZE << TopMC) / Cells);
Cells = FMath::Clamp(Extent / StepMC, 4, 128);
}
const int32 Step = FMath::Max(1, Extent / Cells);
const uint32 TaskEpoch = GenerationEpoch;
const uint32 TaskDesiredEpoch = DesiredEpoch;
@@ -7425,40 +7336,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
// below the configured minimum and all LODs in the gap (no band) mesh full.
int32 BandVoxLo = INT32_MIN, BandVoxHi = INT32_MAX;
int32 BandChunkLo = MIN_int32, BandChunkHi = MAX_int32;
int32 SheetChunkZ = 0;
if (bSheetTile)
{
// F18 — la feuille a besoin de la STRATE de référence (les deux heightfields sont ceux de
// la strate du joueur) : pas de bande armée (gap inter-strates, ou bounds introuvables)
// ⇒ rien à mailler, tuile vide (re-queue automatique via BandRemeshQueue en atterrissant).
if (MeshBandChunkLo == MIN_int32)
{
FChunkResult Empty;
Empty.Tile = Tile;
Empty.Epoch = TaskEpoch;
Empty.DesiredEpoch = TaskDesiredEpoch;
Empty.ModificationEpoch = TaskModificationEpoch;
Empty.RequestStartCycles = RequestStartCycles;
Empty.WorkPriority = WorkPriority;
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
CriticalProcessQueue.Enqueue(MoveTemp(Empty));
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
EditedProcessQueue.Enqueue(MoveTemp(Empty));
else if (WorkPriority == EVoxelTileWorkPriority::LOD0)
NearProcessQueue.Enqueue(MoveTemp(Empty));
else
ProcessQueue.Enqueue(MoveTemp(Empty));
return;
}
BandChunkLo = MeshBandChunkLo; // pour la résolution matériaux sol/cap dans ApplyMeshToTile
BandChunkHi = MeshBandChunkHi;
SheetChunkZ = MeshBandChunkLo + (MeshBandChunkHi - MeshBandChunkLo) / 2; // chunk au cœur de la strate
}
// F18 — trou XY courant (zone couverte par les coquilles MC, découpée des feuilles).
const int32 HoleMinX = SheetHoleMinXVox, HoleMinY = SheetHoleMinYVox;
const int32 HoleMaxX = SheetHoleMaxXVox, HoleMaxY = SheetHoleMaxYVox;
const int32 CutMin = Settings ? Settings->GetEffectiveStrateContentCutMinLevel() : 9;
if (!bSheetTile && Tile.Level >= CutMin && MeshBandChunkLo != MIN_int32)
if (Tile.Level >= CutMin && MeshBandChunkLo != MIN_int32)
{
BandChunkLo = MeshBandChunkLo;
BandChunkHi = MeshBandChunkHi;
@@ -7512,7 +7391,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
: UE::Tasks::ETaskPriority::BackgroundNormal;
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
RequestStartCycles, TaskDesiredEpoch, TaskModificationEpoch, Cancellation,
WorkPriority]()
{
@@ -7565,7 +7443,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
Result.GenerationStartCycles = GenerationStartCycles;
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
Result, &Cancellation->bObsolete);
Result.GenerationEndCycles = GenerationStartCycles != 0
? FPlatformTime::Cycles64() : 0;
@@ -7611,8 +7488,6 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, int32 SheetChunkZ,
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
FChunkResult& Result,
const std::atomic<bool>* ObsoleteFlag)
{
@@ -7666,7 +7541,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
/ static_cast<double>(WormEligibleSamples)
: 0.0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d triangles=%d ceiling_triangles=%d "
TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d aborted=%d empty=%d triangles=%d ceiling_triangles=%d "
"band=(%d,%d) "
"verdict=%d proof=%d air_proof=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f "
"cache_build=%.6f evaluation=%.6f "
@@ -7686,7 +7561,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
"worm_eligible=%llu worm_block_proofs=%llu worm_block_skipped=%llu "
"worm_skip_rate=%.6f"),
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells,
bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0,
Result.bAborted ? 1 : 0,
Result.bEmpty ? 1 : 0, Result.NumTriangles, Result.NumCeilingTriangles,
BandChunkLo, BandChunkHi,
ClassifyVerdict, Result.bSealedSolidProof ? 1 : 0,
@@ -7825,7 +7700,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
// (Gate IsoLevel == 0 : les verdicts du classifieur supposent l'iso MC à zéro exactement.)
bool bTrivialEmpty = false;
if (GVoxelForgeSealedSolidProof != 0
&& !bSheetTile && !bWantCapture && Generator && Mesher
&& !bWantCapture && Generator && Mesher
&& Mesher->IsoLevel == 0.0f)
{
TotalSealedSolidProofCandidates.fetch_add(1, std::memory_order_relaxed);
@@ -7867,7 +7742,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
}
const int32 OuterClassifierMode = FMath::Clamp(GVoxelForgeOuterClassifierMode, 0, 1);
if (!bTrivialEmpty && OuterClassifierMode != 0
&& !bSheetTile && !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f)
&& !bWantCapture && Generator && Mesher && Mesher->IsoLevel == 0.0f)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ClassifyTile);
INC_DWORD_STAT(STAT_VoxelForgeTilesClassified);
@@ -7972,8 +7847,6 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
bTrivialEmpty = (ValidatedVerdict != EVoxelTileClass::Mixed);
}
// F18 — feuille : deux heightfields sol/cap échantillonnés par colonne (pas de marching
// cubes, pas de classifieur — la classe de surface est vraie par construction).
// `TilesMeshed` peut dépasser `TilesClassified` : les tuiles qui ratent cette porte sont
// maillées sans classification. / `TilesMeshed` may exceed `TilesClassified`: tiles that
// fail this gate are meshed without classification.
@@ -7982,10 +7855,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GenerateMesh);
const double MeshStartSeconds = bMeasureTile ? FPlatformTime::Seconds() : 0.0;
MeshData = bSheetTile
? Mesher->GenerateSheetMesh(OriginVoxels, Step, Cells, SheetChunkZ,
HoleMinX, HoleMinY, HoleMaxX, HoleMaxY)
: Mesher->GenerateMesh(OriginVoxels, Step, Cells,
MeshData = Mesher->GenerateMesh(OriginVoxels, Step, Cells,
bWantCapture ? &Result.CaptureGrid : nullptr,
BandVoxLo, BandVoxHi,
&Result.MesherDensityCalls);
@@ -8041,7 +7911,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
// Parity instrumentation observes the same FVoxelMeshData that the runtime stream builder
// consumes. It is intentionally after the abort fence, so a canceled worker cannot publish a
// partial identity, and it records empty/proof tiles as well as visible meshes.
RecordTileHash(Tile, OriginVoxels, Step, Cells, BandChunkLo, BandChunkHi, bSheetTile, MeshData);
RecordTileHash(Tile, OriginVoxels, Step, Cells, BandChunkLo, BandChunkHi, MeshData);
Result.ClassifySeconds = ClassifySeconds;
Result.MeshSeconds = MeshSeconds;
@@ -8157,7 +8027,7 @@ void AVoxelWorld::UnloadTile(const FVoxelTileKey& Tile)
CollisionNotRequiredTiles.Remove(Tile);
CollisionSolidTiles.Remove(Tile);
PendingCollisionCooks.Remove(Tile);
// Water + decorations are no longer tile-bound (water is one player-following ocean plane via
// Water + decorations are not tile-bound (water is one player-following ocean plane via
// UpdateWater; decorations stream by distance via UpdateDecorations) — nothing to clear per tile.
if (URealtimeMeshComponent** Comp = TileComponents.Find(Tile))
{
@@ -8333,7 +8203,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
}
BindRealtimeMeshCollisionEvent(RTMesh);
// Shadow casting: far (level >= 2) tiles never cast; the sky-cap SECTION never casts either
// — otherwise the high rock ceiling shadows the entire terrain below it. F17: shadow is now
// — otherwise the high rock ceiling shadows the entire terrain below it. F17: shadow is
// PER SECTION, so a mixed tile keeps its ground shadow while its cap stays shadowless.
const bool bCastShadow = (Tile.Level <= 1);
MeshComp->SetCastShadow(bCastShadow);
@@ -8453,7 +8323,7 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
TotalSeconds);
}
// Water is no longer spawned per tile — it's a single player-following ocean plane (UpdateWater,
// Water is not spawned per tile — it's a single player-following ocean plane (UpdateWater,
// driven from Tick), so it renders at every LOD and to the horizon with no per-tile gaps.
return true;
}
@@ -9135,9 +9005,9 @@ void AVoxelWorld::UpdateOrbLightMPC()
const FVoxelActiveOrb& O = Orbs[i];
V = FLinearColor((float)O.WorldPos.X, (float)O.WorldPos.Y, (float)O.WorldPos.Z, O.FalloffWorld);
}
// ALWAYS write, even when unchanged. A skip-if-identical cache was tried here and BROKE the
// lighting: the MPC's world INSTANCE can be reset/recreated behind our back (PIE init order,
// asset recompile), and a cached skip then leaves it holding defaults forever. The per-frame
// ALWAYS write, even when unchanged. A skip-if-identical cache BREAKS the lighting: the
// MPC's world INSTANCE can be reset/recreated behind our back (PIE init order, asset
// recompile), and a cached skip then leaves it holding defaults forever. The per-frame
// rewrite is what makes the collection self-healing — and 4 vector writes cost nothing.
UKismetMaterialLibrary::SetVectorParameterValue(this, OrbLightMPC, OrbNames[i], V);
}
@@ -76,16 +76,6 @@ namespace
namespace VoxelWormField
{
float EvaluateExact(
float WorldX,
float WorldY,
float WorldZ,
float Threshold,
const FParameters& Parameters)
{
return EvaluateExactField(WorldX, WorldY, WorldZ, Threshold, Parameters);
}
float Evaluate(
float WorldX,
float WorldY,
@@ -22,9 +22,9 @@
#include "VoxelStrateTypes.h" // FStrateDecoration / FStrateAmbientActor
#include "VoxelBiomeDefinition.generated.h"
// IWYU : utilisé en pointeur seulement ⇒ déclaration avant. Fournie gratuitement par le PCH
// partagé jusqu'ici ; `FPSemantics = Precise` nous en sort. / Pointer-only use, so a forward
// declaration is enough. The shared PCH used to provide this for free.
// IWYU : utilisé en pointeur seulement ⇒ déclaration avant. Le PCH partagé ne la fournit pas :
// `FPSemantics = Precise` nous en sort. / Pointer-only use, so a forward declaration is enough.
// The shared PCH does not provide it (FPSemantics = Precise opts this module out of it).
class UMaterialInterface;
/**
+11 -68
View File
@@ -243,7 +243,7 @@ namespace VoxelHash
* collident donneraient un bruit identique PARTOUT. Salé par site, il faudrait qu'ils
* collident sur les ~50 sites à la fois — c'est-à-dire jamais.
*
* The multiplier no longer decorrelates by amplifying — it IDENTIFIES the site, and the hash
* The multiplier does not decorrelate by amplifying — it IDENTIFIES the site, and the hash
* decorrelates. Output is already in final units and bounded, so the ULP is 10% of a voxel step.
*
* @param SiteKey la constante littérale d'origine (`7.3f`, `97.7f`, …). Gardée VISIBLE au site
@@ -439,10 +439,10 @@ namespace VoxelMazeTopology
// le motif qui a produit `AUDIT §C1` (un correctif appliqué à une copie sur deux). Il monte donc au
// point le plus bas qui voit déjà le hash, et le générateur comme la pile d'opérateurs l'appellent.
//
// Ce corps était `static float CellularNoise3D(const FVector&)` dans VoxelGenerator.cpp, invisible
// à la pile d'opérateurs. Il vit ici pour que les deux chemins partagent la même implémentation.
// Il vit ici pour que le générateur et la pile d'opérateurs partagent la même implémentation.
// The hot overload uses FVector3f: noise coordinates are floats, so no double round-trip is paid
// before the hash/lattice arithmetic. Output compatibility with the old field is not a contract.
// before the hash/lattice arithmetic. Output compatibility with a double-precision evaluation is
// not a contract.
//
// Algorithme : distance au point-feature le plus proche dans une grille hachée.
// 1. cellule entière du point 2. voisinage 3×3×3 3. rendre (F2 − F1), normalisé ~[-1, 1]
@@ -520,7 +520,7 @@ namespace VoxelNoise
// PER-CHUNK SDF CACHE
//=============================================================================
// The room list and tunnel connections are IDENTICAL for all voxels in a chunk.
// Without caching, EvaluateSDF rebuilds these 32,768 times per 32³ chunk:
// Without caching, a per-voxel evaluation would rebuild these 32,768 times per 32³ chunk:
// - Hash every grid cell in the search area
// - Determine room existence, position, size
// - Run O(N²) nearest-neighbor backbone
@@ -604,9 +604,9 @@ struct FCachedRoom
float RoomOpWeight = 1.0f;
// PRE-BAKED SHAPE (BuildChunkCache). The shape roll, variety thresholds and the capsule's
// Cos/Sin direction used to be re-derived PER VOXEL per room inside EvaluateSDFCached — hash
// mixes + trig in the hottest loop of the plugin for values that are constants of the room.
// Bit-identical to the old per-voxel roll (same hashes, same math, done once per chunk).
// Cos/Sin direction are constants of the room: re-deriving them PER VOXEL inside
// EvaluateSDFCached would put hash mixes + trig in the hottest loop of the plugin.
// Bit-identical to a per-voxel roll (same hashes, same math, done once per chunk).
// 0 = ellipsoid → ShapeA = radii (x=y=RadiusXY, z=RadiusZ)
// 1 = rounded box → ShapeA = half-extents, ShapeR = corner rounding
// 2 = capsule → ShapeA/ShapeB = world endpoints, ShapeR = tube radius
@@ -637,9 +637,8 @@ struct FCachedRoomFloorJoin
// One immutable floor segment authored while the tunnel chain is built. The floor is continuous
// unless bLedgeTransition is set; that marker means the segment terminates at a deliberate
// dramatic ledge and the vertical riser is placed at the next control-point boundary. NumSteps is
// retained as a compatibility/diagnostic field and is always zero for newly authored tunnels: a
// steep floor is never quantised into a staircase. ReliefScale is baked with a derivative bound,
// dramatic ledge and the vertical riser is placed at the next control-point boundary. A steep
// floor is never quantised into a staircase. ReliefScale is baked with a derivative bound,
// so evaluation only reads this profile and never decides a floor shape per voxel.
struct FTunnelFloorSegmentProfile
{
@@ -652,9 +651,6 @@ struct FTunnelFloorSegmentProfile
float NaturalEndFloorZ = 0.0f;
float ReliefScale = 0.0f;
bool bLedgeTransition = false;
// Deprecated staircase count. New profiles leave this at zero; keep the member so old
// diagnostic consumers can still compile while the authored representation has one meaning.
int32 NumSteps = 0;
};
// A pre-computed wandering tunnel chain — all connection decisions and hash-derived properties
@@ -933,13 +929,10 @@ struct FChunkSDFCache
VoxelDensityProfile::FCacheMemoryBreakdown GetAllocatedSizeBreakdown() const;
};
/** World-space tunnel shape query. Legacy support-floor fields remain source-compatible but are
* no longer produced or consumed by the graph-tunnel generation path. */
/** World-space tunnel shape query. */
struct FTunnelCoreWorldEvaluation
{
float SDF = FLT_MAX;
// Deprecated compatibility output. The separate graph-tunnel support slab was removed.
bool bSupportFloor = false;
// Room geometry owns the lower side while the world-space tunnel is inside a room. The
// tunnel SDF may still reopen air there, but its swept bottom must not turn that room air back
// into a shelf or wall.
@@ -955,7 +948,6 @@ struct FTunnelCoreWorldEvaluation
// CAVE MORPHOLOGY EVALUATOR
//=============================================================================
// Two-phase evaluation: BuildChunkCache (once per chunk) + EvaluateSDFCached (per voxel).
// The original EvaluateSDF is kept as a convenience wrapper for backward compatibility.
namespace VoxelCaveMorphology
{
@@ -1065,15 +1057,6 @@ namespace VoxelCaveMorphology
bool* OutRoomOwnsBottom = nullptr
);
// Evaluate the raw union of cached graph-tunnel capsules. This is intentionally separate from
// the SmoothMin morphology: the generator uses it after terrain/passage posts to reassert the
// tunnel's walkable air core without reintroducing a connector or changing room ownership.
VOXELFORGE_API float EvaluateTunnelCoreSDF(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
bool bUseSpatialIndex = true
);
// Evaluate the final structural air contract against the world-space wandering chain. Room
// SDFs remain evaluated in warped coordinates; only this post uses world coordinates so its
// walkable floor follows the authored chain exactly.
@@ -1106,46 +1089,6 @@ namespace VoxelCaveMorphology
float ReachScale = 1.0f
);
// Deprecated compatibility query for the removed graph-tunnel support slab. Production
// generation does not call this predicate; the tunnel's own swept shape owns its floor.
VOXELFORGE_API bool IsTunnelSupportFloorWorldPoint(
float WorldX, float WorldY, float WorldZ,
const FChunkSDFCache& Cache,
bool bUseSpatialIndex = true
);
// Deprecated compatibility helper for callers that still inspect the old support-band
// diagnostic. It is detached from production density generation.
VOXELFORGE_API void BuildTunnelSupportFloorColumn(
float WorldX, float WorldY,
const FChunkSDFCache& Cache,
FTunnelSupportFloorColumn& OutColumn,
bool bUseSpatialIndex = true
);
VOXELFORGE_API bool IsTunnelSupportFloorColumnZ(
float WorldZ,
const FTunnelSupportFloorColumn& Column
);
// Returns the legacy projected band for one tunnel at this XY column. The world evaluator no
// longer uses it to write a support slab.
VOXELFORGE_API bool GetTunnelSupportFloorColumnBand(
int32 TunnelIndex,
const FTunnelSupportFloorColumn& Column,
float& OutFloorZ,
float& OutMinZ,
float& OutMaxZ
);
// CONVENIENCE WRAPPER: builds a temporary cache and evaluates in one call.
// Use this for one-off queries (debug visualization, single-point sampling).
// For chunk generation, use BuildChunkCache + EvaluateSDFCached instead.
float EvaluateSDF(
float WorldX, float WorldY, float WorldZ,
const FStrateGenerationParams& Params,
uint32 Seed, int32 StrateIndex
);
}
/**
@@ -17,7 +17,8 @@
// budgeted. The game thread never does decoration density math. Flow (UpdateDecorations each Tick):
// - recompute desired cell set on player cell-boundary / strate change
// - LaunchDecoTasks: fire async march tasks (capped by MaxConcurrentDecorationTasks)
// - ProcessDecoResults: drain finished tasks' results, apply (spawn) budgeted, epoch-guarded
// - ProcessDecoResults: drain finished cell results into their region builds (BuildId-guarded),
// then apply completed regions (spawn) budgeted
// Decorations exist ONLY in the player's current strate (march is strate-bounded) → a strate change
// wipes + rebuilds them, and there is no cross-strate light bleed to cull.
//
+26 -46
View File
@@ -1,34 +1,32 @@
// VoxelDensityOp.h
// LE CONTRAT de la pile d'opérateurs de densité / THE density operator stack CONTRACT.
// Phase 1 de OPSTACK-PLAN.md. The contract is implemented by the opt-in stack path in
// UVoxelGenerator::GetDensityAt; the legacy archetype switch remains for non-opt-in strates.
// Design : Docs/archive/OPSTACK-PLAN.md. The contract is implemented by the stack path in
// UVoxelGenerator::GetDensityAt, which generates every strate.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// POURQUOI / WHY
// ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelGenerator::GetDensityAt est aujourd'hui un `switch` sur 8 ECaveGeneratorType, chacun
// possédant sa fonction de densité et son struct de params. Conséquences : une nouvelle idée de
// monde coûte ~6 sites d'édition, et surtout LES IDÉES NE PEUVENT PAS SE COMBINER — un archétype
// possède le voxel entier. On ne peut pas écrire « une strate de surface dont les montagnes
// contiennent un réseau de salles, avec des îles flottantes dans le vide au-dessus », à aucun prix.
// Chaque archétype (ECaveGeneratorType) est une composition d'opérateurs, pas une fonction de
// densité monolithique qui posséderait le voxel entier. Une idée de monde s'exprime en empilant
// sources, combinateurs et modificateurs — et les idées peuvent donc SE COMBINER : « une strate de
// surface dont les montagnes contiennent un réseau de salles, avec des îles flottantes au-dessus ».
//
// GetDensityAt is today a `switch` over 8 ECaveGeneratorType, each owning a bespoke density
// function and param struct. A new world idea costs ~6 edit sites, and — the real problem —
// IDEAS CANNOT COMBINE: one archetype owns the whole voxel.
// Each archetype (ECaveGeneratorType) is a composition of operators, not a monolithic density
// function that owns the whole voxel. A world idea is expressed by stacking sources, combiners and
// modifiers — so ideas can COMBINE.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ⚠️ CE N'EST PAS L'ANCIEN SYSTÈME DE « ROOM OPERATIONS » / THIS IS NOT THE OLD ROOM-OPS SYSTEM
// ─────────────────────────────────────────────────────────────────────────────────────────
// UVoxelTerrainOpDefinition (Terrace, Ribbing, Cliff, Scallop, Overhang, Arch, Column, Pit…) ne
// sait que PERTURBER une densité près d'une surface qui existe déjà. Il ne décide jamais ce que le
// champ EST — cette décision vit dans le `switch`. Une pile d'opérateurs qui se contenterait de
// cela aurait reconstruit le switch avec des étapes en plus.
// champ EST — c'est le rôle des sources de champ ci-dessous.
//
// D'où QUATRE RÔLES, dont l'ancien système n'occupait que le troisième :
//
// 1. FIELD SOURCE — fabrique un champ À PARTIR DE RIEN. C'est ce qui fait qu'une grotte est
// une grotte et qu'un monde ouvert est un monde ouvert. Chaque archétype
// d'aujourd'hui est fondamentalement l'une de ces sources.
// est fondamentalement l'une de ces sources.
// 2. COMBINER — comment deux champs fusionnent (min/max/smooth/mask). C'EST le rôle qui
// achète la composition ; sans lui il n'y a pas de refactor.
// 3. DETAIL MODIFIER — l'ancien système, rétrogradé à un rôle sur quatre. Inchangé.
@@ -56,9 +54,8 @@
// An SDF-only writer reports an SDF interval; a later converter folds that interval through its own
// response. Each operator answers for itself, so an unverified composition remains sound.
//
// La première version de cette interface n'avait qu'une direction numérique. Presque tout opérateur
// existant est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. Cela suffit à reproduire
// GÉNÉRIQUEMENT chaque garde écrite à la main dans ClassifyTile :
// Presque tout opérateur est UNIDIRECTIONNEL : il ne fait que creuser, ou que remplir. La direction
// suffit donc à exprimer GÉNÉRIQUEMENT les gardes de ClassifyTile :
//
// « passages ⇒ bCanSolid = false » EST CarveOnly
// « ponts/arêtes ⇒ bCanAir = false » EST FillOnly
@@ -71,26 +68,16 @@
// it never lets an SDF writer impersonate a downstream neighbour.
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// LE GROS LOT PERF : les strates de grotte ne sautent AUCUNE tuile aujourd'hui
// THE PERF PRIZE: cave strates skip ZERO tiles today
// CLASSIFYTILE
// ─────────────────────────────────────────────────────────────────────────────────────────
// ClassifyTile conserve ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les
// archétypes de cave opt-in passent maintenant par le même pliage `ClassifyBox`. Le post XY fournit
// en plus la première preuve globale de coque : une tuile entièrement dans la bande forcée peut être
// ClassifyTile garde ses preuves exactes pour les gaps de bedrock et SurfaceWorld ; les slots de
// cave passent par le pliage `ClassifyBox` de la pile même qui produit leur densité. Le post XY
// fournit en plus une preuve globale de coque : une tuile entièrement dans la bande forcée peut être
// sautée sans connaître l'archétype ou le Z.
//
// **Traiter cela comme un livrable explicite de chaque portage, pas comme un effet de bord.**
//
// ─────────────────────────────────────────────────────────────────────────────────────────
// ÉTAT / STATUS
// ─────────────────────────────────────────────────────────────────────────────────────────
// Les huit archétypes ont maintenant une pile derrière l'opt-in `bUseOperatorStack`, et ClassifyTile
// utilise cette pile pour les slots de cave opt-in. Le chemin legacy reste disponible pour les
// strates non opt-in; les tests d'équivalence servent de garde de portage.
//
// All eight archetypes now have an operator-stack path behind `bUseOperatorStack`, and ClassifyTile
// uses that stack for opt-in cave slots. The legacy switch remains available for opt-out strates;
// equivalence tests are port-correctness guards.
// ClassifyTile keeps its exact proofs for bedrock gaps and SurfaceWorld; cave slots go through the
// `ClassifyBox` fold of the same stack that produces their density. The XY post adds a global
// shell proof: a tile entirely inside the forced band is skipped without knowing archetype or Z.
//
// NOTE sur les UENUM : ces types sont volontairement du C++ nu (pas d'UHT, pas de .generated.h).
// Ils deviendront UENUM/USTRUCT en Phase 3, quand les opérateurs deviendront des data assets et
@@ -337,11 +324,6 @@ struct FVoxelOpSample
// FLT_MAX = "no surface nearby" — the initial state, and the early-out placed sources use.
float Sdf = FLT_MAX;
// Deprecated structural-floor compatibility fields. Graph tunnels now carry their floor in
// the swept shape itself; the production stack does not use these fields to write a slab.
bool bProtectedStructuralFloor = false;
float StructuralFloorMinimumDensity = 0.0f;
// Block evaluation keeps the room source's per-sample selection explicit. The scalar path
// still uses the source's worker-local state, but a later block-capable modifier must never
// observe the state belonging to the last sample of the preceding operator.
@@ -353,7 +335,6 @@ struct FVoxelOpSample
// consulting a last-sample TLS value after an op-major evaluation.
bool bHasTunnelCoreWorldEvaluation = false;
float TunnelCoreWorldSDF = FLT_MAX;
bool bTunnelCoreSupportFloor = false;
bool bTunnelCoreRoomFloor = false;
bool bHasTunnelCoreSweptFloor = false;
float TunnelCoreSweptFloorZ = -FLT_MAX;
@@ -720,13 +701,12 @@ public:
/**
* DIAGNOSTIC UNIQUEMENT — le nom que les rapports de test impriment pour cet opérateur.
*
* ⚠️ POURQUOI CETTE MÉTHODE EXISTE, ET CE QU'ELLE A COÛTÉ DE NE PAS AVOIR. Le premier build de
* l'`EffectOverBox` spatial est revenu **vert avec 0 tuile prouvée sur 40**, et la seule chose
* que le rapport pouvait dire était « ou bien les tuiles traversent toutes une grotte, ou bien
* la source n'atteint pas sa branche `Identity` ». Deux causes, zéro nombre pour les
* départager — exactement le piège que ce projet a déjà payé plusieurs fois. La vraie cause
* était un TROISIÈME opérateur (le ver, qui rendait `CarveOnly` partout). Avec un nom par
* opérateur, `ClassifyBoxAttributed` répond « c'est celui-là » au lieu de laisser deviner.
* ⚠️ POURQUOI CETTE MÉTHODE EXISTE. Sans nom par opérateur, un rapport « 0 tuile prouvée sur
* 40 » ne peut dire que « ou bien les tuiles traversent toutes une grotte, ou bien la source
* n'atteint pas sa branche `Identity` » — deux causes, zéro nombre pour les départager, alors
* que la vraie cause peut être un TROISIÈME opérateur (p. ex. un ver qui rend `CarveOnly`
* partout). Avec un nom par opérateur, `ClassifyBoxAttributed` répond « c'est celui-là » au
* lieu de laisser deviner.
*
* N'entre dans AUCUNE clé de cache, dans aucun hash, dans aucune décision de génération : le
* changer ne peut pas changer le monde. Le défaut est volontairement laconique — un opérateur
+21 -43
View File
@@ -2,34 +2,21 @@
// La PILE : un conteneur ordonné d'opérateurs, plus les fabriques d'opérateurs concrets.
// The STACK: an ordered container of operators, plus the concrete-operator factories.
//
// ⚠️ CECI ALIMENTE LE JEU, MAIS SEULEMENT SUR OPT-IN (depuis OPSTACK-PLAN §4, Phase 1, point 3).
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk et l'évalue à la place du `switch`
// UNIQUEMENT quand `UVoxelStrateManager::UsesOperatorStackForChunk` rend true — c.-à-d. quand la
// strate a coché `bUseOperatorStack` ET que son archétype figure dans la liste des portés :
// ⚠️ CECI EST LE CHEMIN DE GÉNÉRATION DU JEU.
// `UVoxelGenerator::GetDensityAt` construit la pile par chunk pour chaque strate :
// **Maze, FlatPlain, CrystalChamber, SurfaceWorld, VerticalShafts, FloatingIslands, TunnelNetwork,
// Underwater (8 sur 8)**.
// Toute autre strate passe encore par le `switch`, inchangé.
// `ClassifyTile` est branché pour les archétypes de cave opt-in : il construit la même pile et
// plie `ClassifyBox`; les gaps et SurfaceWorld gardent leurs preuves exactes dédiées.
// Underwater (8 sur 8)**. `ClassifyTile` construit la même pile pour les archétypes de cave et plie
// `ClassifyBox`; les gaps et SurfaceWorld gardent leurs preuves exactes dédiées.
//
// THIS FEEDS THE GAME, BUT ONLY BEHIND AN OPT-IN. GetDensityAt builds the stack per chunk and
// evaluates it instead of the switch only when UsesOperatorStackForChunk returns true (strate ticked
// bUseOperatorStack AND its archetype is ported — all 8). ClassifyTile uses the same stack for cave
// archetypes; gaps and SurfaceWorld retain their exact hand-written proofs.
//
// ⛔ NE JAMAIS faire tourner les deux chemins dans le même monde.
// ⚠️ EN REVANCHE, LES COMPARER EST DEVENU LÉGITIME — cette ligne disait l'inverse et elle est
// périmée. `AUDIT §C10` (le résidu ~1 ULP) est CLOS depuis `FPSemantics = Precise` : les cinq tests
// d'équivalence comparent bit à bit et sont verts. Ils ne sont plus des contrôles de FIDÉLITÉ (la
// barre `§2.6.1` n'exige aucune ressemblance avec l'ancien monde) mais des oracles de
// CORRECTION DE PORTAGE — une faute de transcription reste un vrai bug, et l'ancienne fonction est
// le moyen le moins cher de l'attraper.
// THIS IS THE GAME'S GENERATION PATH. GetDensityAt builds the stack per chunk for every strate.
// ClassifyTile uses the same stack for cave archetypes; gaps and SurfaceWorld retain their exact
// hand-written proofs.
//
// POURQUOI CETTE FORME / WHY THIS SHAPE
// La question à laquelle la Phase 1 doit répondre n'est pas « est-ce que ça marche ? » mais
// La question à laquelle cette forme répond n'est pas « est-ce que ça marche ? » mais
// **« est-ce que la séparation source / modifier tombe naturellement du code existant ? »**
// (OPSTACK-PLAN §4, le déclencheur d'arrêt). En portant Maze hors du chemin chaud et en le
// comparant à l'original, cette question reçoit une réponse MESURÉE plutôt qu'une opinion.
// (OPSTACK-PLAN §4). Chaque archétype a été porté puis comparé bit à bit à sa fonction
// d'origine avant qu'elle ne soit retirée : la réponse a été MESURÉE plutôt qu'une opinion.
#pragma once
@@ -203,10 +190,10 @@ public:
return EvalSample(WorldX, WorldY, WorldZ).Density;
}
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand une comparaison
* avec l'ancien chemin diverge, c'est le canal SDF qui dit si l'écart naît avant ou après
* la conversion. / The full state after the stack — the SDF channel is what says whether a
* divergence is born before or after the carve. */
/** L'état COMPLET (densité + SDF) après toute la pile. Diagnostic : quand deux évaluations
* divergent, c'est le canal SDF qui dit si l'écart naît avant ou après la conversion.
* / The full state after the stack — the SDF channel is what says whether a divergence is
* born before or after the carve. */
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
{
FVoxelOpSample S;
@@ -229,11 +216,11 @@ public:
// The room-graph source publishes the authored floor ownership while evaluating the
// shape, but it must not be reasserted here. This stack is followed by the common
// disturbance/passage writers; composing the floor at this point made every authored
// floor sample look like a post-hoc clamp (and counted 100% of them). The generator now
// disturbance/passage writers; composing the floor at this point would make every authored
// floor sample look like a post-hoc clamp (and counted 100% of them). The generator
// consumes the same immutable core result after those writers and composes the floor once,
// at the final MC boundary. The metadata remains on the sample for that hand-off and for
// diagnostics, but it is no longer allowed to modify density inside the operator stack.
// diagnostics, but it is not allowed to modify density inside the operator stack.
return S;
}
@@ -568,12 +555,6 @@ namespace VoxelDensityOps
* `FillOnly` quand une colonne atteint la boîte, `Identity` (le cas courant) sinon. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeGridColumnMod(const FSlabGenerationParams& P, int32 Seed);
/** Rôle 1 — le pont entre les deux espaces : consomme les piles de HAUTEUR (sol + voûte,
* `VoxelHeightOp.h`) et en fait une densité. `IsXYPure()` est **false** — les hauteurs sont
* pures en XY, la densité est une distance à celles-ci et ne peut pas l'être. */
VOXELFORGE_API TUniquePtr<IVoxelDensityOp> MakeSurfaceColumnSource(const FSurfaceGenerationParams& P,
int32 Seed);
/**
* SurfaceWorld, COMPLET : colonne (sol + voûte) → densité, overhang 3D, post structurel, et le
* mélange de biomes quand `PerBiomeParams` est non vide.
@@ -597,9 +578,8 @@ namespace VoxelDensityOps
* FlatPlain ET CrystalChamber — la même pile, **sans branchement sur le type** :
* SlabVoidSource → GridColumnMod → [structural post ×4]
*
* C'est le premier vrai gain du refactor (OPSTACK-PLAN §4) : deux des huit archétypes
* disparaissent dans un opérateur, et leur différence redevient ce qu'elle était déjà dans
* `GetSlabDensity` — un jeu de valeurs par défaut, pas du code.
* Deux des huit archétypes tiennent dans un opérateur (OPSTACK-PLAN §4) : leur différence est
* un jeu de valeurs par défaut, pas du code.
*/
VOXELFORGE_API void BuildSlabStack(FVoxelOpStack& OutStack, const FSlabGenerationParams& P,
int32 Seed, float SpineRadius,
@@ -611,10 +591,8 @@ namespace VoxelDensityOps
* ConstantRock → ShaftField → SdfRoughness → SdfCarve → ShaftLedge
* → ShaftConnectorAir → ShaftConnectorFloor → [structural post ×4]
*
* C'est la démonstration que `§2.5` promettait : dans le `switch`, Maze et VerticalShafts sont
* deux fonctions de ~100 lignes sans rien de commun à l'œil ; en opérateurs, ce sont les mêmes
* trois ops avec une source différente et d'autres réglages (fréquence 0.1 au lieu de 0.12,
* fenêtre `rough + 4` au lieu de `R + rough + 2`).
* Trois de ces ops sont ceux de Maze (`§2.5`), avec une source différente et d'autres réglages
* (fréquence 0.1 au lieu de 0.12, fenêtre `rough + 4` au lieu de `R + rough + 2`).
*/
VOXELFORGE_API void BuildVerticalShaftStack(FVoxelOpStack& OutStack, const FVerticalShaftParams& P,
int32 Seed, float SpineRadius,
@@ -68,8 +68,8 @@ FORCEINLINE void VF_ApplyBoundarySeal(float& Density, float WorldZ,
// CARVE DE PASSAGE / PASSAGE CARVING
//=============================================================================
// Creuse un passage inter-strates. Évalué APRÈS le seal pour que les passages puissent percer à
// travers le bouchon solide. Le rayon de blend hard-codé à 4.0f correspond à l'ancienne valeur —
// à exposer via UVoxelSettings si on veut pouvoir le tweaker.
// travers le bouchon solide. Le rayon de blend est hard-codé à 4.0f — à exposer via
// UVoxelSettings si on veut pouvoir le tweaker.
FORCEINLINE void VF_ApplyPassageCarving(float& Density, float ModSDF,
float BaseDensity, float SealThickness)
{
@@ -300,7 +300,6 @@ FORCEINLINE void VF_ApplyXYEdgeSealMC(float& Density, float WorldX, float WorldY
// mathematical inner edge the helper is a true no-op, so claiming AllSolid there would be false.
namespace VoxelDensityReach
{
constexpr float SpineBlend = 3.0f;
constexpr float PassageBlend = 4.0f;
constexpr float EdgeProofMargin = 1.0f;
}
@@ -312,7 +312,6 @@ namespace VoxelDensityProfile
FCacheMemoryBreakdown RoomGraphCacheBreakdown;
};
VOXELFORGE_API void SetEnabled(bool bEnabled);
VOXELFORGE_API void SetMode(EMode Mode, uint32 SampleInterval = DefaultSampleInterval);
// Counters are collected only while a diagnostic mode is enabled. In the ordinary clean
// path this is a single relaxed load, so the measurement hooks stay dormant.
@@ -177,9 +177,9 @@ private:
void LaunchPendingFills(); // flush PendingFills onto the dedicated fill thread
void DrainResults(); // apply finished fills into the toroidal arrays (+ GPU dirty)
// DEDICATED FILL THREAD. The volume fill used to run on the shared UE::Tasks pool (BackgroundLow),
// where it STARVED behind mesh-gen (10 s to resolve shadows at a fresh spot). It now runs on its own
// thread (off the pool) so it's fast AND never steals a core from mesh-gen. Game thread enqueues
// DEDICATED FILL THREAD. On the shared UE::Tasks pool (BackgroundLow) the volume fill STARVES
// behind mesh-gen (10 s to resolve shadows at a fresh spot). It runs on its own thread (off the
// pool) so it's fast AND never steals a core from mesh-gen. Game thread enqueues
// FPendingFill (Spsc), the thread re-evaluates GetDensityAt and pushes FFillResult into Results
// (existing Mpsc, drained on the game thread by DrainResults). Capture still short-circuits most of
// this (cache hits blit on the game thread, no fill); the thread is the backstop for misses.
+16 -77
View File
@@ -3,8 +3,8 @@
//
// Pipeline density-only: plus de grille de blocs, plus de surface heightfield.
// Le mesher appelle GetDensityAt() par voxel pour reconstruire la géométrie.
// Toute la logique de caves vit dans GetDensityWithParams / GetSlabDensity,
// pilotée par les params de strate récupérés auprès du StrateManager.
// Chaque strate est générée par sa pile d'opérateurs (VoxelDensityOpStack.h), pilotée par les
// params de strate récupérés auprès du StrateManager.
#pragma once
@@ -202,12 +202,11 @@ public:
/**
* Densité pour une strate TunnelNetwork (rooms + tunnels + worm noise).
* Utilisée en interne par GetDensityAt quand la strate est de ce type.
* C'est l'évaluateur fusionné de la pile TunnelNetwork/Underwater dans GetDensityAt.
* Exposée pour permettre des tests isolés avec des params custom.
*
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES, ET C'EST LE CORRECTIF
* D'`AUDIT §C2` (2026-07-28). Le cache SDF interne est clé sur (boîte XY, strate, seed) et
* PAS sur les params. Or `GetGenerationParams` BLENDE les params à l'intérieur d'une même
* ⚠️ `ParamsFingerprint` ET `LayoutVersion` SONT OBLIGATOIRES (`AUDIT §C2`). Le cache SDF
* interne est clé sur (boîte XY, strate, seed) et PAS sur les params. Or `GetGenerationParams` BLENDE les params à l'intérieur d'une même
* strate — `Alpha` dépend du chunk Z en mode `Gradient` (le DÉFAUT, avec
* `TransitionBlendChunks = 2`) et du chunk XY en plus en mode `Interleaved`. Deux chunks de la
* même strate, même seed, donc même clé, mais des params DIFFÉRENTS : le worker évalue le
@@ -243,56 +242,8 @@ public:
bool bCollectFusedDiagnostics = false,
// Optional same-sample hand-off from EvaluateSDFCached. This is
// only a room-shape ownership bit; it does not alter the scalar
// result and avoids a duplicate room-index query in native paths.
bool* OutRoomOwnsBottom = nullptr,
// Optional same-sample warped position. Native GetDensityAt uses
// this to feed the core post without applying the cave warp twice.
FVector* OutWarpedPosition = nullptr) const;
/**
* Densité pour une strate Slab (FlatPlain / CrystalChamber).
* Vide horizontal entre un sol bruité et un plafond bruité.
*/
float GetSlabDensity(float WorldX, float WorldY, float WorldZ,
const FSlabGenerationParams& Params) const;
/**
* Densité pour une strate Maze — couloirs sur un treillis 3D déterministe, avec les décisions
* parent/boucle reconstruites par cellule dans un cache thread-local; le hot path ne parcourt
* que les capsules déjà émises.
*/
float GetMazeDensity(float WorldX, float WorldY, float WorldZ,
const FMazeGenerationParams& Params) const;
/**
* Densité pour une strate SurfaceWorld — terrain à ciel ouvert (collines,
* montagnes, plages) sous un plafond solide, avec nappe d'eau optionnelle.
*
* Biome output-blend: the heightfield is evaluated with ParamsD (the voxel's dominant
* biome) and, when NeighborWeight > 0, also with ParamsN (its nearest neighbour); the
* two surface HEIGHTS are lerped. This stays seamless across ANY param difference
* (frequencies included). With no biomes, pass ParamsD == ParamsN and weight 0 →
* bit-identical to the single-param terrain. Structural fields (Z bounds, seal, base,
* water level) must be equal in both (forced from the strate).
*/
float GetSurfaceDensity(float WorldX, float WorldY, float WorldZ,
const FSurfaceGenerationParams& ParamsD,
const FSurfaceGenerationParams& ParamsN,
float NeighborWeight) const;
/**
* Densité pour une strate VerticalShafts — puits verticaux pleine hauteur,
* vires horizontales, et connecteurs horizontaux occasionnels.
*/
float GetVerticalShaftDensity(float WorldX, float WorldY, float WorldZ,
const FVerticalShaftParams& Params) const;
/**
* Densité pour une strate FloatingIslands — masses de terre suspendues dans
* un grand vide ouvert (îles flottantes), sommets aplatis, dessous rugueux.
*/
float GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ,
const FFloatingIslandParams& Params) const;
// result and avoids a duplicate room-index query.
bool* OutRoomOwnsBottom = nullptr) const;
//=========================================================================
// CLIMATE & BIOME FIELDS (pure XY, deterministic, window-invariant)
@@ -309,20 +260,19 @@ public:
* La chaîne de hauteur complète de SurfaceWorld : structural → cliff → terrace → layer lines →
* plage. Rend une ALTITUDE monde en voxels, pas une densité.
*
* PUBLIQUE pour la même raison que `GetSlabDensity` / `GetMazeDensity` : permettre un test
* isolé. C'est la référence de `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la
* pile d'opérateurs de hauteur (`VoxelHeightOp.h`) à cette fonction point par point.
* Public so the height-op stack can be measured against it — same reason as GetSlabDensity.
* PUBLIQUE pour permettre un test isolé : c'est la référence de
* `VoxelForge.OpStack.SurfaceHeightEquivalence`, qui compare la pile d'opérateurs de hauteur
* (`VoxelHeightOp.h`) à cette fonction point par point.
* Public so the height-op stack can be measured against it.
*/
float ComputeSurfaceTerrainZ(float WorldX, float WorldY, const FSurfaceGenerationParams& Params) const;
/**
* La colonne de surface : terrain Z, plafond, et le gate d'OVERHANG résolu par colonne
* (amplitude + direction amont). PUBLIQUES toutes deux pour la même raison que ci-dessus :
* c'est le seul chemin qui calcule l'overhang — `GetSurfaceDensity` passe `OverhangAmp = 0` —
* donc c'est la seule référence possible pour `FOverhangShelfMod`.
* Public because this is the ONLY path that computes the overhang (GetSurfaceDensity passes 0),
* so it is the only possible reference for the ported op.
* (amplitude + direction amont). C'est la colonne que `ClassifyTile` teste pour SurfaceWorld ;
* publique pour que les tests puissent placer des échantillons dans la fenêtre d'overhang.
* The surface column ClassifyTile tests for SurfaceWorld; public so tests can aim samples at
* the overhang window.
*/
void ComputeSurfaceColumn(float WorldX, float WorldY, int32 ChunkZ,
const FSurfaceGenerationParams& BaseSurface, const FBiomeContext& BiomeCtx,
@@ -330,12 +280,6 @@ public:
float& OutTerrainZ, float& OutCeilSurf,
float& OutOverhangAmp, float& OutDirX, float& OutDirY) const;
/** Le combine par voxel : colonne → densité, overhang compris, puis le post structurel. */
float SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ,
float TerrainZ, float CeilSurf,
float OverhangAmp, float DirX, float DirY,
const FSurfaceGenerationParams& S) const;
/**
* Moisture field at a world XY → [0,1]. The second climate axis for biome placement.
*/
@@ -415,7 +359,7 @@ public:
*
* 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,
* chemin densité. Un unique slot cave 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).
@@ -468,11 +412,6 @@ private:
FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx,
TArray<FSurfaceGenerationParams>& OutBiomeParams) const;
// ComputeSurfaceColumn et SurfaceDensityFromColumn ont été DÉPLACÉES en `public` (voir plus
// haut) : c'est le seul chemin qui calcule l'overhang, donc la seule référence possible pour
// VoxelForge.OpStack.SurfaceHeightEquivalence. Une seule déclaration chacune.
// Moved to public above — the only path that computes the overhang, hence the only oracle.
/** (Re)build the per-chunk biome cell grid covering chunk (X,Y) footprint + margin. */
void RebuildBiomeGrid(int32 ChunkX, int32 ChunkY, int32 ChunkZ,
const FBiomeContext& Ctx, FChunkBiomeCache& Cache,
@@ -78,35 +78,6 @@ public:
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
int64* OutDensitySampleCount = nullptr);
/**
* F18 — FEUILLE de champ lointain (anneau render-distance, cf. UVoxelSettings::bFarSheetRing).
* Dans une strate ouverte (SurfaceWorld) le champ lointain est exactement DEUX heightfields —
* TerrainZ (sol) et CeilSurf (plafond sky-cap), déjà calculés par colonne par l'oracle
* GetSurfaceHeightAt. On construit donc deux grilles déplacées régulières au lieu d'un marching
* cubes 3D : sol = polygroup 0, cap = polygroup 1 (classes vraies PAR CONSTRUCTION — pas de vote,
* pas de sonde de classification), mêmes conventions que GenerateMesh (positions monde cm, UVs
* planaires, masques couleur F6 biome/pente/fondu, normales du gradient de hauteur, jupes
* périmètre par seau, run d'indices sol‖cap + NumCeilingTriangles).
*
* @param OriginVoxels - Coin min de la tuile (voxels). Seul XY est utilisé (les hauteurs sont absolues).
* @param StepXY - Pas d'échantillonnage XY en voxels (aligné sur l'anneau MC pour la continuité).
* @param CellsXY - Cellules par axe XY (extent = CellsXY × StepXY).
* @param StrateChunkZ - Chunk Z DANS la strate de référence (le cœur de la bande) — identifie la
* strate dont on maille sol+cap. Hors SurfaceWorld ⇒ mesh vide.
* @param HoleMin/MaxX/YVox - TROU XY (voxels, Max EXCLUSIF ; sentinelles MAX/MIN = pas de trou) :
* les cellules ENTIÈREMENT dans ce rectangle (la zone couverte par les
* coquilles MC autour du joueur) sont sautées — sinon la feuille recouvre
* le terrain proche avec son échantillonnage grossier. Les cellules à
* cheval restent (anneau de recouvrement au raccord) ; pas de jupe sur
* les bords du trou (le terrain MC remplit derrière).
* Non couvert (accepté, cf. fable-idea F18) : passages/spine/chasms creusés (le heightfield pur ne
* les contient pas), diff layer — invisibles à distance de feuille, l'anneau MC proche les garde.
*/
FVoxelMeshData GenerateSheetMesh(FIntVector OriginVoxels, int32 StepXY, int32 CellsXY,
int32 StrateChunkZ,
int32 HoleMinXVox = INT32_MAX, int32 HoleMinYVox = INT32_MAX,
int32 HoleMaxXVox = INT32_MIN, int32 HoleMaxYVox = INT32_MIN);
//=========================================================================
// SERVICES (injectés par AVoxelWorld)
//=========================================================================
@@ -45,10 +45,6 @@ namespace VoxelPassageGeometry
// ownership band: the floor writer owns the plane, while the air writer owns the capsule
// volume above it even when two overlapping passage projections differ by a fraction.
constexpr float WalkableTunnelFloorAirClearanceVoxels = 0.5f;
// The graph-tunnel post owns the interior above its floor after terrain and passage-floor
// writers have run. The support-floor predicate is also an exclusion mask for the air post,
// so overlapping graph tubes cannot erase one another's walkable support band.
constexpr float CaveTunnelAirCoreInsetVoxels = 0.5f;
// A 15 degree ramp is comfortably below the 44 degree engine walkability ceiling. The
// latter is a scramble limit, not a sensible default for a tunnel the player walks through.
@@ -32,7 +32,6 @@ public:
int32 GetSeasonSeed() const { return SeasonSeed; }
float GetOriginSpineRadius() const { return OriginSpineRadius; }
float GetWorldRadiusVoxels() const { return WorldRadiusVoxels; }
const FString& GetManifestContentHash() const { return ManifestContentHash; }
/** Create the asset in Content Browser, choose a JSON file, then press this button. */
UFUNCTION(CallInEditor, Category = "Season")
@@ -134,10 +134,6 @@ struct VOXELFORGE_API FVoxelSeasonCompositionSettings
}
};
// Friendly aliases for callers that use the task's shorter naming.
using FVoxelSeasonComposerSettings = FVoxelSeasonCompositionSettings;
using FVoxelSeasonComposeSettings = FVoxelSeasonCompositionSettings;
/** Compact audit row; the full vectors live only for selected strates. */
struct VOXELFORGE_API FVoxelSeasonCandidateAudit
{
@@ -260,10 +256,6 @@ struct VOXELFORGE_API FVoxelSeasonManifest
}
};
/** Human-readable enum names used by JSON and the review page. */
VOXELFORGE_API const TCHAR* VF_GetVoxelSeasonSelectionReasonName(
EVoxelSeasonSelectionReason Reason);
/** Compose, validate, select, serialize, and (by default) write one season. Editor/build-box only. */
#if WITH_EDITOR
VOXELFORGE_API FVoxelSeasonManifest VF_ComposeSeason(
-40
View File
@@ -123,21 +123,6 @@ public:
return FMath::Clamp(FMath::Max(StrateContentCutMinLevel, 1), 1, 9);
}
// Open-world SKY reach: the sky-cap ceiling of an open strate (SurfaceWorld / FloatingIslands)
// is FAR, so the ceiling BAND is streamed across a wider horizontal radius = ViewDistanceXY ×
// this, so the sky reaches toward the horizon instead of being a patch over the player's head.
// Only the ceiling band (top CeilingBandChunks of the strate) gets the wide radius — the empty
// air below it stays at base XY. Cost grows ~ multiplier² (more far chunks/draws; gen is cheap
// and batching keeps the game-thread cost low). 1 = off. Live-tunable on the asset — push to 4
// if the draw budget allows; a proper to-the-horizon view (terrain too) is the chunk-LOD clipmap.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1"))
int32 CeilingViewMultiplier = 2;
// How many chunks down from the strate top get the wide CeilingViewMultiplier radius (covers
// the sky-cap slab). Bigger = no gaps if the ceiling surface dips, but more far chunks.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "1"))
int32 CeilingBandChunks = 4;
//=========================================================================
// CLIPMAP (chunked-LOD streaming — supersedes the ViewDistance box above)
//=========================================================================
@@ -183,25 +168,6 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "0"))
int32 RenderDistanceChunks = 768;
// F18 — the render-distance ring streams per-surface SHEETS instead of MC tiles: in an open
// strate the far field is exactly two heightfields (TerrainZ + sky-cap CeilSurf, both already
// computed per column), so each far tile becomes two displaced grids (ground polygroup 0 /
// cap polygroup 1 — same materials), ~3-6× cheaper to generate and far fewer components (one
// sheet spans 2^FarSheetSpanLevels MC-tile footprints per axis). Non-open strates produce
// empty sheets (their far ring was enclosed rock anyway). Carved features (passages, chasms,
// spine) don't show at sheet distance. Needs the strate band armed (StrateContentCutMinLevel
// active); in the inter-strate gap the sheet ring blanks until you land. The ship default is OFF:
// the ring stays real MC tiles at level MaxClipLevel (pre-F18 behaviour). Only matters when
// RenderDistanceChunks > 0.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
bool bFarSheetRing = false;
// Sheet tile size = MaxClipLevel + this many levels (2 → one sheet covers 4×4 MC-tile
// footprints → ~16× fewer far components). Sampling density stays that of the MaxClipLevel
// MC ring (cell count grows instead), capped at 128 cells/axis (beyond, cells coarsen).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap", meta = (ClampMin = "1", ClampMax = "4", EditCondition = "bFarSheetRing"))
int32 FarSheetSpanLevels = 2;
// SKIRTS — seal the thin cracks where neighbouring clipmap shells (different resolutions) meet.
// A short wall is extruded into the solid from each surface edge on the tile's outer faces.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Clipmap")
@@ -345,12 +311,6 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "5"))
int32 DensityVolumeLevels = 3;
// DEPRECATED / unused: the volume fill no longer runs on the shared UE::Tasks pool (where it
// starved behind mesh-gen). It now runs on ONE dedicated thread off the pool, so there's no task
// budget to cap. Kept only so existing saved assets don't error; safe to ignore.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Lighting", meta = (ClampMin = "1", ClampMax = "16"))
int32 DensityVolumeMaxTasks = 4;
// A fill box is split into Z-slabs of at most this many cells per task, so no single task is
// huge (a full level refill on startup/teleport fans out across workers). Lower = more, smaller
// tasks (better parallelism / latency); higher = fewer, fatter tasks.
@@ -48,15 +48,6 @@ public:
uint64 RetainedSamples = 0;
uint64 DroppedSamples = 0;
uint64 TotalFrames = 0;
// Kept for source compatibility with the earlier in-process summary. Raw sessions leave
// these zero; the offline summary owns all resolved-frame counts and tables.
uint64 ResolvedFrames = 0;
uint64 UnknownFrames = 0;
uint64 WaitLikeFrames = 0;
uint64 SamplesWithResolvedLeaf = 0;
uint64 SamplesWithWaitLikeFrame = 0;
uint64 UniqueProgramCounters = 0;
uint64 InlineSymbols = 0;
uint64 MaxActiveRegisteredThreads = 0;
uint64 ModuleCount = 0;
double RunSeconds = 0.0;
@@ -78,8 +69,6 @@ public:
/** Used by the module shutdown path as a final lifecycle backstop. */
void Shutdown();
bool IsRunning() const;
/** Called only by FScopedVoxelStackRegistration; cheap when disabled. */
static bool RegisterCurrentThread(int32 LODLevel = INDEX_NONE);
static void DeregisterCurrentThread();
+1 -1
View File
@@ -13,7 +13,7 @@ namespace VoxelForgeStartupTrace
int32 TileY = 0;
int32 TileZ = 0;
int32 Level = -1;
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path)
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (early path)
bool bSealedSolidProof = false;
bool bEmpty = true;
int32 Vertices = 0;
-4
View File
@@ -14,10 +14,6 @@ DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Skipped All Air"), STAT_VoxelForge
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Meshed"), STAT_VoxelForgeTilesMeshed, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Solid"), STAT_VoxelForgeTilesOpStackSolid, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Tiles Operator Stack Air"), STAT_VoxelForgeTilesOpStackAir, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Sole Slot"), STAT_VoxelForgeCaveBailNotOpStackSoleSlot, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Boundary Tile"), STAT_VoxelForgeCaveBailNotOpStackBoundaryTile, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack No Layout"), STAT_VoxelForgeCaveBailNotOpStackNoLayout, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave Bail Not Op Stack Recheck"), STAT_VoxelForgeCaveBailNotOpStackRecheck, STATGROUP_VoxelForge, VOXELFORGE_API);
DECLARE_DWORD_COUNTER_STAT_EXTERN(TEXT("Cave 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);
@@ -594,11 +594,6 @@ public:
const FString& PromotedStorePath = FString(),
const IVoxelStratePromotionVerifier* PromotionVerifier = nullptr);
/**
* Compatibility alias retained for callers compiled against the first Tier 4a pass. The
* settings argument is audited only for diagnostics; it is not a corpus input.
*/
bool LoadFromSettings(const UVoxelSettings* Settings, FString& OutReport);
bool LoadFromDefinitions(const TArray<UVoxelStrateDefinition*>& Definitions, FString& OutReport);
/** Add an already-resolved vector, primarily for offline tools and focused tests. */
@@ -625,7 +620,6 @@ public:
int32 Num() const { return Entries.Num(); }
const TArray<FVoxelStrateCorpusEntry>& GetEntries() const { return Entries; }
const TArray<FVoxelStrateFieldSpread>& GetFieldSpreads() const { return FieldSpreads; }
const TArray<FString>& GetSkippedDefinitions() const { return SkippedDefinitions; }
int32 NumForArchetype(ECaveGeneratorType Archetype) const;
int32 NumForProvenance(EVoxelStrateCorpusProvenance Provenance) const;
@@ -804,10 +798,6 @@ VOXELFORGE_API uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Re
VOXELFORGE_API bool VF_AreStrateStructureRecipesIdentical(
const FVoxelOpStackRecipe& A, const FVoxelOpStackRecipe& B);
/** The compact API requested by the composer design. */
VOXELFORGE_API FStrateGenerationParams VF_RollStrateParams(
const FVoxelStrateCorpus& Corpus, int32 Seed, int32 Index);
/** Bitwise comparison over exactly the fields in FStrateGenerationParams::Lerp. */
VOXELFORGE_API bool VF_AreStrateParamsBitIdentical(
const FStrateGenerationParams& A, const FStrateGenerationParams& B);
@@ -115,29 +115,6 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation")
ECaveGeneratorType GeneratorType = ECaveGeneratorType::TunnelNetwork;
/**
* OPERATOR STACK (experimental) — generate this strate through the composable density operator
* stack instead of the hardcoded archetype switch. Same world, different machinery.
*
* This is the A/B switch for `OPSTACK-PLAN §2.6`'s acceptance bar: flip it, regenerate, and
* judge on a screenshot that it is recognisably the same place. Both systems coexist
* indefinitely — the switch is not going away until every archetype is ported.
*
* ⚠️ ONLY `Maze` IS PORTED SO FAR. On any other GeneratorType this flag is ignored and the
* switch runs as before, so setting it is harmless but does nothing yet.
*
* ⚠️ Do NOT flip this on a strate mid-session and expect the old and new geometry to agree to
* the bit — they differ by ~1-2 ULP with ZERO isosurface crossings, so the shape is identical
* but the floats are not (`AUDIT-2026-07.md §C10`). Regenerate the world after changing it
* rather than letting old and new tiles sit side by side.
*
* Pile d'opérateurs (expérimental) : génère cette strate via la pile composable au lieu du
* `switch` d'archétype. Seul `Maze` est porté ; ailleurs le drapeau est ignoré.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Strate|Generation",
meta = (DisplayName = "Use Operator Stack (experimental)"))
bool bUseOperatorStack = false;
/**
* Authoritative per-strate worm switch. Existing assets default to enabled. The manager
* resolves a disabled strate to WormStrength == 0 before any fused, stack, composer, or
+5 -28
View File
@@ -249,15 +249,6 @@ public:
* live edit) can never serve a stale strate index or passage shortlist. */
uint32 GetLayoutVersion() const { return PassagesVersion; }
/**
* Fingerprint of the immutable density inputs used by the generator.
*
* Returns zero when the manager has dynamic inputs that this compact cache key intentionally
* does not serialize (seasons, composer recipes, biome maps, or terrain-op assets). Zero is a
* fail-closed answer: callers must skip verdict reuse rather than risk an old all-air proof.
*/
uint64 GetGenerationParamsFingerprint() const;
/** Diagnostic-only switch for isolating the inter-strate support writer. */
void SetPassageSupportFloorWritesEnabledForDiagnostics(bool bEnabled)
{
@@ -289,7 +280,7 @@ public:
* Get generation params for a chunk, with boundary blending.
*
* BLENDING CONCEPT:
* When a chunk is near a strate boundary (within BlendChunks of the edge),
* When a chunk is near a strate boundary (within the definition's TransitionBlendChunks of the edge),
* the params are lerped between the two adjacent strates. This prevents
* hard visual seams where one strate ends and another begins.
*
@@ -303,7 +294,7 @@ public:
*
* Returns TunnelNetwork if the chunk is outside all strates or if the
* strate definition is null. The generator uses this to pick which
* density function to call (GetDensityWithParams vs GetSlabDensity).
* operator stack to build for the chunk.
*
* @param ChunkCoord - The chunk position
* @return The ECaveGeneratorType for the strate containing this chunk
@@ -311,12 +302,9 @@ public:
ECaveGeneratorType GetGeneratorTypeForChunk(const FIntVector& ChunkCoord) const;
/**
* True when this chunk's strate opts into the density OPERATOR STACK instead of the hardcoded
* archetype switch (`UVoxelStrateDefinition::bUseOperatorStack`).
*
* Returns false for archetypes that have no port yet, so the flag can be set on any strate
* without changing its output until that archetype lands. Only `Maze` is ported today — this
* predicate is where that list grows, and it is deliberately the ONLY place it is written down.
* True when this chunk lies in a strate slot whose archetype has an operator stack (all eight
* do). False outside the layout or for a slot without a definition. This predicate is the ONLY
* place the archetype list is written down.
*/
bool UsesOperatorStackForChunk(const FIntVector& ChunkCoord) const;
@@ -419,14 +407,6 @@ public:
void ApplyPassageTunnelAir(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Re-assert landing air after MC-space disturbances, still before the final XY seal. */
void ApplyPassageLandingAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Re-assert walkable tunnel air after MC-space disturbances, still before the final XY seal. */
void ApplyPassageTunnelAirMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
/** Apply the landing/tunnel MC post in one cached passage scan after disturbances. */
void ApplyPassageStructuralPostsMC(float& Density, float WorldX, float WorldY, float WorldZ,
float BaseDensity, float SealThickness) const;
@@ -530,9 +510,6 @@ protected:
// allocator address reuse must not make a stale worker-local stack look current.
uint64 CacheLifetimeId = 0;
// How many chunks at strate boundaries are blended (transition zone)
int32 BlendChunks = 2;
// World seed (stored for passage generation)
int32 CachedSeed = 0;
@@ -510,23 +510,6 @@ VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnoseConnectivityWithSampler(
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings);
/**
* Player-fit connectivity over a custom stack. This is deliberately a separate API from the
* legacy air route: it refuses unless Settings.SampleStep == 1, resolves endpoints against the
* direct capsule-occupancy/floor mask, and flood-fills/routes only through player-fit cells.
*/
VOXELFORGE_API EVoxelConnectivityResult VF_ArePlayerFitConnectedWithSampler(
const IVoxelStrateDensitySampler& Sampler,
int32 StrateBottomWorldZ,
int32 StrateTopWorldZ,
float BoundarySealThickness,
const FVector& AVoxel,
const FVector& BVoxel,
const FVoxelStrateMeasureSettings& Settings,
bool& bOutStartSnapped,
bool& bOutGoalSnapped,
FVoxelConnectivityDiagnostics* OutDiagnostics = nullptr);
/** Full diagnostics variant for the player-fit custom-stack route. */
VOXELFORGE_API FVoxelConnectivityDiagnostics VF_DiagnosePlayerFitConnectivityWithSampler(
const IVoxelStrateDensitySampler& Sampler,
+3 -3
View File
@@ -207,7 +207,7 @@ enum class ECaveGeneratorType : uint8
* is defined on the UPPER strate's definition and controls the boundary between
* itself and the strate below it.
*
* Gradient: Smooth linear interpolation of generation params across BlendChunks.
* Gradient: Smooth linear interpolation of generation params across TransitionBlendChunks.
* This is the classic approach — no seam visible, but also no dramatic
* geological feature at the boundary. Best for similar-looking strates.
*
@@ -228,7 +228,7 @@ UENUM(BlueprintType)
enum class EVoxelStrateTransition : uint8
{
// Smooth linear interpolation of all generation params across the blend zone.
// No visible boundary — params change gradually over BlendChunks distance.
// No visible boundary — params change gradually over TransitionBlendChunks distance.
Gradient UMETA(DisplayName = "Gradient (smooth blend)"),
// No blending — params switch instantly at the boundary.
@@ -2256,7 +2256,7 @@ enum class ELandmarkAnchor : uint8
/**
* FStrateLandmark — a RARE, deliberately-placed object: mini-suns, ruins, shrines, monuments (§8.5, F7).
* The one placement primitive for "notable things you navigate by" (set-pieces folded in here 2026-07-06).
* The one placement primitive for "notable things you navigate by" (set-pieces included).
*
* This is the right primitive for things like the underground "mini-suns" (in-lore light sources): one
* object per ~`SpacingChunks` lattice cell, so the work scales with how MANY landmarks are in range
@@ -411,14 +411,4 @@ namespace VoxelGenLOD
Density = Before;
}
/** Compare a scalar proof input such as EvaluateModifierSDF without changing the field. */
FORCEINLINE void CompareSkippedPostValue(
float Candidate, float Canonical,
ETilePostComparisonKind Kind = ETilePostComparisonKind::Other)
{
if (bTilePostReachDebug && !bTilePostReachBypass)
{
RecordSkippedPostComparison(Kind, !SameFloatBits(Candidate, Canonical));
}
}
}
+4 -74
View File
@@ -203,15 +203,12 @@ namespace VoxelMath
// Pourquoi 32 ? Puissance de 2 → astuces bit à bit + bon alignement GPU.
// VOXEL_SIZE = 25 cm/voxel (Unreal travaille en centimètres). 1 chunk = 8 m.
//
// NOTE: 64³ a été essayé ("B", 2026-06-16) pour couper les draw calls (8× moins de
// chunks) mais le streaming devenait trop saccadé (briques 8× plus lourdes → applies
// + spawns de contenu en gros à-coups sur le game thread). Reverté à 32³ : le fps se
// règle côté RENDU (ombres off sur LOD lointain, voir ApplyMeshToChunk), pas via la
// taille de chunk. La taille de chunk reste le levier streaming-vs-draws si besoin.
// NOTE: 64³ coupe les draw calls (8× moins de chunks) mais rend le streaming trop
// saccadé (briques 8× plus lourdes → applies + spawns de contenu en gros à-coups sur
// le game thread). Le fps se règle côté RENDU (ombres off sur LOD lointain, voir
// ApplyMeshToChunk), pas via la taille de chunk. La taille de chunk reste le levier streaming-vs-draws si besoin.
constexpr int32 CHUNK_SIZE = 32;
constexpr int32 CHUNK_SIZE_SQUARED = CHUNK_SIZE * CHUNK_SIZE; // 1024
constexpr int32 CHUNK_VOLUME = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE; // 32768
constexpr float VOXEL_SIZE = 25.0f;
@@ -276,26 +273,6 @@ enum class EVoxelFace : uint8
NegativeZ, // Down (-Z)
};
inline FIntVector GetFaceDirection(EVoxelFace Face)
{
switch (Face)
{
case EVoxelFace::PositiveX: return FIntVector( 1, 0, 0);
case EVoxelFace::NegativeX: return FIntVector(-1, 0, 0);
case EVoxelFace::PositiveY: return FIntVector( 0, 1, 0);
case EVoxelFace::NegativeY: return FIntVector( 0, -1, 0);
case EVoxelFace::PositiveZ: return FIntVector( 0, 0, 1);
case EVoxelFace::NegativeZ: return FIntVector( 0, 0, -1);
default: return FIntVector( 0, 0, 0);
}
}
inline FVector GetFaceNormal(EVoxelFace Face)
{
const FIntVector Dir = GetFaceDirection(Face);
return FVector(Dir.X, Dir.Y, Dir.Z);
}
//=============================================================================
// COORDINATE CONVERSION
//=============================================================================
@@ -319,53 +296,6 @@ inline FIntVector WorldToChunkCoord(const FVector& WorldPos)
);
}
inline FIntVector WorldToLocalCoord(const FVector& WorldPos)
{
// ((x % n) + n) % n → modulo positif même pour x négatif.
// (-5 % 32) = -5 en C++, mais on veut 27.
return FIntVector(
((FMath::FloorToInt(WorldPos.X / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
((FMath::FloorToInt(WorldPos.Y / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE,
((FMath::FloorToInt(WorldPos.Z / VOXEL_SIZE) % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE
);
}
inline FVector ChunkToWorldPos(const FIntVector& ChunkCoord)
{
return FVector(
ChunkCoord.X * CHUNK_SIZE * VOXEL_SIZE,
ChunkCoord.Y * CHUNK_SIZE * VOXEL_SIZE,
ChunkCoord.Z * CHUNK_SIZE * VOXEL_SIZE
);
}
// Index 3D → 1D pour un tableau plat (voir doc CLAUDE.md: "x + y*SizeX + z*SizeX*SizeY").
inline int32 LocalToIndex(int32 X, int32 Y, int32 Z)
{
return X + (Y * CHUNK_SIZE) + (Z * CHUNK_SIZE_SQUARED);
}
inline FIntVector IndexToLocal(int32 Index)
{
return FIntVector(
Index % CHUNK_SIZE,
(Index / CHUNK_SIZE) % CHUNK_SIZE,
Index / CHUNK_SIZE_SQUARED
);
}
inline bool IsValidLocalCoord(int32 X, int32 Y, int32 Z)
{
return X >= 0 && X < CHUNK_SIZE
&& Y >= 0 && Y < CHUNK_SIZE
&& Z >= 0 && Z < CHUNK_SIZE;
}
inline bool IsValidLocalCoord(const FIntVector& Coord)
{
return IsValidLocalCoord(Coord.X, Coord.Y, Coord.Z);
}
//=============================================================================
// MATH HELPERS (partagés entre générateur, morphologie, manager)
//=============================================================================
+5 -18
View File
@@ -160,7 +160,6 @@ struct FVoxelTileHashDumpRecord
int32 Cells = 0;
int32 BandChunkLo = MIN_int32;
int32 BandChunkHi = MAX_int32;
bool bSheetTile = false;
bool bEmpty = true;
int32 NumTriangles = 0;
FString GeometryHash;
@@ -567,7 +566,7 @@ private:
void LogReadyTransitionVisibilityMeasure();
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, const FVoxelMeshData& MeshData);
const FVoxelMeshData& MeshData);
void WriteTileHashDump();
// Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector
@@ -761,7 +760,7 @@ public:
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon);
/**
* Worker-side gen for one tile: classify → GenerateMesh/GenerateSheetMesh → BuildTileStreamSet.
* Worker-side gen for one tile: classify → GenerateMesh → BuildTileStreamSet.
* Fills Result (no enqueue, no bookkeeping). Called from the async ChunkGen task AND from the
* synchronous carve path (SyncRemeshTile) — reads Generator/Mesher only, so it's safe on either
* thread. See LoadTile for how the parameters are derived.
@@ -769,8 +768,6 @@ public:
void GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, int32 SheetChunkZ,
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
FChunkResult& Result,
const std::atomic<bool>* ObsoleteFlag = nullptr);
@@ -933,15 +930,6 @@ public:
// result cannot publish and the post-carve result is admitted immediately.
TSet<FVoxelTileKey> DirtyRemeshQueue;
// F18 — TROU XY de l'anneau feuille (voxels ; Max EXCLUSIF ; sentinelles MAX/MIN = pas de
// trou) : la zone couverte par les coquilles MC (boîte niveau-MaxClipLevel autour du joueur,
// rétrécie d'une tuile pour garder un anneau de recouvrement au raccord) est DÉCOUPÉE des
// feuilles — sinon une feuille partiellement couverte recouvre le terrain proche avec son
// échantillonnage grossier. Mis à jour par crossing ; changement ⇒ re-queue des feuilles
// chevauchantes via BandRemeshQueue (re-gen en place).
int32 SheetHoleMinXVox = MAX_int32, SheetHoleMinYVox = MAX_int32;
int32 SheetHoleMaxXVox = MIN_int32, SheetHoleMaxYVox = MIN_int32;
// Set to true during EndPlay — async tasks check this before accessing UObjects
std::atomic<bool> bShuttingDown{false};
@@ -1018,8 +1006,7 @@ public:
TArray<FReadyTransitionVisibilitySample> ReadyTransitionVisibilitySamples;
TArray<FFarFieldSubmitSample> FarFieldSubmitSamples;
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level; sheet tiles
// above the configured MC range never enter the outer classifier.
// Clean performance accounting. LOD 0..8 covers every marching-cubes clip level.
static constexpr int32 TrackedClassifierLODCount = 9;
std::atomic<uint64> TotalWorkerGenerationCycles{0};
std::atomic<uint64> TotalWorkerGenerationTasks{0};
@@ -1259,8 +1246,8 @@ public:
// Desired-set membership STAMPÉE : clé → numéro du dernier crossing où la tuile était désirée.
// BuildDesiredTiles upserte le stamp courant puis balaie la map UNE fois : les entrées à stamp
// périmé sont les "leavers" (retirées + renvoyées). Le cull ne considère que ces leavers + la
// TransitionHold — fini le scan O(toutes-les-tuiles-chargées) à chaque crossing (le spike
// CullTiles ~1.6 ms/crossing de la trace 2026-07-05).
// TransitionHold — pas de scan O(toutes-les-tuiles-chargées) à chaque crossing (un tel scan
// coûtait ~1.6 ms/crossing dans CullTiles).
TMap<FVoxelTileKey, uint32> DesiredStamped;
uint32 DesiredStamp = 0;
bool IsDesired(const FVoxelTileKey& T) const
@@ -17,14 +17,6 @@ namespace VoxelWormField
};
/** Evaluate the exact two-noise field, retaining the N1 threshold short-circuit. */
VOXELFORGE_API float EvaluateExact(
float WorldX,
float WorldY,
float WorldZ,
float Threshold,
const FParameters& Parameters);
/** Evaluate the exact two-noise field. */
VOXELFORGE_API float Evaluate(
float WorldX,
float WorldY,
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff. Show more