diff --git a/.gitignore b/.gitignore index 782bb75..4c5a6e0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4eca0ae..af2a173 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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` (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 — diff --git a/CLAUDE.md b/CLAUDE.md index a3998c2..c19c974 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/CODEMAP.md b/CODEMAP.md index 77fafd4..06cb52a 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -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=`, 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=` 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. | --- diff --git a/COMPOSER-NOTES.md b/COMPOSER-NOTES.md index 0f84e3f..714360e 100644 --- a/COMPOSER-NOTES.md +++ b/COMPOSER-NOTES.md @@ -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 diff --git a/AUDIT-2026-07.md b/Docs/archive/AUDIT-2026-07.md similarity index 100% rename from AUDIT-2026-07.md rename to Docs/archive/AUDIT-2026-07.md diff --git a/AUDIT-2026-08-CODEX.md b/Docs/archive/AUDIT-2026-08-CODEX.md similarity index 100% rename from AUDIT-2026-08-CODEX.md rename to Docs/archive/AUDIT-2026-08-CODEX.md diff --git a/AUDIT-COLD-2026-09-14.md b/Docs/archive/AUDIT-COLD-2026-09-14.md similarity index 100% rename from AUDIT-COLD-2026-09-14.md rename to Docs/archive/AUDIT-COLD-2026-09-14.md diff --git a/AUDIT-SOL-2026-09-11.md b/Docs/archive/AUDIT-SOL-2026-09-11.md similarity index 100% rename from AUDIT-SOL-2026-09-11.md rename to Docs/archive/AUDIT-SOL-2026-09-11.md diff --git a/CODEX-TASK-001-tile-skip-stats.md b/Docs/archive/CODEX-TASK-001-tile-skip-stats.md similarity index 100% rename from CODEX-TASK-001-tile-skip-stats.md rename to Docs/archive/CODEX-TASK-001-tile-skip-stats.md diff --git a/CODEX-TASK-002-column-memo-thrash.md b/Docs/archive/CODEX-TASK-002-column-memo-thrash.md similarity index 100% rename from CODEX-TASK-002-column-memo-thrash.md rename to Docs/archive/CODEX-TASK-002-column-memo-thrash.md diff --git a/CODEX-TASK-003-extrareach-perlin-bound.md b/Docs/archive/CODEX-TASK-003-extrareach-perlin-bound.md similarity index 100% rename from CODEX-TASK-003-extrareach-perlin-bound.md rename to Docs/archive/CODEX-TASK-003-extrareach-perlin-bound.md diff --git a/CODEX-TASK-004-minmax-radius-envelope.md b/Docs/archive/CODEX-TASK-004-minmax-radius-envelope.md similarity index 100% rename from CODEX-TASK-004-minmax-radius-envelope.md rename to Docs/archive/CODEX-TASK-004-minmax-radius-envelope.md diff --git a/CODEX-TASK-005-tile-scan-sampler-extent.md b/Docs/archive/CODEX-TASK-005-tile-scan-sampler-extent.md similarity index 100% rename from CODEX-TASK-005-tile-scan-sampler-extent.md rename to Docs/archive/CODEX-TASK-005-tile-scan-sampler-extent.md diff --git a/CODEX-TASK-006-cave-morphology-radius-envelope.md b/Docs/archive/CODEX-TASK-006-cave-morphology-radius-envelope.md similarity index 100% rename from CODEX-TASK-006-cave-morphology-radius-envelope.md rename to Docs/archive/CODEX-TASK-006-cave-morphology-radius-envelope.md diff --git a/CODEX-TASK-007-generation-pause.md b/Docs/archive/CODEX-TASK-007-generation-pause.md similarity index 100% rename from CODEX-TASK-007-generation-pause.md rename to Docs/archive/CODEX-TASK-007-generation-pause.md diff --git a/CODEX-TASK-008-column-memo-and-t1d-diagnosis.md b/Docs/archive/CODEX-TASK-008-column-memo-and-t1d-diagnosis.md similarity index 100% rename from CODEX-TASK-008-column-memo-and-t1d-diagnosis.md rename to Docs/archive/CODEX-TASK-008-column-memo-and-t1d-diagnosis.md diff --git a/CODEX-TASK-1-PROFILER-BATCH-RESEARCH.md b/Docs/archive/CODEX-TASK-1-PROFILER-BATCH-RESEARCH.md similarity index 100% rename from CODEX-TASK-1-PROFILER-BATCH-RESEARCH.md rename to Docs/archive/CODEX-TASK-1-PROFILER-BATCH-RESEARCH.md diff --git a/CODEX-TASK-2-PERF-REPORT.md b/Docs/archive/CODEX-TASK-2-PERF-REPORT.md similarity index 100% rename from CODEX-TASK-2-PERF-REPORT.md rename to Docs/archive/CODEX-TASK-2-PERF-REPORT.md diff --git a/DESIGN-SOL-2026-09-15-CODE.md b/Docs/archive/DESIGN-SOL-2026-09-15-CODE.md similarity index 100% rename from DESIGN-SOL-2026-09-15-CODE.md rename to Docs/archive/DESIGN-SOL-2026-09-15-CODE.md diff --git a/DESIGN-SOL-2026-09-15.md b/Docs/archive/DESIGN-SOL-2026-09-15.md similarity index 100% rename from DESIGN-SOL-2026-09-15.md rename to Docs/archive/DESIGN-SOL-2026-09-15.md diff --git a/INVESTIGATION-2026-08-SOL.md b/Docs/archive/INVESTIGATION-2026-08-SOL.md similarity index 100% rename from INVESTIGATION-2026-08-SOL.md rename to Docs/archive/INVESTIGATION-2026-08-SOL.md diff --git a/OPSTACK-DECOMPOSITION.md b/Docs/archive/OPSTACK-DECOMPOSITION.md similarity index 100% rename from OPSTACK-DECOMPOSITION.md rename to Docs/archive/OPSTACK-DECOMPOSITION.md diff --git a/OPSTACK-HANDOFF.md b/Docs/archive/OPSTACK-HANDOFF.md similarity index 100% rename from OPSTACK-HANDOFF.md rename to Docs/archive/OPSTACK-HANDOFF.md diff --git a/OPSTACK-PLAN.md b/Docs/archive/OPSTACK-PLAN.md similarity index 100% rename from OPSTACK-PLAN.md rename to Docs/archive/OPSTACK-PLAN.md diff --git a/OPSTACK-PROGRESS.md b/Docs/archive/OPSTACK-PROGRESS.md similarity index 100% rename from OPSTACK-PROGRESS.md rename to Docs/archive/OPSTACK-PROGRESS.md diff --git a/OPSTACK-PROMPT.md b/Docs/archive/OPSTACK-PROMPT.md similarity index 100% rename from OPSTACK-PROMPT.md rename to Docs/archive/OPSTACK-PROMPT.md diff --git a/PERF-ATTRIBUTION.md b/Docs/archive/PERF-ATTRIBUTION.md similarity index 100% rename from PERF-ATTRIBUTION.md rename to Docs/archive/PERF-ATTRIBUTION.md diff --git a/PERF-SAMPLED.md b/Docs/archive/PERF-SAMPLED.md similarity index 100% rename from PERF-SAMPLED.md rename to Docs/archive/PERF-SAMPLED.md diff --git a/WORK-NEXT.md b/Docs/archive/WORK-NEXT.md similarity index 100% rename from WORK-NEXT.md rename to Docs/archive/WORK-NEXT.md diff --git a/REVIEW_FINDINGS.md b/REVIEW_FINDINGS.md index 309e294..ae5795e 100644 --- a/REVIEW_FINDINGS.md +++ b/REVIEW_FINDINGS.md @@ -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) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp index 65a5d54..ef18c92 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeClassifyTileTest.cpp @@ -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; } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeClipmapNoOverlapTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeClipmapNoOverlapTest.cpp index 0d02b01..b5c54e6 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeClipmapNoOverlapTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeClipmapNoOverlapTest.cpp @@ -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); } } } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp index 00e9994..53dea07 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerCorpusFreeTest.cpp @@ -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()); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerParameterRollTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerParameterRollTest.cpp index e1c42b5..6696d3b 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerParameterRollTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerParameterRollTest.cpp @@ -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,123 +1341,109 @@ 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); + if (!World.IsValid()) { - FTestWorld World; - World.Build(1337, 2, bUseOperatorStack); - if (!World.IsValid()) + AddError(World.WhyInvalid()); + return false; + } + + UVoxelStrateDefinition* Definition = + World.Definitions[FTestWorld::SlotTunnelNetwork].Get(); + if (Definition == nullptr) + { + AddError(TEXT("The fixture has no TunnelNetwork definition for the liveness experiment.")); + return false; + } + Definition->TerrainOperations.Reset(); + TestEqual(TEXT("fixture has no terrain-op asset pool"), Definition->TerrainOperations.Num(), 0); + + TArray Points; + VF_BuildTerrainDetailLattice(World, Points); + TestEqual(TEXT("experiment uses the fixed lattice"), Points.Num(), ExpectedLatticeSamples); + + FStrateGenerationParams BaselineParams = Definition->GenerationParams; + VF_ClearTerrainDetailFields(BaselineParams); + TArray FirstBaseline; + bool bHaveFirstBaseline = false; + + for (const FTerrainDetailProbeSpec& Spec : GTerrainDetailProbeSpecs) + { + Definition->GenerationParams = BaselineParams; + World.Reinitialize(); + + TArray BaselineSamples; + const FTerrainDetailDensitySummary BaselineSummary = + VF_SampleTerrainDetailLattice(World, Points, BaselineSamples); + if (!bHaveFirstBaseline) { - AddError(World.WhyInvalid()); - return false; + FirstBaseline = BaselineSamples; + bHaveFirstBaseline = true; } - - UVoxelStrateDefinition* Definition = - World.Definitions[FTestWorld::SlotTunnelNetwork].Get(); - if (Definition == nullptr) + else { - AddError(TEXT("The fixture has no TunnelNetwork definition for the liveness experiment.")); - 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); - - TArray Points; - VF_BuildTerrainDetailLattice(World, Points); - TestEqual( - FString::Printf(TEXT("%s experiment uses the fixed lattice"), - bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy")), - Points.Num(), ExpectedLatticeSamples); - - FStrateGenerationParams BaselineParams = Definition->GenerationParams; - VF_ClearTerrainDetailFields(BaselineParams); - TArray FirstBaseline; - bool bHaveFirstBaseline = false; - - for (const FTerrainDetailProbeSpec& Spec : GTerrainDetailProbeSpecs) - { - Definition->GenerationParams = BaselineParams; - World.Reinitialize(); - - TArray BaselineSamples; - const FTerrainDetailDensitySummary BaselineSummary = - VF_SampleTerrainDetailLattice(World, Points, BaselineSamples); - if (!bHaveFirstBaseline) + bool bSameBaseline = FirstBaseline.Num() == BaselineSamples.Num(); + for (int32 SampleIndex = 0; bSameBaseline && SampleIndex < FirstBaseline.Num(); ++SampleIndex) { - FirstBaseline = BaselineSamples; - bHaveFirstBaseline = true; - } - else - { - bool bSameBaseline = FirstBaseline.Num() == BaselineSamples.Num(); - for (int32 SampleIndex = 0; bSameBaseline && SampleIndex < FirstBaseline.Num(); ++SampleIndex) - { - 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), - bSameBaseline); - } - - FStrateGenerationParams VariantParams = BaselineParams; - VF_SetTerrainDetailProbe(VariantParams, Spec.Group); - Definition->GenerationParams = VariantParams; - World.Reinitialize(); - - TArray VariantSamples; - const FTerrainDetailDensitySummary VariantSummary = - VF_SampleTerrainDetailLattice(World, Points, VariantSamples); - const FTerrainDetailDensityDelta Delta = - VF_CompareTerrainDetailSamples(BaselineSamples, VariantSamples); - - const bool bObservedLive = Delta.Changed > 0; - const bool bVerdictMatches = bObservedLive == Spec.bExpectedLive; - ++TotalRows; - if (!bVerdictMatches) - { - ++FailedRows; + bSameBaseline = BitEqual(FirstBaseline[SampleIndex], BaselineSamples[SampleIndex]); } TestTrue( - FString::Printf(TEXT("%s %s field group has the measured liveness verdict"), - bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"), - 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"), - Spec.Name, - Points.Num(), - BaselineSummary.Digest, - VariantSummary.Digest, - BaselineSummary.Mean, - VariantSummary.Mean, - Delta.Changed, - Delta.SignChanged, - Delta.SumAbs, - Delta.MaxAbs, - bObservedLive ? TEXT("LIVE") : TEXT("DEAD")); + FString::Printf(TEXT("baseline is stable before %s probe"), Spec.Name), + bSameBaseline); } + + FStrateGenerationParams VariantParams = BaselineParams; + VF_SetTerrainDetailProbe(VariantParams, Spec.Group); + Definition->GenerationParams = VariantParams; + World.Reinitialize(); + + TArray VariantSamples; + const FTerrainDetailDensitySummary VariantSummary = + VF_SampleTerrainDetailLattice(World, Points, VariantSamples); + const FTerrainDetailDensityDelta Delta = + VF_CompareTerrainDetailSamples(BaselineSamples, VariantSamples); + + const bool bObservedLive = Delta.Changed > 0; + const bool bVerdictMatches = bObservedLive == Spec.bExpectedLive; + ++TotalRows; + if (!bVerdictMatches) + { + ++FailedRows; + } + TestTrue( + FString::Printf(TEXT("%s field group has the measured liveness verdict"), Spec.Name), + bVerdictMatches); + + Table += FString::Printf( + TEXT("%s | %d | 0x%08x | 0x%08x | %.9g | %.9g | %d | %d | %.9g | %.9g | %s\n"), + Spec.Name, + Points.Num(), + BaselineSummary.Digest, + VariantSummary.Digest, + BaselineSummary.Mean, + VariantSummary.Mean, + Delta.Changed, + Delta.SignChanged, + Delta.SumAbs, + 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(2 * UE_ARRAY_COUNT(GTerrainDetailProbeSpecs))); + TestEqual(TEXT("every terrain detail group was measured"), TotalRows, + static_cast(UE_ARRAY_COUNT(GTerrainDetailProbeSpecs))); return FailedRows == 0; } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerPromotionTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerPromotionTest.cpp index 73b354c..f64757f 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerPromotionTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerPromotionTest.cpp @@ -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()); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp index 6135e36..a3f8ff1 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerShowcaseTest.cpp @@ -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()); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeComposerStructureRollTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeComposerStructureRollTest.cpp index 5ec9544..994cf90 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeComposerStructureRollTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeComposerStructureRollTest.cpp @@ -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()); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeCorrectnessRoundTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeCorrectnessRoundTest.cpp index b3a3986..cc5f5cb 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeCorrectnessRoundTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeCorrectnessRoundTest.cpp @@ -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() diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeDensityBlockEquivalenceTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeDensityBlockEquivalenceTest.cpp index 92c9e41..2b780a7 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeDensityBlockEquivalenceTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeDensityBlockEquivalenceTest.cpp @@ -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( diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeDensityPurityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeDensityPurityTest.cpp index d3a303e..354446a 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeDensityPurityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeDensityPurityTest.cpp @@ -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())); } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp index 380c252..16f3d4f 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeDescentCorridorTest.cpp @@ -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, diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp index 94836ee..ae9b953 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeHeightStackTest.cpp @@ -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 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); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeLateralRegionsTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeLateralRegionsTest.cpp index 21695e3..01c5ecd 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeLateralRegionsTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeLateralRegionsTest.cpp @@ -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; diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp index 86e8819..f6d432d 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeMazeSeamTest.cpp @@ -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 LegacyProbes; - TArray 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 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()); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackChannelTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackChannelTest.cpp index 2d905a8..95ddc21 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackChannelTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackChannelTest.cpp @@ -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)) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackIslandTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackIslandTest.cpp index 072476c..402726f 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackIslandTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackIslandTest.cpp @@ -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 ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; } + if (bInterior && New < 0.0f) { ++NumInsideIsland; } // solide loin des seals ⇒ une île + if (bInterior && New >= 0.0f) { ++NumOpenVoid; } } - - 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); + AddInfo(FString::Printf( + 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 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)); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp index ca8d713..a9ed67e 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp @@ -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.")); } } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackShaftTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackShaftTest.cpp index e87a124..753ed43 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackShaftTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackShaftTest.cpp @@ -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; } - } - if ((Old >= 0.0f) != (New >= 0.0f)) { ++NumSideDisagree; } + // air ⇒ dans un puits/connecteur/étagère / air ⇒ inside a shaft, connector or ledge + if (EvalStackMCForTest(Stack, X, Y, Z) >= 0.0f) { ++NumInsideShaft; } } - - 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(CellX) + 0.37f) * PerfSpacing; LastY = (static_cast(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; diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp index e8e055a..774c16e 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackSlabTest.cpp @@ -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)); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp index 8201f2b..cd864a1 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgePassageOpenSpaceTest.cpp @@ -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 SourceOnlyGenerator( - NewObject(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; } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp index 4a9a64e..143f3d4 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeScaleDiagnosisTest.cpp @@ -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()); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeSealedSolidProofTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeSealedSolidProofTest.cpp index abed1f4..a8b8392 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeSealedSolidProofTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeSealedSolidProofTest.cpp @@ -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()); diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp index 707aa5b..7929d5b 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeStrateConnectivityTest.cpp @@ -1325,54 +1325,38 @@ 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); + if (!World.IsValid()) { - FTestWorld World; - World.Build(/*InSeed=*/1337, /*InGapChunks=*/2, bUseOperatorStack); - 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; - } - - int32 Checked = 0; - int32 Mismatches = 0; - 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; - } - - Summary += FString::Printf( - TEXT(" %s path: %d same-position re-evaluations, %d bit mismatches%s%s.\n"), - bUseOperatorStack ? TEXT("operator-stack") : TEXT("legacy"), - Checked, - Mismatches, - Mismatches > 0 ? TEXT("; first ") : 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"), - *FirstMismatch)); - bAllChecksPassed = false; - } + AddError(TEXT("HARD FAILURE: could not build the seam-test fixture.")); + return false; } - AddInfo(Summary); - return bAllChecksPassed; + int32 Checked = 0; + int32 Mismatches = 0; + FString FirstMismatch; + if (!CheckVerticalShaftSeams(World, Checked, Mismatches, FirstMismatch)) + { + AddError(TEXT("HARD FAILURE: the seam test sampled no VerticalShafts positions.")); + return false; + } + + 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(""))); + if (Mismatches != 0) + { + AddError(FString::Printf( + TEXT("SEAM: VerticalShafts density changed after different chunk-context orders: %s"), + *FirstMismatch)); + return false; + } + return true; } bool FVoxelForgeStrateConnectivityTest::RunTest(const FString& Parameters) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h b/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h index 65b9295..1a83d0d 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h +++ b/Source/VoxelForge/Private/Tests/VoxelForgeTestFixture.h @@ -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( NewObject(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& OutPoints) diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeTilePostReachTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeTilePostReachTest.cpp index d909cf2..134b5ca 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeTilePostReachTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeTilePostReachTest.cpp @@ -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 NativeOrigins; - TArray 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 Origins; + AppendWorldReachTiles(World, Origins); + TestTrue(TEXT("the representative tile set is non-empty"), Origins.Num() > 0); - TStrongObjectPtr NativeMesher( + TStrongObjectPtr Mesher( NewObject(GetTransientPackage(), NAME_None, RF_Transient)); - TStrongObjectPtr FusedMesher( - NewObject(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(NativeFull.Tiles), - static_cast(FusedFull.Tiles), - static_cast(FullFinalComparisons), - static_cast(FullFinalDifferences), - static_cast(ShrunkFinalComparisons), - static_cast(ShrunkFinalDifferences), - static_cast(NativeFull.DensitySamples), - static_cast(NativeShrunk.DensitySamples), - static_cast(FusedFull.DensitySamples), - static_cast(FusedShrunk.DensitySamples))); + TEXT("tile_post_reach_proof: tiles=%llu full_final=%llu/%llu shrunk_final=%llu/%llu " + "samples=%lld/%lld."), + static_cast(Full.Tiles), + static_cast(Full.FinalComparisons), + static_cast(Full.FinalDifferences), + static_cast(Shrunk.FinalComparisons), + static_cast(Shrunk.FinalDifferences), + static_cast(Full.DensitySamples), + static_cast(Shrunk.DensitySamples))); return true; } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeVerticalStrateReachTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeVerticalStrateReachTest.cpp index 9815bac..5eac2a3 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeVerticalStrateReachTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeVerticalStrateReachTest.cpp @@ -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 Desired; VoxelClipmapDesiredTiles::Build( FIntVector(0, 0, PlayerChunkZ), MinChunkZ, MaxChunkZ, Clipmap, Desired); TArray 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(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; } diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeWorldEdgeSealTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeWorldEdgeSealTest.cpp index d5e8d22..8b80dcd 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeWorldEdgeSealTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeWorldEdgeSealTest.cpp @@ -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") diff --git a/Source/VoxelForge/Private/VoxelAtmosphereManager.cpp b/Source/VoxelForge/Private/VoxelAtmosphereManager.cpp index 28d7d4b..7e95bbd 100644 --- a/Source/VoxelForge/Private/VoxelAtmosphereManager.cpp +++ b/Source/VoxelForge/Private/VoxelAtmosphereManager.cpp @@ -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); diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index 7c8cea0..fe11ac0 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -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(Position.X), static_cast(Position.Y)); - const TArray& ControlPoints = bWorldChain - ? Tunnel.WorldControlPoints : Tunnel.ControlPoints; - const TArray& 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* 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 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> 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(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(Tunnel.WorldBoundCenter.X); - const float DY = WorldY - static_cast(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(Tunnel.WorldBoundCenter.Z) - HalfZ); - MaxZ = FMath::Min(MaxZ, static_cast(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(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 - ); -} diff --git a/Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h b/Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h index c3b2876..6cf9c2f 100644 --- a/Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h +++ b/Source/VoxelForge/Private/VoxelClipmapDesiredTiles.h @@ -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); - } - } - } } } diff --git a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp index 0a66e80..b028912 100644 --- a/Source/VoxelForge/Private/VoxelDensityOpStack.cpp +++ b/Source/VoxelForge/Private/VoxelDensityOpStack.cpp @@ -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(), 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 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(-BaseDensity); } @@ -9463,18 +9345,13 @@ namespace VoxelDensityOps return MakeUnique(P, Seed); } - TUniquePtr MakeSurfaceColumnSource(const FSurfaceGenerationParams& P, int32 Seed) - { - return MakeUnique(P, Seed); - } - void BuildSurfaceStack(FVoxelOpStack& OutStack, const FSurfaceGenerationParams& P, int32 Seed, float SpineRadius, const UVoxelStrateManager* StrateManager, const TArray& PerBiomeParams, TUniquePtr 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 ColumnSource = MakeUnique(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 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(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(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)); diff --git a/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Source/VoxelForge/Private/VoxelDensityProfile.cpp index d151dce..c01c3e2 100644 --- a/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -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) diff --git a/Source/VoxelForge/Private/VoxelForgeModule.cpp b/Source/VoxelForge/Private/VoxelForgeModule.cpp index e341c6c..9d54985 100644 --- a/Source/VoxelForge/Private/VoxelForgeModule.cpp +++ b/Source/VoxelForge/Private/VoxelForgeModule.cpp @@ -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!")); diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index adcda2c..e9d7c54 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -16,7 +16,7 @@ #include "VoxelWormField.h" #include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements #include "VoxelDensityPrimitives.h" // spine / seals / passage — shared with the operator stack -#include "VoxelDensityOpStack.h" // OPSTACK Phase 1: the opt-in per-strate operator stack +#include "VoxelDensityOpStack.h" // the per-strate operator stack #include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it #include "VoxelStats.h" #include "VoxelDensityProfile.h" @@ -264,13 +264,12 @@ private: }; //============================================================================= -// SURFACE COLUMN CACHE (T1.a) — kill the per-Z heightfield redundancy +// SURFACE COLUMN CACHE — ClassifyTile's SurfaceWorld column proof (T1.d) //============================================================================= -// The SurfaceWorld heightfield + sky-cap + biome blend are functions of XY ONLY, but -// the mesher samples ~33 Z grid-points per column, each re-running that XY work. We cache -// the column (terrain Z + ceiling Z) once per integer XY and reuse it down the column. -// Used ONLY for integer-XY queries (the density grid); fractional queries (gradient -// normals at interpolated vertices) fall through and compute directly → bit-identical. +// The SurfaceWorld heightfield + sky-cap + biome blend are functions of XY ONLY, but a tile +// spans many Z grid-points per column. ClassifyTile caches the column (terrain Z + ceiling Z) +// once per integer XY and reuses it down the column. (The surface density stack keeps its own +// column memo in FSurfaceColumnSource.) // The surface heightfield (TerrainZ + sky-cap CeilSurf) is a PURE function of (worldX, worldY, // seed, strate) — ZERO Z dependence: the climate/Voronoi fields are pure-XY (see ResolveBiomeSampleAt: // "result is the pure function of XY either way") and surface params are constant within a strate (Hard @@ -283,7 +282,7 @@ private: // F20 phase 2 OVERHANG, resolved once per column (biome-blended, slope-gated) so the per-voxel // density path is cheap: OverhangAmp = strength · slope-gate (0 ⇒ no overhang here); (DirX,DirY) = // the UNIT UPHILL direction of the terrain gradient (the lip extends downhill, borrowing rock from -// uphill). See SurfaceDensityFromColumn for the warped-terrain union that makes the shelf. +// uphill). See FOverhangShelfMod for the warped-terrain union that makes the shelf. struct FSurfaceColumn { // Toutes les sorties sont écrites par ComputeSurfaceColumn avant Computed=true ; les valeurs @@ -362,9 +361,9 @@ struct FSurfaceColumnCache } }; -// Cache de colonnes PARTAGÉ par thread : GetDensityAt (hot path, T1.a) + ClassifyTile (T1.d). -// Les deux stockent des valeurs bit-identiques (même ComputeSurfaceColumn, champs purs en XY), -// donc un ClassifyTile qui rend Mixed laisse ses colonnes chaudes pour le GenerateMesh qui suit. +// Cache de colonnes par thread de ClassifyTile (T1.d) : les pré-passes et passes complètes +// successives d'une même tuile, et les tuiles voisines, réutilisent leurs colonnes. +// Per-thread column cache for ClassifyTile: successive passes and neighbouring tiles reuse columns. static thread_local FSurfaceColumnCache GSurfColCache; //============================================================================= @@ -669,9 +668,9 @@ struct FVoxelDensityBlockSession return; } - // Allocate lazily on the first operator-stack sample. Legacy archetypes still use the - // ordinary scalar generator and should not pay for a block scratch lattice merely because - // the mesher opened the scoped hand-off. + // Allocate lazily on the first operator-stack sample. Chunks that never reach a stack + // (gaps, out-of-layout, degenerate strates) use the ordinary scalar path and should not + // pay for a block scratch lattice merely because the mesher opened the scoped hand-off. BlockScratch.Reset(); } @@ -969,12 +968,11 @@ static float RidgedNoise3D(const FVector3f& Position, int32 Octaves = 4, // Using F2-F1 (difference of two closest distances) gives smooth cell // boundaries with ridges between cells — more interesting than raw distance. -// Le CORPS a déménagé dans Public/VoxelCaveMorphology.h (namespace VoxelNoise), au plus bas point +// Le CORPS vit dans Public/VoxelCaveMorphology.h (namespace VoxelNoise), au plus bas point // qui voit déjà `VoxelHash` : la pile d'opérateurs a besoin exactement du même bruit pour la // rugosité (4b, type Cellular) et pour les festons (4f), et deux copies d'une fonction pure finissent // par diverger — c'est littéralement `AUDIT §C1`. Ce forwarder garde les ~3 sites d'appel ci-dessous -// inchangés, comme l'ont fait FractalNoise3D et RidgedNoise3D lors de T2.a. Le chemin chaud garde -// maintenant les coordonnées en float : la compatibilité d'arrondi historique est abandonnée. +// inchangés, comme FractalNoise3D et RidgedNoise3D. Le chemin chaud garde les coordonnées en float. static float CellularNoise3D(const FVector3f& Position) { return VoxelNoise::Cellular3D(Position); @@ -1198,9 +1196,9 @@ static void ApplyDisturbances(float& MC, float X, float Y, float Z, } } -// The disturbance pass is deterministic and spatially sparse. ClassifyTile used to treat a -// non-zero density as a world-wide influence, which made an otherwise proven all-solid/all-air -// LOD0 box fall back to the full mesher. These predicates are deliberately one-sided: they only +// The disturbance pass is deterministic and spatially sparse. Treating a non-zero density as a +// world-wide influence would make an otherwise proven all-solid/all-air LOD0 box fall back to the +// full mesher. These predicates are deliberately one-sided: they only // prove that no hash-placed primitive can touch the queried box. An intersecting candidate, an // invalid range, or an unknown parameter keeps the conservative Mixed result. static FORCEINLINE bool VF_DisturbanceBandTouchesBox( @@ -2550,12 +2548,11 @@ namespace /** * Construit la pile de cet archétype dans `OutStack` et remplit les bornes Z de `OutCtx`. * - * @return false quand la pile NE DOIT PAS être utilisée — archétype non porté, params absents, - * ou **strate dégénérée**. Ce dernier cas n'est pas de la prudence : cinq fonctions - * d'archétype court-circuitent sur `return 1.0f` (= air) quand la hauteur est nulle, - * et la pile n'a pas cet early-out, par conception. L'appelant retombe sur le `switch`, - * qui EST le comportement de référence. (`GetDensityWithParams`, lui, n'a aucun - * early-out de ce genre : TunnelNetwork/Underwater n'ont donc pas cette garde.) + * @return false quand la pile NE DOIT PAS être utilisée — archétype inconnu, params absents, + * ou **strate dégénérée** (hauteur nulle). La pile n'a pas d'early-out pour ce dernier + * cas, par conception : l'appelant rend de l'air (`GetDensityAt`) ou refuse de prouver + * la tuile (`ClassifyTile`). (`GetDensityWithParams` n'a aucun early-out de ce genre : + * TunnelNetwork/Underwater n'ont donc pas cette garde.) */ bool VF_BuildOpStackForChunk(ECaveGeneratorType Type, FVoxelStackParamRefs& Refs, int32 Seed, float SpineRadius, const UVoxelStrateManager* SM, @@ -2573,8 +2570,8 @@ namespace case ECaveGeneratorType::FlatPlain: case ECaveGeneratorType::CrystalChamber: - // UN SEUL cas pour les deux, comme le `switch` de production : `GetSlabDensity` ne les - // distingue pas non plus. Voir BuildSlabStack. + // UN SEUL cas pour les deux : ils ne diffèrent que par leurs valeurs par défaut. + // Voir BuildSlabStack. if (!Refs.Slab) { return false; } if (Refs.Slab->StrateTopWorldZ - Refs.Slab->StrateBottomWorldZ <= 0.0f) { return false; } OutCtx.StrateTopWorldZ = Refs.Slab->StrateTopWorldZ; @@ -2622,9 +2619,8 @@ namespace return true; default: - // `UsesOperatorStackForChunk` ne rend true que pour les archétypes portés (les 8), donc - // on ne devrait jamais arriver ici. Si ça arrive : retomber sur le `switch` plutôt que - // générer du vide — un monde faux est pire qu'un monde non porté. + // `UsesOperatorStackForChunk` ne rend true que pour les 8 archétypes, donc on ne devrait + // jamais arriver ici (valeur d'enum invalide). return false; } } @@ -2843,11 +2839,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co thread_local FBiomeContext CP_BiomeCtx; thread_local FChunkBiomeCache CP_BiomeCache; thread_local TArray CP_SurfaceBiomeParams; - // T1.a per-column surface cache: GSurfColCache (file-scope, shared with ClassifyTile). - // Discriminates the surface cache by strate: same strate ⇒ identical heightfield params ⇒ columns - // are shareable across the whole vertical chunk stack. Taken from the params themselves - // (StrateBottomWorldZ is unique per stacked strate) so the key can never disagree with CP_Surface. - thread_local int32 CP_StrateKey = MIN_int32; // AUDIT C2 — la clé DOIT contenir la version de layout, pas seulement le chunk. Après un // RebuildStrates / une édition à chaud, StrateManager reconstruit le layout et bumpe la // version ; un worker dont CP_Chunk vaut encore ce chunk sauterait le refetch et @@ -2860,9 +2851,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // tile-window context. A cache hit moves the prepared stack/core out of these TLS values; // when the other context misses, this bit must force the rebuild guard below to run. thread_local bool CP_UsesTileCacheWindow = false; - // OPSTACK Phase 1 — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les - // params (donc même clé owner+chunk+version, aucune logique d'invalidation en plus). Vide tant que - // la strate n'a pas coché `bUseOperatorStack` ET que son archétype n'est pas porté. + // OPSTACK — la pile d'opérateurs, construite dans le MÊME bloc de refetch que les + // params (donc même clé owner+chunk+version, aucune logique d'invalidation en plus). Vide + // seulement pour une strate dégénérée (hauteur nulle). thread_local FVoxelOpStack CP_OpStack; thread_local bool CP_UseOpStack = false; thread_local uint64 CP_ManagerLifetimeId = 0; @@ -2891,8 +2882,8 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // A coarse tile samples thousands of exact XY chunk keys, but those keys describe the // same immutable stack whenever the selected archetype is operator-stack backed. Share // that prepared state by Z; the stack still queries the actual world position, and its - // room-graph cache remains keyed by the real XY search window. Legacy tunnel states keep - // the full XYZ key because their native cache is built for one XY window. + // room-graph cache remains keyed by the real XY search window. Chunks outside the strate + // layout (constant air) keep the full XYZ key. const ECaveGeneratorType QueryGeneratorType = bUseChunkRoutingCache ? CP_RouteGeneratorType : StrateManager->GetGeneratorTypeForChunk(ChunkCoord); @@ -3067,7 +3058,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co CP_Maze = StrateManager->GetMazeParamsForChunk(ChunkCoord); break; case ECaveGeneratorType::SurfaceWorld: ResolveSurfaceChunkParams(ChunkCoord, CP_Surface, CP_BiomeCtx, CP_SurfaceBiomeParams); - CP_StrateKey = FMath::RoundToInt(CP_Surface.StrateBottomWorldZ); break; case ECaveGeneratorType::VerticalShafts: CP_Vert = StrateManager->GetVerticalShaftParamsForChunk(ChunkCoord); break; @@ -3086,131 +3076,112 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co } CP_Dist = StrateManager->GetDisturbanceParamsForChunk(ChunkCoord); - // ── OPSTACK Phase 1 : (re)construire la pile si cette strate l'a demandée. ── - // Une seule branche ajoutée au chemin densité, et elle est FROIDE : la construction est - // par chunk (comme le refetch de params juste au-dessus), jamais par voxel. - CP_UseOpStack = bUseChunkRoutingCache - ? CP_RouteUsesOperatorStack - : StrateManager->UsesOperatorStackForChunk(ChunkCoord); - CP_UseOpStack = CP_UseOpStack || CP_UseCustomRecipe; + // ── OPSTACK : (re)construire la pile de ce chunk. ── + // Toute strate est générée par une pile d'opérateurs. La construction est par chunk + // (comme le refetch de params juste au-dessus), jamais par voxel. + // Every strate is generated by an operator stack. 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. + CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile + CP_UseOpStack = false; #if WITH_EDITOR - // A composer override is already an explicit, validated stack description. It must - // be evaluated even when the authored definition had the normal operator-stack opt-in - // disabled; otherwise a multi-region candidate would be installed in the manager but - // silently fall back to the old one-archetype switch. - CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions; -#endif - if (CP_UseOpStack) + if (CP_UseComposerRegions) { - CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile - bool bBuiltSpecialStack = false; -#if WITH_EDITOR - if (CP_UseComposerRegions) + FVoxelOpContext RegionContext; + if (VF_BuildStrateRegionStack( + CP_ComposerRegions, OriginSpineRadius, StrateManager, + CP_OpStack, RegionContext, nullptr)) { - bBuiltSpecialStack = true; - FVoxelOpContext RegionContext; - if (VF_BuildStrateRegionStack( - CP_ComposerRegions, OriginSpineRadius, StrateManager, - CP_OpStack, RegionContext, nullptr)) - { - RegionContext.ChunkCoord = ChunkCoord; - RegionContext.Step = 1; - RegionContext.LayoutVersion = LayoutVersion; - RegionContext.WorldRadiusVoxels = WorldRadiusVoxels; - RegionContext.EdgeSealThickness = EdgeSealThickness; - CP_OpStack.PrepareChunk(RegionContext); - } - else - { - CP_UseOpStack = false; - } + RegionContext.ChunkCoord = ChunkCoord; + RegionContext.Step = 1; + RegionContext.LayoutVersion = LayoutVersion; + RegionContext.WorldRadiusVoxels = WorldRadiusVoxels; + RegionContext.EdgeSealThickness = EdgeSealThickness; + CP_OpStack.PrepareChunk(RegionContext); + CP_UseOpStack = true; } -#endif - if (!bBuiltSpecialStack) + else { - if (CP_UseCustomRecipe) - { - FVoxelOpContext RecipeContext; - if (VF_BuildStackFromRecipe( - CP_CustomRecipe, CP_CustomParams, CP_CustomRecipeSeed, - OriginSpineRadius, StrateManager, CP_OpStack, RecipeContext, nullptr)) - { - RecipeContext.ChunkCoord = ChunkCoord; - RecipeContext.Step = 1; - RecipeContext.LayoutVersion = LayoutVersion; - // The recipe builder supplies the candidate's vertical seal to the - // structural post. The XY edge seal is global and uses live settings. - RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels; - RecipeContext.EdgeSealThickness = EdgeSealThickness; - CP_OpStack.PrepareChunk(RecipeContext); - } - else - { - // Import/load validates recipes before a season becomes active. Keep - // the fallback hole-safe if a future schema changes underneath it. - CP_UseOpStack = false; - } - } - else - { - FVoxelOpContext OpCtx; - OpCtx.ChunkCoord = ChunkCoord; - OpCtx.Seed = (uint32)Seed; - OpCtx.LayoutVersion = LayoutVersion; - OpCtx.WorldRadiusVoxels = WorldRadiusVoxels; - OpCtx.EdgeSealThickness = EdgeSealThickness; + CP_OpStack = FVoxelOpStack(); + CP_UseComposerRegions = false; + } + } +#endif + if (!CP_UseOpStack && CP_UseCustomRecipe) + { + FVoxelOpContext RecipeContext; + if (VF_BuildStackFromRecipe( + CP_CustomRecipe, CP_CustomParams, CP_CustomRecipeSeed, + OriginSpineRadius, StrateManager, CP_OpStack, RecipeContext, nullptr)) + { + RecipeContext.ChunkCoord = ChunkCoord; + RecipeContext.Step = 1; + RecipeContext.LayoutVersion = LayoutVersion; + // The recipe builder supplies the candidate's vertical seal to the + // structural post. The XY edge seal is global and uses live settings. + RecipeContext.WorldRadiusVoxels = WorldRadiusVoxels; + RecipeContext.EdgeSealThickness = EdgeSealThickness; + CP_OpStack.PrepareChunk(RecipeContext); + CP_UseOpStack = true; + } + else + { + // Import/load validates recipes before a season becomes active. If a future + // schema still fails here, fall back to the archetype's native stack below. + CP_OpStack = FVoxelOpStack(); + CP_UseCustomRecipe = false; + } + } + if (!CP_UseOpStack) + { + FVoxelOpContext OpCtx; + OpCtx.ChunkCoord = ChunkCoord; + OpCtx.Seed = (uint32)Seed; + OpCtx.LayoutVersion = LayoutVersion; + OpCtx.WorldRadiusVoxels = WorldRadiusVoxels; + OpCtx.EdgeSealThickness = EdgeSealThickness; // ⚠️ LE MAPPING VIT DANS `VF_BuildOpStackForChunk` (haut de ce fichier) ET NULLE // PART AILLEURS — `ClassifyTile` appelle la MÊME fabrique. Un verdict de tuile issu // d'une pile construite autrement serait un trou. Ici on ne fait que fournir les // params déjà cherchés juste au-dessus. - FVoxelStackParamRefs Refs; - Refs.Slab = &CP_Slab; - Refs.Maze = &CP_Maze; - Refs.Vert = &CP_Vert; - Refs.Float = &CP_Float; - Refs.Tunnel = &CP_Tunnel; + FVoxelStackParamRefs Refs; + Refs.Slab = &CP_Slab; + Refs.Maze = &CP_Maze; + Refs.Vert = &CP_Vert; + Refs.Float = &CP_Float; + Refs.Tunnel = &CP_Tunnel; // SurfaceWorld : le champ de biomes est fabriqué ICI, du côté qui connaît le // générateur, et TRANSFÉRÉ à la pile. L'opérateur ne voit qu'une `IVoxelBiomeField`, // ce qui lui permet de devenir un asset en Phase 3 sans traîner le générateur. - TArray PerBiome; - if (CP_GenType == ECaveGeneratorType::SurfaceWorld) - { - Refs.Surface = &CP_Surface; - if (CP_BiomeCtx.IsValid() && CP_SurfaceBiomeParams.Num() > 0) - { - PerBiome = CP_SurfaceBiomeParams; - Refs.SurfaceBiomeParams = &PerBiome; - Refs.BiomeField = MakeUnique( - this, &CP_BiomeCtx, &CP_BiomeCache, ChunkCoord.Z); - } - } - - CP_UseOpStack = VF_BuildOpStackForChunk( - CP_GenType, Refs, Seed, OriginSpineRadius, - StrateManager, CP_OpStack, OpCtx); - - // Le test appelle PrepareChunk, pas la production : c'est exactement la divergence - // qui rend un opérateur vert en test et faux en jeu. Les sept `PrepareChunk` - // actuels sont vides, donc ceci ne change RIEN aujourd'hui — c'est le point : le - // premier opérateur qui hisse vraiment du travail par chunk doit trouver l'appel - // déjà là. `Step` reste 1 : GetDensityAt ne connaît pas le pas d'échantillonnage - // du mesher (voir le contrat T2.b dans VoxelDensityOp.h). - // The test calls PrepareChunk and production did not — the exact divergence that - // makes an op green in test and wrong in game. All seven bodies are empty today, - // which is the point: the first op that hoists real per-chunk work must find the - // call already here. - if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); } + TArray PerBiome; + if (CP_GenType == ECaveGeneratorType::SurfaceWorld) + { + Refs.Surface = &CP_Surface; + if (CP_BiomeCtx.IsValid() && CP_SurfaceBiomeParams.Num() > 0) + { + PerBiome = CP_SurfaceBiomeParams; + Refs.SurfaceBiomeParams = &PerBiome; + Refs.BiomeField = MakeUnique( + this, &CP_BiomeCtx, &CP_BiomeCache, ChunkCoord.Z); } } + + CP_UseOpStack = VF_BuildOpStackForChunk( + CP_GenType, Refs, Seed, OriginSpineRadius, + StrateManager, CP_OpStack, OpCtx); + + // PrepareChunk runs here exactly as in the tests, so an op that hoists real + // per-chunk work behaves the same in game. `Step` reste 1 : GetDensityAt ne connaît + // pas le pas d'échantillonnage du mesher (voir le contrat T2.b dans VoxelDensityOp.h). + if (CP_UseOpStack) { CP_OpStack.PrepareChunk(OpCtx); } } - // The legacy path needs its prepared native tunnel-core cache. An operator-stack - // tunnel source publishes the same world-space result after EvalMC, so preparing a - // second full graph/core cache here would only duplicate the work that made coarse - // streaming expensive in the first place. Custom/composer paths retain the legacy - // cache unless their own stack supplies the hand-off. + // An operator-stack tunnel source publishes the world-space core after EvalMC, so + // preparing a second full graph/core cache here would only duplicate the work that made + // coarse streaming expensive in the first place. Only an editor composer region stack, + // which does not publish that hand-off, still gets the prepared native core. const bool bNativeTunnelCore = !CP_UseCustomRecipe && (CP_GenType == ECaveGeneratorType::TunnelNetwork || CP_GenType == ECaveGeneratorType::Underwater); @@ -3287,7 +3258,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co float BlockDensity = 0.f; bool bBlockHasTunnelCore = false; float BlockTunnelCoreSDF = FLT_MAX; - bool bBlockTunnelCoreSupportFloor = false; bool bBlockTunnelCoreRoomFloor = false; bool bBlockHasTunnelCoreSweptFloor = false; float BlockTunnelCoreSweptFloorZ = -FLT_MAX; @@ -3296,12 +3266,9 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co FTunnelCoreWorldEvaluation FusedTunnelCore; bool bRoomOwnsBottom = false; bool bRoomOwnsBottomValid = false; - FVector NativeWarpedPosition = FVector::ZeroVector; - bool bNativeWarpedPositionValid = false; // Le seul point d'entrée de la pile dans le chemin de production. Elle rend la convention - // MC (négatif = solide) comme les fonctions d'archétype, donc les disturbances et la couche - // de diff qui suivent ne voient aucune différence. + // MC (négatif = solide), que les disturbances et la couche de diff qui suivent attendent. if (CP_UseOpStack) { const bool bCanUseFusedEvaluator = @@ -3362,7 +3329,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co BlockDensity = BlockCoreSample.Density; bBlockHasTunnelCore = BlockCoreSample.bHasTunnelCoreWorldEvaluation; BlockTunnelCoreSDF = BlockCoreSample.TunnelCoreWorldSDF; - bBlockTunnelCoreSupportFloor = BlockCoreSample.bTunnelCoreSupportFloor; bBlockTunnelCoreRoomFloor = BlockCoreSample.bTunnelCoreRoomFloor; bBlockHasTunnelCoreSweptFloor = BlockCoreSample.bHasTunnelCoreSweptFloor; @@ -3386,66 +3352,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co } } } - else switch (CP_GenType) + else { - case ECaveGeneratorType::FlatPlain: - case ECaveGeneratorType::CrystalChamber: - Result = GetSlabDensity(WorldX, WorldY, WorldZ, CP_Slab); break; - case ECaveGeneratorType::Maze: - Result = GetMazeDensity(WorldX, WorldY, WorldZ, CP_Maze); break; - case ECaveGeneratorType::SurfaceWorld: - { - // Integer XY (the density grid) → reuse the column down its whole Z extent (T1.a). - // Fractional XY (gradient-normal samples) → compute directly (no cache key). - if (WorldX == FMath::FloorToFloat(WorldX) && WorldY == FMath::FloorToFloat(WorldY)) - { - const int32 IX = (int32)WorldX, IY = (int32)WorldY; - // XY-keyed LRU box (shared down the whole vertical strate stack). Acquire centres a box on - // the first sample so the rest of the chunk's queries — incl. the ±Step margin ring — hit. - FSurfaceColumnBox& Box = GSurfColCache.Acquire( - IX, IY, CP_StrateKey, Seed, LayoutVersion, - DensityCacheOwnerId, ManagerLifetimeId); - const int32 CI = (IY - Box.BaseY) * FSurfaceColumnBox::Dim + (IX - Box.BaseX); - if (!Box.Computed[CI]) - { - ComputeSurfaceColumn(WorldX, WorldY, ChunkCoord.Z, CP_Surface, CP_BiomeCtx, - CP_SurfaceBiomeParams, CP_BiomeCache, - Box.Cols[CI].TerrainZ, Box.Cols[CI].CeilSurf, - Box.Cols[CI].OverhangAmp, Box.Cols[CI].DirX, Box.Cols[CI].DirY); - Box.Computed[CI] = true; - } - const FSurfaceColumn& Col = Box.Cols[CI]; - Result = SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, - Col.TerrainZ, Col.CeilSurf, Col.OverhangAmp, - Col.DirX, Col.DirY, CP_Surface); - } - else - { - float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; - ComputeSurfaceColumn(WorldX, WorldY, ChunkCoord.Z, CP_Surface, CP_BiomeCtx, - CP_SurfaceBiomeParams, CP_BiomeCache, TerrainZ, CeilSurf, - OverhangAmp, DirX, DirY); - Result = SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, - TerrainZ, CeilSurf, OverhangAmp, DirX, DirY, CP_Surface); - } - break; - } - case ECaveGeneratorType::VerticalShafts: - Result = GetVerticalShaftDensity(WorldX, WorldY, WorldZ, CP_Vert); break; - case ECaveGeneratorType::FloatingIslands: - Result = GetFloatingIslandDensity(WorldX, WorldY, WorldZ, CP_Float); break; - case ECaveGeneratorType::Underwater: - case ECaveGeneratorType::TunnelNetwork: - default: - // Underwater shares tunnel rock (water table is a render-side overlay). - Result = GetDensityWithParams(WorldX, WorldY, WorldZ, CP_Tunnel, - CP_TunnelFP, LayoutVersion, - /*bApplyLegacyStructuralPosts=*/false, - nullptr, false, &bRoomOwnsBottom, - &NativeWarpedPosition); - bRoomOwnsBottomValid = true; - bNativeWarpedPositionValid = true; - break; + // Only a degenerate strate (zero height) has no stack. It is open air, like the + // archetype functions the stack replaced. + Result = 1.0f; } DensityCoreTimer.End(); @@ -3466,7 +3377,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co else if (bUsedOpBlockSample && bBlockHasTunnelCore) { PreDisturbanceTunnelCore.SDF = BlockTunnelCoreSDF; - PreDisturbanceTunnelCore.bSupportFloor = bBlockTunnelCoreSupportFloor; PreDisturbanceTunnelCore.bRoomFloor = bBlockTunnelCoreRoomFloor; PreDisturbanceTunnelCore.bHasSweptFloor = bBlockHasTunnelCoreSweptFloor; @@ -3486,21 +3396,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCorePosts); - FVector RoomQueryPosition; - if (bNativeWarpedPositionValid) + FVector RoomQueryPosition(WorldX, WorldY, WorldZ); + if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f) { - RoomQueryPosition = NativeWarpedPosition; - } - else - { - RoomQueryPosition = FVector(WorldX, WorldY, WorldZ); - if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f) - { - RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale; - } - RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp( - RoomQueryPosition, CP_Tunnel, static_cast(Seed)); + RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale; } + RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp( + RoomQueryPosition, CP_Tunnel, static_cast(Seed)); PreDisturbanceTunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr, VoxelGenLOD::ShouldUseSpatialIndex( @@ -3524,7 +3426,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co // A disturbance is allowed to add visual detail, but it must not refill the landing's // measured air volume or turn its support slab back into a hole. Reassert the landing // air first, then the structural floor, in MC space before the final XY seal. The two - // calls use the same landing geometry already evaluated by the legacy and op-stack post. + // calls use the same landing geometry already evaluated by the op-stack structural post. VoxelDensityProfile::FScopedTimer DensityStructuralPostsTimer( VoxelDensityProfile::EBucket::DensityStructuralPosts); VoxelDensityProfile::FScopedTimer StructuralTailTimer( @@ -3699,21 +3601,13 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co { VoxelDensityProfile::FScopedTimer ProfileTimer( VoxelDensityProfile::EBucket::TunnelCorePosts); - FVector RoomQueryPosition; - if (bNativeWarpedPositionValid) + FVector RoomQueryPosition(WorldX, WorldY, WorldZ); + if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f) { - RoomQueryPosition = NativeWarpedPosition; - } - else - { - RoomQueryPosition = FVector(WorldX, WorldY, WorldZ); - if (CP_Tunnel.VerticalScale != 1.0f && CP_Tunnel.VerticalScale > 0.0f) - { - RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale; - } - RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp( - RoomQueryPosition, CP_Tunnel, static_cast(Seed)); + RoomQueryPosition.Z = WorldZ / CP_Tunnel.VerticalScale; } + RoomQueryPosition = VoxelCaveMorphology::ApplyCaveWarp( + RoomQueryPosition, CP_Tunnel, static_cast(Seed)); TunnelCore = VoxelCaveMorphology::EvaluateTunnelCoreWorld( WorldX, WorldY, WorldZ, ActiveTunnelCoreCache->Cache, nullptr, @@ -3858,8 +3752,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo bool bApplyLegacyStructuralPosts, FTunnelCoreWorldEvaluation* OutTunnelCore, bool bCollectFusedDiagnostics, - bool* OutRoomOwnsBottom, - FVector* OutWarpedPosition) const + bool* OutRoomOwnsBottom) const { //========================================================================= // STRATE DENSITY FUNCTION (Morphology Pipeline) @@ -3894,10 +3787,6 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo { *OutRoomOwnsBottom = false; } - if (OutWarpedPosition != nullptr) - { - *OutWarpedPosition = FVector::ZeroVector; - } if (bCollectFusedDiagnostics && VoxelDensityProfile::AreCountersEnabled()) { VoxelDensityProfile::AddCounter( @@ -3969,10 +3858,6 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo WarpedY += WarpY * VOXEL_NOISE_SCALE * WS; WarpedZ += WarpZ * VOXEL_NOISE_SCALE * WS; } - if (OutWarpedPosition != nullptr) - { - *OutWarpedPosition = FVector(WarpedX, WarpedY, WarpedZ); - } //========================================================================= // STEP 4: SDF MORPHOLOGY (rooms + tunnels) — CACHED PER CHUNK @@ -4002,7 +3887,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo thread_local float CachedSMinY = 0.0f, CachedSMaxY = 0.0f; thread_local int32 CachedStrate = INT32_MIN; thread_local uint32 CachedSeed = 0; - // ⚠️ AUDIT §C2 (corrigé le 2026-07-28). Les deux lignes qui manquaient à cette clé. + // ⚠️ AUDIT §C2 — `CachedFingerprint` et `CachedLayout` ci-dessous sont OBLIGATOIRES dans cette clé. // La clé ci-dessus décrit la GÉOMÉTRIE de la fenêtre (boîte, strate, seed) et rien de ce qui // détermine les PARAMS avec lesquels les salles ont été cuites. Comme `GetGenerationParams` // blende à l'intérieur d'une strate (`Alpha` = f(chunk Z), et f(chunk XY) aussi en @@ -4013,7 +3898,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // Pourquoi ça ne casse PAS l'invariant de perf de §8.10 : la clé reste une BOÎTE, donc les // sondes de gradient à `WorldX ± 1` ne font toujours pas tourner le cache. Ce qui le fait // tourner en plus, c'est un changement RÉEL de params — une fois par chunk dans une bande de - // transition, ce qui est le nombre de reconstructions que ce cache aurait toujours dû faire. + // transition, ce qui est exactement le nombre de reconstructions nécessaire. thread_local uint32 CachedFingerprint = 0xFFFFFFFFu; thread_local uint32 CachedLayout = 0xFFFFFFFFu; thread_local bool CachedUsesFusedCacheWindow = false; @@ -4521,7 +4406,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo // // We copy Params and apply the nearest room's terrain op on top. // Base Params has all terrain op fields = 0 (disabled) since - // BuildParamsFromDefinition no longer merges them globally. + // BuildParamsFromDefinition does not merge them globally. // // Result: voxels inside different rooms see different terrain ops. // Rooms with no assigned op leave terrain fields at 0 → no terrain op. Clean. @@ -5177,353 +5062,6 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo return -Density; } -//============================================================================= -// SLAB DENSITY (FlatPlain / CrystalChamber generator types) -//============================================================================= -// Produces a large horizontal void between a noisy floor and a noisy ceiling. -// No rooms, no tunnels, no worm noise — just two surfaces with noise displacement. -// -// Key difference from TunnelNetwork: ceiling uses abs(noise) so ALL formations -// point DOWNWARD. This creates the stalactite/crystal hanging-from-above effect. -// Floor uses signed noise for natural ground undulation (hills and valleys). -// -// Columns are placed on a world-space hash grid (no rooms to anchor them to). - -float UVoxelGenerator::GetSlabDensity(float WorldX, float WorldY, float WorldZ, - const FSlabGenerationParams& Params) const -{ - const float StrateHeight = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; - - // Degenerate strate (zero or inverted bounds) — return solid. - if (StrateHeight <= 0.0f) return 1.0f; - - const uint32 SeedU = (uint32)Seed; - - //========================================================================= - // STEP 1: FLOOR SURFACE - //========================================================================= - // The floor is at FloorZ + signed noise displacement. - // Signed noise allows both hills (noise > 0 → floor rises) and - // valleys (noise < 0 → floor dips) for natural rolling ground. - // - // ⚠️ XY-PUR / XY-PURE (OPSTACK-DECOMPOSITION §3.1, tranché par Jahni 2026-07-27). - // La 3e coordonnée était `WorldZ * FF * 0.05f` : une hauteur de sol qui dépendait de - // l'altitude d'où on la demandait. Le coefficient était minuscule, donc ça se lisait comme un - // léger étirement vertical plutôt que comme un bug — mais ça bloquait le cache de colonnes T1.a - // et rendait toute classification de boîte inexacte. Constante ⇒ la surface est une vraie - // fonction de (X,Y). Le monde se re-tune une fois : on échantillonne une autre tranche du champ - // de bruit, donc la forme du sol change (elle ne se dégrade pas). - // - // The 3rd coord was WorldZ * FF * 0.05f — a floor height that depended on the altitude you - // asked from. Now a constant, so the surface is a genuine function of (X,Y): the T1.a column - // cache and an exact box verdict both become available. Worlds re-tune once. - - const float FloorZ = Params.StrateBottomWorldZ + StrateHeight * Params.FloorRelativeHeight; - - float FloorNoise = 0.0f; - if (Params.FloorRoughness > 0.0f) - { - float FF = Params.FloorRoughnessFrequency; - FloorNoise = FractalNoise3D(FVector3f( - WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f), - WorldY * FF + VoxelHash::SeedOffset(SeedU, 11.1f), - 0.0f // XY-pur : plus aucune dépendance en Z / no Z dependence - ), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE * Params.FloorRoughness; - } - - // Actual floor surface Z after noise displacement. - const float FloorSurface = FloorZ + FloorNoise; - - //========================================================================= - // STEP 2: CEILING SURFACE (formations hang DOWNWARD) - //========================================================================= - // The ceiling uses abs(noise) so ALL displacement pushes the ceiling DOWN. - // When abs(noise) is high, rock protrudes further into the void — stalactite. - // When abs(noise) is near 0, the ceiling is near the base CeilZ line. - // - // This asymmetry (only downward protrusions, never upward pockets) creates - // the crystal-forest / stalactite silhouette from below. - // - // XY-PUR, même raison que le sol ci-dessus (§3.1). Le `+ 3000.0f` RESTE : ce n'est pas un - // terme en Z, c'est le décalage qui décorrèle le champ du plafond de celui du sol. - // XY-pure for the same reason as the floor. The + 3000.0f STAYS — it is not a Z term, it is - // the offset that decorrelates the ceiling's noise field from the floor's. - - const float CeilZ = Params.StrateBottomWorldZ + StrateHeight * Params.CeilingRelativeHeight; - - float CeilNoise = 0.0f; - if (Params.CeilingRoughness > 0.0f) - { - float CF = Params.CeilingRoughnessFrequency; - float RawNoise = FractalNoise3D(FVector3f( - WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f, - WorldY * CF + VoxelHash::SeedOffset(SeedU, 19.7f) + 2000.0f, - 3000.0f // XY-pur : décalage de décorrélation seul / offset only - ), VoxelGenLOD::Eff(3)) * VOXEL_NOISE_SCALE; - - // abs() → formations ONLY hang down, never push ceiling up into solid rock. - // Result: every noise peak creates a downward protrusion (crystal/stalactite). - CeilNoise = FMath::Abs(RawNoise) * Params.CeilingRoughness; - } - - // Actual ceiling surface Z (can only move downward due to abs above). - // Clamp so ceiling never drops below floor + 2 voxels of headroom. - // Without this clamp, extreme CeilingRoughness could completely fill the void. - const float CeilSurface = FMath::Max(CeilZ - CeilNoise, FloorSurface + 2.0f); - - //========================================================================= - // STEP 3: VOID FIELD → BASE DENSITY - //========================================================================= - // Each voxel is measured against both surfaces: - // DistAboveFloor > 0 → voxel is above the floor (possibly in the void) - // DistBelowCeil > 0 → voxel is below the ceiling (possibly in the void) - // - // VoidField = min of both distances. - // Positive inside the void (between floor and ceiling). - // Negative outside (below floor or above ceiling = solid rock). - // - // Density = -VoidField (internal convention: positive = solid, negative = air). - - const float DistAboveFloor = WorldZ - FloorSurface; // + when above floor - const float DistBelowCeil = CeilSurface - WorldZ; // + when below ceiling - - const float VoidField = FMath::Min(DistAboveFloor, DistBelowCeil); - - float Density = -VoidField; // Negative = air (inside void), positive = solid - - //========================================================================= - // STEP 4: COLUMNS (hash-based, world-space grid) - //========================================================================= - // Unlike TunnelNetwork columns (anchored to room centers), slab columns - // are placed on a regular world-space hash grid. They are infinite-height - // cylinders — the void field already defines where solid/air is, so the - // column SDF just adds density everywhere along its XY position. - // The column is only visible where the void field carved air around it. - if (Params.ColumnDensity > 0.0f && Params.ColumnSpacing > 0.0f) - { - const float Spacing = Params.ColumnSpacing; - - // Which cell are we in? - const int32 ColCX = FMath::FloorToInt(WorldX / Spacing); - const int32 ColCY = FMath::FloorToInt(WorldY / Spacing); - - // The 3×3 neighbourhood's columns (existence roll, jitter, radius) are a pure function of - // (cell, seed, params) yet were re-derived PER VOXEL — 9 hash rolls + mixes in the slab hot - // loop. Bake them once per centre cell (thread_local); rebuild only when the query crosses a - // cell border or the seed/params change. Bit-identical (same hashes, same math). - struct FSlabColumn { float X, Y, R; }; - thread_local TArray> SC_Cols; - thread_local int32 SC_CX = INT32_MAX, SC_CY = INT32_MAX; - thread_local uint32 SC_Seed = 0xFFFFFFFFu; - thread_local float SC_Spacing = -1.0f, SC_Dens = -1.0f, SC_MinR = -1.0f, SC_MaxR = -1.0f; - - if (ColCX != SC_CX || ColCY != SC_CY || (uint32)Seed != SC_Seed || Spacing != SC_Spacing || - Params.ColumnDensity != SC_Dens || Params.ColumnMinRadius != SC_MinR || Params.ColumnMaxRadius != SC_MaxR) - { - SC_CX = ColCX; SC_CY = ColCY; SC_Seed = (uint32)Seed; SC_Spacing = Spacing; - SC_Dens = Params.ColumnDensity; SC_MinR = Params.ColumnMinRadius; SC_MaxR = Params.ColumnMaxRadius; - SC_Cols.Reset(); - - for (int32 DY = -1; DY <= 1; DY++) - { - for (int32 DX = -1; DX <= 1; DX++) - { - const int32 NCX = ColCX + DX; - const int32 NCY = ColCY + DY; - - // Deterministic: same seed → same column pattern every session. - // XOR with a prime salt so columns don't correlate with room placement. - const uint32 H = VoxelHash::Cell(NCX, NCY, (uint32)Seed ^ 0xC01C01u); - - // ColumnDensity is the probability this cell has a column. - if (VoxelHash::ToFloat01(H) > Params.ColumnDensity) continue; - - // Jitter the column center within the cell (15%-85% of cell extent) - // to avoid a perfectly regular grid pattern. - const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x12345678u)); - const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0x9ABCDEF0u)); - - FSlabColumn& Col = SC_Cols.AddDefaulted_GetRef(); - Col.X = (NCX + 0.15f + JX * 0.7f) * Spacing; - Col.Y = (NCY + 0.15f + JY * 0.7f) * Spacing; - // Column radius: hash-derived within configured range. - Col.R = FMath::Lerp(Params.ColumnMinRadius, Params.ColumnMaxRadius, - VoxelHash::ToFloat01(VoxelHash::Mix(H ^ 0xBEEFu))); - } - } - } - - float ColumnSDF = FLT_MAX; - for (const FSlabColumn& Col : SC_Cols) - { - // 2D cylinder SDF (infinite height — void field handles top/bottom). - const float DX2D = WorldX - Col.X; - const float DY2D = WorldY - Col.Y; - ColumnSDF = FMath::Min(ColumnSDF, FMath::Sqrt(DX2D * DX2D + DY2D * DY2D) - Col.R); - } - - // Smoothstep blend zone around the column edge (avoids hard MC aliasing). - const float ColBlend = 2.0f; - if (ColumnSDF < ColBlend && ColumnSDF < FLT_MAX) - { - float Fill = FMath::Clamp((ColBlend - ColumnSDF) / (ColBlend * 2.0f), 0.0f, 1.0f); - Fill = SmoothStep01(Fill); // Smoothstep - Density += Fill * Params.BaseDensity * 1.5f; - } - } - - //========================================================================= - // STEP 5: BOUNDARY SEAL + STEP 6: PASSAGES + STEP 7: XY EDGE SEAL - //========================================================================= - // Même logique que TunnelNetwork — factorisée dans les helpers ci-dessus. - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); - - // Convention MC: négatif = solide. - return -Density; -} - -//============================================================================= -// MAZE GENERATOR (ECaveGeneratorType::Maze) -//============================================================================= -// Solid rock carved by a deterministic 3D lattice of corridors. Each non-origin lattice node -// chooses one parent toward (0,0,0), which is a spanning tree by construction. A small, capped -// hash-gated loop set adds alternate routes without being needed for connectivity. The local -// source evaluates the eight child nodes in the current cell's {-1,0} halo, so chunk seams cannot -// change an edge decision. - -float UVoxelGenerator::GetMazeDensity(float WorldX, float WorldY, float WorldZ, - const FMazeGenerationParams& Params) const -{ - const float StrateHeight = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; - if (StrateHeight <= 0.0f) return 1.0f; - - const float CS = FMath::Max(Params.CellSize, 1.0f); - const FVector Pos(WorldX, WorldY, WorldZ); - const uint32 S = (uint32)Seed ^ 0x4D617A65u; // 'Maze' - - float Density = Params.BaseDensity; // start solid - - const int32 CX = FMath::FloorToInt(WorldX / CS); - const int32 CY = FMath::FloorToInt(WorldY / CS); - const int32 CZ = FMath::FloorToInt(WorldZ / CS); - - // The edge set reachable from this voxel's cell is a pure function of (cell, seed, params). - // Bake the parent and optional loop capsule endpoints once per cell (thread_local); the - // per-voxel work is just the capsule SDFs. Rebuilds only on a cell crossing / param change. - struct FMazeEdge { FVector A, B; }; - thread_local TArray> MZ_Edges; - thread_local FIntVector MZ_Cell(INT32_MAX, INT32_MAX, INT32_MAX); - thread_local uint32 MZ_Seed = 0xFFFFFFFFu; - thread_local float MZ_CS = -1.0f, MZ_Branch = -1.0f, MZ_Vert = -1.0f; - - const FIntVector Cell(CX, CY, CZ); - if (Cell != MZ_Cell || S != MZ_Seed || CS != MZ_CS || - Params.BranchProbability != MZ_Branch || Params.Verticality != MZ_Vert) - { - MZ_Cell = Cell; MZ_Seed = S; MZ_CS = CS; - MZ_Branch = Params.BranchProbability; MZ_Vert = Params.Verticality; - MZ_Edges.Reset(); - - auto NodeCenter = [CS](int32 X, int32 Y, int32 Z) - { - return FVector((X + 0.5f) * CS, (Y + 0.5f) * CS, (Z + 0.5f) * CS); - }; - // Canonical lower-node edges in {-1,0} per axis cover every corridor that can reach this - // voxel's cell. IsOpenEdge checks both endpoints, including a +1 node's parent choice. - for (int32 dz = -1; dz <= 0; dz++) - for (int32 dy = -1; dy <= 0; dy++) - for (int32 dx = -1; dx <= 0; dx++) - { - const int32 nx = CX + dx, ny = CY + dy, nz = CZ + dz; - const FVector A = NodeCenter(nx, ny, nz); - - if (VoxelMazeTopology::IsOpenEdge( - nx, ny, nz, VoxelMazeTopology::EAxis::X, - S, Params.BranchProbability, Params.Verticality)) - { - MZ_Edges.Add({ A, NodeCenter(nx + 1, ny, nz) }); - } - if (VoxelMazeTopology::IsOpenEdge( - nx, ny, nz, VoxelMazeTopology::EAxis::Y, - S, Params.BranchProbability, Params.Verticality)) - { - MZ_Edges.Add({ A, NodeCenter(nx, ny + 1, nz) }); - } - if (VoxelMazeTopology::IsOpenEdge( - nx, ny, nz, VoxelMazeTopology::EAxis::Z, - S, Params.BranchProbability, Params.Verticality)) - { - MZ_Edges.Add({ A, NodeCenter(nx, ny, nz + 1) }); - } - } - } - - const float R = FMath::Max(Params.CorridorRadius, 0.5f); - float MazeSDF = FLT_MAX; - for (const FMazeEdge& E : MZ_Edges) - { - MazeSDF = FMath::Min(MazeSDF, VoxelSDF::Capsule(Pos, E.A, E.B, R)); - } - - // Wall roughness: perturb the corridor surface. - if (Params.SurfaceRoughness > 0.0f && MazeSDF < R + Params.SurfaceRoughness + 2.0f) - { - MazeSDF += FractalNoise3D(FVector3f(WorldX * 0.12f, WorldY * 0.12f, WorldZ * 0.12f), VoxelGenLOD::Eff(3)) - * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; - } - - // Carve air where inside a corridor. - const float Blend = 2.0f; - if (MazeSDF < Blend) - { - float Carve = FMath::Clamp((Blend - MazeSDF) / (Blend * 2.0f), 0.0f, 1.0f); - Carve = SmoothStep01(Carve); - Density -= Carve * Params.BaseDensity * 2.0f; - } - - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); - - return -Density; -} - //============================================================================= // SURFACE-WORLD GENERATOR (ECaveGeneratorType::SurfaceWorld) //============================================================================= @@ -5726,71 +5264,6 @@ float UVoxelGenerator::ComputeSurfaceCeiling(float WorldX, float WorldY, return CeilZ - Hang; } -float UVoxelGenerator::SurfaceDensityFromColumn(float WorldX, float WorldY, float WorldZ, - float TerrainZ, float CeilSurf, - float OverhangAmp, float DirX, float DirY, - const FSurfaceGenerationParams& S) const -{ - // Solid below the terrain surface; solid above the sky-cap ceiling. - float Density = TerrainZ - WorldZ; - Density = FMath::Max(Density, WorldZ - CeilSurf); - - // F20 phase 2 — OVERHANG shelf (warped-terrain union): for AIR voxels in a window just above a - // steep slope, re-sample the heightfield UPHILL (toward the cliff) by a height-varying amount and - // union that rock in → the cliff-top rock juts OUT over the void below, self-capping at the cliff's - // height. Genuine 3D (per-voxel re-eval), so it's gated hard: only steep overhang columns (amp>0), - // only air voxels within OverhangHeight of the local ground, only when the shift is ≥ a voxel. - // The lip is capped at TerrainZ+OverhangHeight, which ClassifyTile treats as ambiguous (no holes). - if (OverhangAmp > 0.0f && S.OverhangHeight > 0.0f - && WorldZ > TerrainZ && WorldZ <= TerrainZ + S.OverhangHeight) - { - const uint32 SeedU = (uint32)Seed; - const float f = S.OverhangFrequency; - // Shelf-shape noise [0,1]; the Z term makes the reach fold/curl with height (ragged, not a lip). - const float Ns = FractalNoise3D(FVector3f( - WorldX * f + VoxelHash::SeedOffset(SeedU, 17.3f), - WorldY * f + VoxelHash::SeedOffset(SeedU, 23.9f), - WorldZ * f * S.OverhangZScale + VoxelHash::SeedOffset(SeedU, 5.1f)), 3) * 0.5f + 0.5f; // [0,1] - // KEY: the uphill reach GROWS with height in the window (Frac: 0 at ground → 1 at the cap). Low - // down the shift is tiny ⇒ borrows nearby low rock ⇒ stays AIR over the void; high up the shift - // reaches the far cliff ⇒ solid ⇒ the lip sits on top with air UNDERNEATH = a real overhang. - const float Frac = (WorldZ - TerrainZ) / S.OverhangHeight; // (0,1] inside the window - const float ShiftV = S.OverhangReach * OverhangAmp * Frac * Ns; // uphill reach (voxels) - if (ShiftV > 0.5f) - { - // Borrow the uphill STRUCTURAL height (not the full op'd surface): the shelf underside doesn't - // need cliff/terrace refinement, and this avoids re-running Cliff's resamples per lip voxel. - float Ms; - const float ShiftedTZ = SampleSurfaceStructuralZ(WorldX + DirX * ShiftV, WorldY + DirY * ShiftV, S, Ms); - Density = FMath::Max(Density, ShiftedTZ - WorldZ); // union: solid where uphill rock covers Z - } - } - - // Structural fields (Z bounds, seal, base) are forced equal across biomes → S is safe. - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - S.StrateTopWorldZ, S.StrateBottomWorldZ, - S.BoundarySealThickness, S.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - S.StrateTopWorldZ, S.StrateBottomWorldZ, - S.BoundarySealThickness, S.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - S.BaseDensity, S.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - S.StrateTopWorldZ, S.StrateBottomWorldZ, - S.BoundarySealThickness, S.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, S.BaseDensity); - - return -Density; -} - void UVoxelGenerator::ResolveSurfaceChunkParams(const FIntVector& ChunkCoord, FSurfaceGenerationParams& OutSurface, FBiomeContext& OutBiomeCtx, TArray& OutBiomeParams) const @@ -5934,10 +5407,9 @@ bool UVoxelGenerator::GetSurfaceHeightAt(float WorldX, float WorldY, int32 Chunk //============================================================================= // ~84 % des tuiles générées sortent VIDES (tout-roc sous le terrain, tout-air // au-dessus, cap solide) mais payaient quand même le pré-échantillonnage 33³+ -// complet (trace 2026-07-05 : 83 925 GenerateMesh pour 13 227 maillages réels). -// Une 1re tentative (2026-06-26) a été REVERTÉE : borne analytique GLOBALE du -// plafond pas assez conservative (cap bas ⇒ trous dans le toit). Ici on suit la -// prescription du revert : les colonnes sont ÉCHANTILLONNÉES sur le treillis +// complet (une trace : 83 925 GenerateMesh pour 13 227 maillages réels). +// ⚠️ Pas de borne analytique GLOBALE du plafond : pas assez conservative (cap +// bas ⇒ trous dans le toit). Les colonnes sont donc ÉCHANTILLONNÉES sur le treillis // exact du mesher (mêmes fonctions ⇒ mêmes floats ⇒ verdict exact, pas une // estimation), et le cas tout-solide porte des gardes spine/passages/ // disturbances/diff. Tout ce qui n'est pas prouvable ⇒ Mixed (le seul coût d'un @@ -6296,9 +5768,9 @@ static EVoxelTileClass VF_ClassifyBoxRefined(const FVoxelOpStack& Stack, && (StructuralManager->AnyLandingFloorAtLattice( Box, Context.LatticeOriginVoxels, Context.Step) // The passage's authored D-floor is a final MC-space writer, not part of the - // interpreted stack. It was deliberately left out of the old support-floor query - // when the legacy slab was removed; omitting the native-floor reach here lets an - // exact core-lattice AllAir claim skip a tile whose final density is solid. + // interpreted stack, and no support-floor query covers it; omitting the + // native-floor reach here lets an exact core-lattice AllAir claim skip a tile + // whose final density is solid. || StructuralManager->AnyPassageStructuralPostNearLattice( Box, Context.LatticeOriginVoxels, Context.Step)); const bool bHasStructuralCandidate = Whole == EVoxelTileClass::AllSolid @@ -6828,7 +6300,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( bool bCanSolid = true; // "tout le treillis est solide" encore prouvable bool bCanAir = true; // "tout le treillis est air" encore prouvable // ── Catégorisation par Z du treillis : gap bedrock = solide ; hors layout = air constant ; - // SurfaceWorld = test colonne ; un slot cave opt-in = verdict de pile sur sa sous-boîte. ── + // SurfaceWorld = test colonne ; un slot cave = verdict de pile sur sa sous-boîte. ── struct FSurfSlot { int32 BotChunkZ = INT32_MAX; // identité du slot (borne basse de la strate, en chunks) @@ -6909,7 +6381,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( // EST FAUX) et `IsGapChunk` rend false au-dessus du sommet (« open air, NOT a gap »). // Résultat : chaque tuile touchant l'air libre au-dessus du monde entrait dans la // BRANCHE DE CAVE, n'y trouvait aucun slot, et abandonnait — mesuré en jeu à 83 % des - // tuiles classées (`Cave Bail Not Op Stack No Layout` = 1.58 / 1.90). + // tuiles classées. // // La vérité est dans `GetGenerationParams` : hors layout il rend `BaseDensity = -1`, // `RoomDensity = 0`, `WormStrength = 0` — un champ CONSTANT, donc de l'air, sans salle @@ -6992,38 +6464,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( // le pliage générique de la pile — mais SEULEMENT sous des conditions vérifiables, // parce qu'un faux verdict ici est un trou (pas de géométrie, pas de collision). // - // Condition 1 : la strate doit RÉELLEMENT être générée par la pile. Sinon on - // classerait un champ que le mesher ne produira pas. C'est le même drapeau, lu au - // même endroit, que `GetDensityAt`. + // Condition 1 : le slot doit avoir une pile (une définition d'archétype connue). + // Sinon on classerait un champ que le mesher ne produira pas. if (!StrateManager->UsesOperatorStackForChunk(CC)) { - // Attribution DIAGNOSTIQUE uniquement : l'ancien compteur mélangeait une - // strate cave entièrement désactivée avec une tuile de frontière qui avait - // rencontré un slot désactivé avant la garde « slot différent » ci-dessous. - // On résout les bornes APRÈS l'échec du même prédicat ; elles ne changent ni - // la condition, ni le point de retour, ni le verdict. - // Diagnostic attribution only: the old counter mixed a wholly disabled cave - // slot with a boundary tile that met a disabled slot before the different-slot - // guard below. Resolve bounds only after the same predicate fails; classification - // control flow and return value stay unchanged. - int32 FailedTopCZ = 0, FailedBotCZ = 0; - if (!StrateManager->GetStrateChunkZBounds(ChunkZ, FailedTopCZ, FailedBotCZ)) - { - INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackNoLayout); - } - else - { - const int32 TileMinCZ = FloorDivC(MinZ, CHUNK_SIZE); - const int32 TileMaxCZ = FloorDivC(MaxZ, CHUNK_SIZE); - if (TileMinCZ >= FailedBotCZ && TileMaxCZ <= FailedTopCZ) - { - INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackSoleSlot); - } - else - { - INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackBoundaryTile); - } - } + INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack); return EVoxelTileClass::Mixed; } @@ -7143,8 +6588,8 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( bCanAir = false; } // Native passage D-floors are authored structural posts applied after the operator stack. - // They are not the removed legacy support slab, but they still add solid MC samples and - // therefore must kill the AllAir hypothesis before a tile can be skipped. + // They add solid MC samples and therefore must kill the AllAir hypothesis before a tile can + // be skipped. if (bHasPassageStructuralPostCandidate) { bCanAir = false; @@ -7182,7 +6627,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( // `GetGenerationParams` et ses homologues BLENDENT les params dans les bandes de transition : // `Alpha` dépend du chunk Z pour `Gradient`, et du chunk XY EN PLUS pour `Interleaved`. Deux // chunks d'une même sous-boîte peuvent donc porter des params différents — c'est le constat de - // `AUDIT §C2`, confirmé par lecture le 2026-07-28 — et UNE pile ne peut pas représenter DEUX + // `AUDIT §C2` — et UNE pile ne peut pas représenter DEUX // champs. On construit donc les params pour CHAQUE coordonnée de chunk que la boîte touche et // on exige qu'ils soient identiques bit à bit. // @@ -7228,16 +6673,11 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( return EVoxelTileClass::Mixed; // la boîte déborde sur un autre archétype } - // Le drapeau doit tenir sur TOUS les chunks de la boîte, pas seulement sur celui qui a - // déclenché la tentative : un seul chunk hors pile invaliderait le verdict. + // Every chunk of the box must have a stack, not only the one that triggered the + // attempt (redundant with the Z pass today, kept hole-safe). if (!StrateManager->UsesOperatorStackForChunk(CC)) { - // Le passage Z précédent a déjà accepté l'unique slot cave. Avec le layout actuel - // (prédicat indépendant de X/Y), ce recheck est redondant ; un hit nomme donc - // précisément cette garde tardive au lieu d'être agrégé aux opt-ins désactivés. - // The prior Z pass already accepted the sole cave slot. With the current X/Y- - // independent predicate this recheck is redundant, so attribute it separately. - INC_DWORD_STAT(STAT_VoxelForgeCaveBailNotOpStackRecheck); + INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack); return EVoxelTileClass::Mixed; } @@ -7336,8 +6776,8 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( if (!VF_BuildOpStackForChunk(CaveType, Refs, Seed, OriginSpineRadius, StrateManager, TileStack, OpCtx)) { - // Strate dégénérée ou archétype non porté : `GetDensityAt` retomberait sur le `switch`, - // donc la pile ne décrit pas ce que le mesher verra. Aucun verdict. + // Strate dégénérée : `GetDensityAt` y rend de l'air sans pile, donc la pile ne décrit + // pas ce que le mesher verra. Aucun verdict. INC_DWORD_STAT(STAT_VoxelForgeCaveBailNoStack); return EVoxelTileClass::Mixed; } @@ -7428,7 +6868,7 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( // ── Balayage des colonnes XY sur le treillis exact du mesher (marge incluse). Une colonne // tranche chaque z intérieur : air côté MC ⇔ TerrainZ ≤ z ≤ CeilSurf (D = −interne ≥ 0, - // cf. SurfaceDensityFromColumn ; spine/passages ne font QUE de l'air → gardés plus haut). + // cf. FSurfaceColumnSource ; spine/passages ne font QUE de l'air → gardés plus haut). // Pré-passe clairsemée ~5×5 pour tuer vite les tuiles traversées par la surface, puis // passe complète — les colonnes recalculées par la pré-passe restent chaudes dans la boîte. auto TestColumn = [&](int32 gx, int32 gy) -> bool // false ⇒ les deux hypothèses sont mortes @@ -7499,27 +6939,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTileUncached( return bCanSolid ? EVoxelTileClass::AllSolid : EVoxelTileClass::AllAir; } -float UVoxelGenerator::GetSurfaceDensity(float WorldX, float WorldY, float WorldZ, - const FSurfaceGenerationParams& ParamsD, - const FSurfaceGenerationParams& ParamsN, - float NeighborWeight) const -{ - if (ParamsD.StrateTopWorldZ - ParamsD.StrateBottomWorldZ <= 0.0f) return 1.0f; - - // Heightfield: dominant biome, OUTPUT-BLENDED toward the nearest neighbour in the - // border band. Blending heights (not params) keeps borders seamless across any param - // difference. ParamsD == ParamsN, weight 0 ⇒ single eval (bit-identical, no biomes). - float TerrainZ = ComputeSurfaceTerrainZ(WorldX, WorldY, ParamsD); - if (NeighborWeight > 0.0f) - { - TerrainZ = FMath::Lerp(TerrainZ, ComputeSurfaceTerrainZ(WorldX, WorldY, ParamsN), NeighborWeight); - } - const float CeilSurf = ComputeSurfaceCeiling(WorldX, WorldY, ParamsD); - - return SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, TerrainZ, CeilSurf, - /*OverhangAmp*/0.0f, /*DirX*/0.0f, /*DirY*/0.0f, ParamsD); -} - //============================================================================= // CLIMATE & BIOME FIELDS //============================================================================= @@ -7933,593 +7352,3 @@ void UVoxelGenerator::GetBiomeMaterialAt(float WorldX, float WorldY, float World OutBlendWeight = S.NeighborWeight; // 0 inside a cell → ~0.5 at the border } } - -//============================================================================= -// VERTICAL-SHAFT GENERATOR (ECaveGeneratorType::VerticalShafts) -//============================================================================= -// Solid rock carved by hash-placed full-height vertical shafts (cylinders), a deterministic -// drainage tree of horizontal connector tunnels rooted at the origin spine, additive random -// connector loops, and partial ledges inside them. Emphasises climbing and falling. - -float UVoxelGenerator::GetVerticalShaftDensity(float WorldX, float WorldY, float WorldZ, - const FVerticalShaftParams& Params) const -{ - const float StrateHeight = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; - if (StrateHeight <= 0.0f) return 1.0f; - - const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f); - const FVector Pos(WorldX, WorldY, WorldZ); - const uint32 S = (uint32)Seed ^ 0x53686674u; // 'Shft' - - float Density = Params.BaseDensity; // start solid - - const int32 CX = FMath::FloorToInt(WorldX / Spacing); - const int32 CY = FMath::FloorToInt(WorldY / Spacing); - - // The shaft topology is pure, but it is deliberately split into two windows: - // * COLLECT a deterministic halo once during this thread-local cache rebuild. This is the - // source data needed to resolve every 5×5 parent window and the fixed ±3 fallback window. - // * EVALUATE only the INNER 3×3 shaft cylinders. Tree links are collected from a wider - // rebuild-only child window and culled to the current cell before the voxel loop sees - // them. That matters: a capsule from a child to a parent two or three cells away must be - // present in the cells along its segment, not only in the cache centred on the child. - struct FLocalShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; }; - struct FLocalConn - { - FVector A, B; - float Radius = 0.0f; - float FloorZ = 0.0f; - bool bWalkableFloor = false; - }; - thread_local TArray> Shafts; - thread_local TArray> Conns; - thread_local TArray> TreeEmitShafts; - thread_local int32 VS_CX = INT32_MAX, VS_CY = INT32_MAX; - thread_local uint32 VS_Seed = 0xFFFFFFFFu; - thread_local float VS_Spacing = -1.0f, VS_Dens = -1.0f, VS_MinR = -1.0f, VS_MaxR = -1.0f, - VS_Cross = -1.0f, VS_ConnR = -1.0f, VS_Rough = -1.0f, - VS_SpineR = -1.0f, VS_BotZ = FLT_MAX, VS_TopZ = FLT_MAX, - VS_Seal = -1.0f, VS_LedgeSpacing = -1.0f, VS_LedgeDepth = -1.0f; - - if (CX != VS_CX || CY != VS_CY || S != VS_Seed || Spacing != VS_Spacing || - Params.ShaftDensity != VS_Dens || Params.ShaftMinRadius != VS_MinR || Params.ShaftMaxRadius != VS_MaxR || - Params.CrossConnectChance != VS_Cross || Params.ConnectorRadius != VS_ConnR || - Params.SurfaceRoughness != VS_Rough || OriginSpineRadius != VS_SpineR || - Params.StrateBottomWorldZ != VS_BotZ || Params.StrateTopWorldZ != VS_TopZ || - Params.BoundarySealThickness != VS_Seal - || Params.LedgeSpacing != VS_LedgeSpacing || Params.LedgeDepth != VS_LedgeDepth) - { - VS_CX = CX; VS_CY = CY; VS_Seed = S; VS_Spacing = Spacing; - VS_Dens = Params.ShaftDensity; VS_MinR = Params.ShaftMinRadius; VS_MaxR = Params.ShaftMaxRadius; - VS_Cross = Params.CrossConnectChance; VS_ConnR = Params.ConnectorRadius; - VS_Rough = Params.SurfaceRoughness; VS_SpineR = OriginSpineRadius; - VS_BotZ = Params.StrateBottomWorldZ; VS_TopZ = Params.StrateTopWorldZ; - VS_Seal = Params.BoundarySealThickness; - VS_LedgeSpacing = Params.LedgeSpacing; - VS_LedgeDepth = Params.LedgeDepth; - Shafts.Reset(); - Conns.Reset(); - TreeEmitShafts.Reset(); - - constexpr int32 EmitRadius = 1; // 3×3: shaft cylinders and random links - constexpr int32 CandidateRadius = 2; // 5×5 parent window around an emitting shaft - constexpr int32 FallbackRadius = 3; // fixed fallback window around an emitting shaft - constexpr uint32 TreeSalt = 0x7A11u; - - const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness; - const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness; - const float RoughnessReach = FMath::Max(Params.SurfaceRoughness, 0.0f) - * VOXEL_NOISE_SCALE * 1.5f; - // A tree link must remain open at the centreline after the SDF roughness pass. The +1 - // margin makes the radius strictly greater than the proven roughness supremum. - const float TreeConnectorRadius = FMath::Max(Params.ConnectorRadius, RoughnessReach + 1.0f); - // A tree capsule can affect a cell even when neither endpoint is in that cell. Keep a - // rebuild-only child halo for that geometric reach, then add the parent-search halo. The - // default is a 4-cell tree-emission radius + 3-cell collection pad = 7-cell radius - // (15×15 rolls; the emitted tree-child box itself is 9×9). - const int32 ConnectorCellPad = FMath::Max( - 1, FMath::CeilToInt(TreeConnectorRadius / Spacing)); - const int32 TreeEmitRadius = FallbackRadius + ConnectorCellPad; - const int32 CollectRadius = TreeEmitRadius + FallbackRadius; - - const int32 CollectSide = CollectRadius * 2 + 1; - TArray> ShaftGrid; - TArray> ShaftPresent; - ShaftGrid.SetNum(CollectSide * CollectSide); - ShaftPresent.Init(0, CollectSide * CollectSide); - // Collect the wider deterministic neighbourhood once. Only the inner cells are retained - // in `Shafts` for the per-voxel cylinder/ledge work; `TreeEmitShafts` is still rebuilt - // before evaluation and its connectors are spatially culled below. - for (int32 dy = -CollectRadius; dy <= CollectRadius; dy++) - for (int32 dx = -CollectRadius; dx <= CollectRadius; dx++) - { - const int32 nx = CX + dx, ny = CY + dy; - const uint32 Hh = VoxelHash::Cell(nx, ny, S); - if (VoxelHash::ToFloat01(Hh) > Params.ShaftDensity) continue; - - const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x12345678u)); - const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x9ABCDEF0u)); - FLocalShaft Sh; - Sh.X = (nx + 0.15f + JX * 0.7f) * Spacing; - Sh.Y = (ny + 0.15f + JY * 0.7f) * Spacing; - Sh.R = FMath::Lerp(Params.ShaftMinRadius, Params.ShaftMaxRadius, - VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0xBEEFu))); - Sh.CellX = nx; - Sh.CellY = ny; - Sh.bOriginSpine = false; - const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius); - ShaftGrid[GridIndex] = Sh; - ShaftPresent[GridIndex] = 1; - if (FMath::Abs(dx) <= EmitRadius && FMath::Abs(dy) <= EmitRadius) - { - Shafts.Add(Sh); - } - if (FMath::Abs(dx) <= TreeEmitRadius && FMath::Abs(dy) <= TreeEmitRadius) - { - TreeEmitShafts.Add(Sh); - } - } - - auto FindCollectedShaft = [&](int32 CellX, int32 CellY) -> const FLocalShaft* - { - const int32 dx = CellX - CX; - const int32 dy = CellY - CY; - if (FMath::Abs(dx) > CollectRadius || FMath::Abs(dy) > CollectRadius) - { - return nullptr; - } - const int32 GridIndex = (dy + CollectRadius) * CollectSide + (dx + CollectRadius); - return ShaftPresent[GridIndex] ? &ShaftGrid[GridIndex] : nullptr; - }; - - // The structural (0,0) spine is not re-carved here. It participates only as a - // connector endpoint. The local 3x3 set is origin-adjacent when its centre cell is in - // the central 3x3; the structural post remains the sole owner of the vertical column. - const bool bOriginNearby = FMath::Abs(CX) <= 1 && FMath::Abs(CY) <= 1; - if (bOriginNearby && OriginSpineRadius > 0.0f) - { - Shafts.Add({0.0f, 0.0f, OriginSpineRadius, 0, 0, true}); - } - - const float CellMinX = static_cast(CX) * Spacing; - const float CellMaxX = static_cast(CX + 1) * Spacing; - const float CellMinY = static_cast(CY) * Spacing; - const float CellMaxY = static_cast(CY + 1) * Spacing; - auto ConnectorMayReachCell = [&](const FLocalConn& Conn) - { - const float MinX = FMath::Min(Conn.A.X, Conn.B.X) - Conn.Radius; - const float MaxX = FMath::Max(Conn.A.X, Conn.B.X) + Conn.Radius; - const float MinY = FMath::Min(Conn.A.Y, Conn.B.Y) - Conn.Radius; - const float MaxY = FMath::Max(Conn.A.Y, Conn.B.Y) + Conn.Radius; - const float GapX = FMath::Max3(CellMinX - MaxX, MinX - CellMaxX, 0.0f); - const float GapY = FMath::Max3(CellMinY - MaxY, MinY - CellMaxY, 0.0f); - return GapX == 0.0f && GapY == 0.0f; - }; - - auto EmitTreeConnector = [&](const FLocalShaft& Child, const FLocalShaft* Parent) - { - const float Zc = VoxelPassageGeometry::VerticalShaftConnectorCenterZ( - Params.StrateBottomWorldZ, - BottomZ, - TopZ, - Params.LedgeSpacing, - Params.LedgeDepth, - TreeConnectorRadius); - const float FloorZ = VoxelPassageGeometry::VerticalShaftConnectorFloorZ( - Params.StrateBottomWorldZ, - BottomZ, - TopZ, - Params.LedgeSpacing, - Params.LedgeDepth, - TreeConnectorRadius); - const FVector ParentPoint = Parent != nullptr - ? FVector(Parent->X, Parent->Y, Zc) - : FVector(0.0f, 0.0f, Zc); - const FLocalConn Conn{ - FVector(Child.X, Child.Y, Zc), ParentPoint, TreeConnectorRadius, - FloorZ, true }; - if (ConnectorMayReachCell(Conn)) - { - Conns.Add(Conn); - } - }; - - // Structural drainage tree: every real shaft in the rebuild-only child window first - // selects one strictly more-central shaft from its complete 5×5 window. If that window - // has a local minimum, select the nearest shaft in the collected deterministic halo whose - // origin distance is lower; that shaft is already on a strictly descending path by the - // same rule. A shaft whose window reaches the origin uses the spine directly, and the - // final direct-spine fallback covers an unusually empty finite halo. Thus every emitted - // parent edge either strictly decreases origin distance or terminates at the spine: no - // cycles and no orphans. Spatial culling makes the same edge visible throughout its - // capsule, independent of which cache cell is evaluating it. - // The explicit cell-coordinate tie-break makes this independent of array iteration order; - // the synthetic spine is not a candidate shaft. - for (const FLocalShaft& Child : TreeEmitShafts) - { - if (Child.bOriginSpine) continue; - - const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y); - const FLocalShaft* Parent = nullptr; - float BestDistanceSq = FLT_MAX; - for (int32 dy = -CandidateRadius; dy <= CandidateRadius; ++dy) - for (int32 dx = -CandidateRadius; dx <= CandidateRadius; ++dx) - { - const FLocalShaft* Candidate = FindCollectedShaft( - Child.CellX + dx, Child.CellY + dy); - if (Candidate == nullptr - || (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY)) - { - continue; - } - - const float CandidateOriginSq = FMath::Square(Candidate->X) - + FMath::Square(Candidate->Y); - if (!(CandidateOriginSq < ChildOriginSq)) - { - continue; - } - - const float DistanceSq = FMath::Square(Child.X - Candidate->X) - + FMath::Square(Child.Y - Candidate->Y); - const bool bLowerCell = Parent == nullptr - || Candidate->CellY < Parent->CellY - || (Candidate->CellY == Parent->CellY && Candidate->CellX < Parent->CellX); - if (DistanceSq < BestDistanceSq - || (DistanceSq == BestDistanceSq && bLowerCell)) - { - BestDistanceSq = DistanceSq; - Parent = Candidate; - } - } - - if (Parent == nullptr - && !(FMath::Abs(Child.CellX) <= CandidateRadius - && FMath::Abs(Child.CellY) <= CandidateRadius)) - { - // This is the specified neighbour-shaft fallback. Restricting it to a lower - // origin distance makes the parent graph well-founded even though the fallback - // candidate need not be in the child's 5×5 window. - for (int32 dy = -FallbackRadius; dy <= FallbackRadius; ++dy) - for (int32 dx = -FallbackRadius; dx <= FallbackRadius; ++dx) - { - const FLocalShaft* Candidate = FindCollectedShaft( - Child.CellX + dx, Child.CellY + dy); - if (Candidate == nullptr - || (Candidate->CellX == Child.CellX && Candidate->CellY == Child.CellY)) - { - continue; - } - - const float CandidateOriginSq = FMath::Square(Candidate->X) - + FMath::Square(Candidate->Y); - if (!(CandidateOriginSq < ChildOriginSq)) - { - continue; - } - - const float DistanceSq = FMath::Square(Child.X - Candidate->X) - + FMath::Square(Child.Y - Candidate->Y); - const bool bLowerCell = Parent == nullptr - || Candidate->CellY < Parent->CellY - || (Candidate->CellY == Parent->CellY - && Candidate->CellX < Parent->CellX); - if (DistanceSq < BestDistanceSq - || (DistanceSq == BestDistanceSq && bLowerCell)) - { - BestDistanceSq = DistanceSq; - Parent = Candidate; - } - } - } - - EmitTreeConnector(Child, Parent); - } - - // Existing probabilistic links remain as texture and loops. They intentionally stay on - // the cached inner 3×3 working set; only the structural tree build needs the 9×9/15×15 halo. - if (Params.CrossConnectChance > 0.0f && Shafts.Num() >= 2) - { - for (int32 i = 0; i < Shafts.Num(); i++) - for (int32 j = i + 1; j < Shafts.Num(); j++) - { - const FLocalShaft& A = Shafts[i]; - const FLocalShaft& B = Shafts[j]; - const bool bSpineConnector = A.bOriginSpine || B.bOriginSpine; - const float DSq = FMath::Square(A.X - B.X) + FMath::Square(A.Y - B.Y); - if (DSq > FMath::Square(Spacing * 1.6f)) continue; // only neighbours - - // Symmetric pair hash from quantised endpoints. - const uint32 PH = VoxelHash::Pair( - FMath::RoundToInt(A.X), FMath::RoundToInt(A.Y), - FMath::RoundToInt(B.X), FMath::RoundToInt(B.Y), S ^ 0xC04Eu); - if (VoxelHash::ToFloat01(PH) >= Params.CrossConnectChance) continue; - - const float Zc = FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(PH))); - const float ConnectorR = bSpineConnector - ? FMath::Max( - Params.ConnectorRadius, - RoughnessReach + 1.0f) - : Params.ConnectorRadius; - const FLocalConn Conn{ - FVector(A.X, A.Y, Zc), FVector(B.X, B.Y, Zc), ConnectorR, - 0.0f, false }; - if (ConnectorMayReachCell(Conn)) - { - Conns.Add(Conn); - } - } - } - } - - float CaveSDF = FLT_MAX; - - // Vertical shafts as infinite cylinders (boundary seal handles the ends). - for (const FLocalShaft& Sh : Shafts) - { - if (Sh.bOriginSpine) continue; // VF_ApplyOriginSpine owns the structural column. - const float DX = WorldX - Sh.X; - const float DY = WorldY - Sh.Y; - CaveSDF = FMath::Min(CaveSDF, FMath::Sqrt(DX * DX + DY * DY) - Sh.R); - } - for (const FLocalConn& C : Conns) - { - CaveSDF = FMath::Min(CaveSDF, VoxelSDF::Capsule(Pos, C.A, C.B, C.Radius)); - } - - // Wall roughness. - if (Params.SurfaceRoughness > 0.0f && CaveSDF < Params.SurfaceRoughness + 4.0f) - { - CaveSDF += FractalNoise3D(FVector3f(WorldX * 0.1f, WorldY * 0.1f, WorldZ * 0.1f), VoxelGenLOD::Eff(3)) - * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; - } - - // Carve air inside shafts/connectors. - const float Blend = 2.0f; - if (CaveSDF < Blend) - { - float Carve = FMath::Clamp((Blend - CaveSDF) / (Blend * 2.0f), 0.0f, 1.0f); - Carve = SmoothStep01(Carve); - Density -= Carve * Params.BaseDensity * 2.0f; - } - - // Partial ledges inside shafts: thin shelves on one side at LedgeSpacing intervals, - // leaving the opposite side open so the shaft stays traversable. - if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f && CaveSDF < 0.0f && Shafts.Num() > 0) - { - const float Phase = FMath::Frac((WorldZ - Params.StrateBottomWorldZ) / Params.LedgeSpacing); - const float BandT = FMath::Min(Phase, 1.0f - Phase) * Params.LedgeSpacing; // dist to nearest band - if (BandT < Params.LedgeDepth) - { - // Nearest shaft center → only shelf the +X/+Y half so a climb path remains. - const FLocalShaft* Near = nullptr; float BestSq = FLT_MAX; - for (const FLocalShaft& Sh : Shafts) - { - if (Sh.bOriginSpine) continue; // ledges belong to real shafts, not the post. - const float D2 = FMath::Square(WorldX - Sh.X) + FMath::Square(WorldY - Sh.Y); - if (D2 < BestSq) { BestSq = D2; Near = &Sh; } - } - // The shaft axis is the structural tree's terminal point. Keep the mathematical - // half-plane boundary open: tiny cancellation error must not turn an exact axis - // landing into a solid ledge cap and sever the parent capsule. - if (Near && (WorldX - Near->X) + (WorldY - Near->Y) > 1.0e-3f) - { - float Shelf = 1.0f - SmoothStep01(BandT / Params.LedgeDepth); - Density = FMath::Max(Density, Shelf * Params.BaseDensity); - } - } - } - - // The drainage tree is a walkable shaft network, not only a connected air SDF. Reassert its - // air core after partial shaft ledges, then emit support bands at the common first ledge level - // so every parent edge can be traversed by the player-fit stencil. This remains local to - // shaft edges; no landing-to-origin road is added. - for (const FLocalConn& C : Conns) - { - if (C.bWalkableFloor) - { - VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorAir( - Density, WorldX, WorldY, WorldZ, - C.A, C.B, C.Radius, C.FloorZ, Params.BaseDensity); - } - } - for (const FLocalConn& C : Conns) - { - if (C.bWalkableFloor) - { - VoxelPassageGeometry::VF_ApplyVerticalShaftConnectorFloor( - Density, WorldX, WorldY, WorldZ, - C.A, C.B, C.Radius, C.FloorZ, Params.BaseDensity); - } - } - - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); - - return -Density; -} - -//============================================================================= -// FLOATING-ISLAND GENERATOR (ECaveGeneratorType::FloatingIslands) -//============================================================================= -// A large open void with hash-placed island blobs (flattened-top ellipsoids, rough -// undersides) floating at jittered heights. Top/bottom sealed so the void encloses. - -float UVoxelGenerator::GetFloatingIslandDensity(float WorldX, float WorldY, float WorldZ, - const FFloatingIslandParams& Params) const -{ - const float H = Params.StrateTopWorldZ - Params.StrateBottomWorldZ; - if (H <= 0.0f) return 1.0f; - - const float Spacing = FMath::Max(Params.IslandSpacing, 1.0f); - const uint32 S = (uint32)Seed ^ 0x49736C64u; // 'Isld' - const float BlendK = FMath::Max(Params.SDFBlendRadius, 0.01f); - - float Density = -Params.BaseDensity; // start as open air (void) - - const int32 CX = FMath::FloorToInt(WorldX / Spacing); - const int32 CY = FMath::FloorToInt(WorldY / Spacing); - - const float MidZ = (Params.StrateTopWorldZ + Params.StrateBottomWorldZ) * 0.5f; - - float IslandSDF = FLT_MAX; - - // IRREGULAR OUTLINE: domain-warp the horizontal query so island edges are lobed and - // organic instead of perfect circles. Computed once per voxel and shared by all nearby - // islands (each samples a different part of the field → distinct silhouettes). - const float WarpAmp = (Params.IslandMinRadius + Params.IslandMaxRadius) * 0.5f * 0.35f; - // ⚠️ AUDIT §C1 — DERNIER SITE DU PLUGIN, trouvé en portant cet archétype (2026-07-28). Le - // balayage du 2026-07-27 cherchait le motif `SeedF * K` et celui-ci s'écrit `(float)S * K`, donc - // il a survécu : à Seed = 2e9 le terme atteint ~1.4e6, où l'ULP du float vaut 0.125 contre un pas - // de 0.04 par voxel — le warp s'aplatit et les îles redeviennent des cercles parfaits. Corrigé - // dans les DEUX chemins (ici et FIslandBlobSource) en une passe, pour que le test d'équivalence - // reste un oracle valable. - // NOTE : `SeedOffset` quantifie la clé de site par ×100, donc 0.0007 → site 0. Unique aujourd'hui - // (toutes les autres clés du plugin sont ≥ 0.19) ; la prochaine clé sous 0.005 devra en choisir - // une autre plutôt que de collisionner en silence. - const float WX = WorldX + FractalNoise3D(FVector3f(WorldX * 0.04f + VoxelHash::SeedOffset(S, 0.0007f), WorldY * 0.04f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) - * VOXEL_NOISE_SCALE * WarpAmp; - const float WY = WorldY + FractalNoise3D(FVector3f(WorldX * 0.04f + 31.0f, WorldY * 0.04f + 7.0f, WorldZ * 0.012f), VoxelGenLOD::Eff(3)) - * VOXEL_NOISE_SCALE * WarpAmp; - - // Per-island constants (existence roll, jitter, radius, Z anchor, taper) are pure functions - // of (cell, seed, params) yet were re-hashed PER VOXEL. Bake the 3×3 neighbourhood's islands - // once per centre cell (thread_local); the per-voxel work keeps only the warped-frame - // distance / taper / top-surface math (those genuinely vary per voxel). - struct FLocalIsland { float X, Y, Rxy, TopHalf, TopZ, BotZ, TaperEnd; }; - thread_local TArray> Islands; - thread_local int32 FI_CX = INT32_MAX, FI_CY = INT32_MAX; - thread_local uint32 FI_Seed = 0xFFFFFFFFu; - thread_local float FI_Spacing = -1.0f, FI_Dens = -1.0f, FI_MinR = -1.0f, FI_MaxR = -1.0f, - FI_Thick = -1.0f, FI_VJit = -1.0f, FI_BotZ = FLT_MAX, FI_TopZ = FLT_MAX, - FI_Seal = -1.0f; - - if (CX != FI_CX || CY != FI_CY || S != FI_Seed || Spacing != FI_Spacing || - Params.IslandDensity != FI_Dens || Params.IslandMinRadius != FI_MinR || Params.IslandMaxRadius != FI_MaxR || - Params.ThicknessRatio != FI_Thick || Params.VerticalJitter != FI_VJit || - Params.StrateBottomWorldZ != FI_BotZ || Params.StrateTopWorldZ != FI_TopZ - || Params.BoundarySealThickness != FI_Seal) - { - FI_CX = CX; FI_CY = CY; FI_Seed = S; FI_Spacing = Spacing; - FI_Dens = Params.IslandDensity; FI_MinR = Params.IslandMinRadius; FI_MaxR = Params.IslandMaxRadius; - FI_Thick = Params.ThicknessRatio; FI_VJit = Params.VerticalJitter; - FI_BotZ = Params.StrateBottomWorldZ; FI_TopZ = Params.StrateTopWorldZ; - FI_Seal = Params.BoundarySealThickness; - Islands.Reset(); - - for (int32 dy = -1; dy <= 1; dy++) - for (int32 dx = -1; dx <= 1; dx++) - { - const int32 nx = CX + dx, ny = CY + dy; - const uint32 Hh = VoxelHash::Cell(nx, ny, S); - if (VoxelHash::ToFloat01(Hh) > Params.IslandDensity) continue; - - const float JX = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x12345678u)); - const float JY = VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x9ABCDEF0u)); - - FLocalIsland& Isl = Islands.AddDefaulted_GetRef(); - Isl.X = (nx + 0.15f + JX * 0.7f) * Spacing; - Isl.Y = (ny + 0.15f + JY * 0.7f) * Spacing; - Isl.Rxy = FMath::Lerp(Params.IslandMinRadius, Params.IslandMaxRadius, - VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x5A5Au))); - - // ASYMMETRIC ISLAND PROFILE: a fairly flat land slab on TOP, and an underside that - // tapers DOWN to a rough point (the hanging "roots"). ThicknessRatio scales how deep - // the underside hangs. This is what reads as a floating island vs. a sphere. - Isl.TopHalf = Isl.Rxy * 0.20f; // land slab above centre - const float UnderDepth = Isl.Rxy * FMath::Max(Params.ThicknessRatio, 0.25f); // tapering underside - - const float SpreadZ = FMath::Max(H * 0.5f - FMath::Max(Isl.TopHalf, UnderDepth) - Params.BoundarySealThickness, 0.0f) - * Params.VerticalJitter; - const float Cz = MidZ + VoxelHash::ToFloatSigned(VoxelHash::Mix(Hh ^ 0xB17Du)) * SpreadZ; - Isl.TopZ = Cz + Isl.TopHalf; - Isl.BotZ = Cz - UnderDepth; - - // Per-island taper sharpness (SmoothStep end point) for varied silhouettes. - Isl.TaperEnd = FMath::Lerp(0.45f, 0.7f, VoxelHash::ToFloat01(VoxelHash::Mix(Hh ^ 0x7A1Eu))); - } - } - - for (const FLocalIsland& Isl : Islands) - { - // Horizontal distance in the WARPED (lobed) frame so the outline isn't a circle. - const float Dxw = WX - Isl.X, Dyw = WY - Isl.Y; - const float DistXY = FMath::Sqrt(Dxw * Dxw + Dyw * Dyw); - - // Radius envelope by height: full width across the top, narrowing to a point at the - // bottom tip (SmoothStep taper). - const float Hgt = FMath::Clamp((WorldZ - Isl.BotZ) / FMath::Max(Isl.TopZ - Isl.BotZ, 1.0f), 0.0f, 1.0f); - const float Taper = SmoothStep01(FMath::Clamp(Hgt / Isl.TaperEnd, 0.0f, 1.0f)); - const float Env = Isl.Rxy * Taper; - - // Top surface: flat by default; dome the edges down when TopFlatten < 1. - float TopSurf = Isl.TopZ; - if (Params.TopFlatten < 1.0f) - { - const float Edge = FMath::Clamp(DistXY / FMath::Max(Isl.Rxy, 1.0f), 0.0f, 1.0f); - TopSurf = Isl.TopZ - (1.0f - Params.TopFlatten) * Isl.TopHalf * 2.0f * Edge * Edge; - } - - // Pseudo-SDF: outside if beyond the radial envelope OR above the top surface. - const float Sdf = FMath::Max(DistXY - Env, WorldZ - TopSurf); - - IslandSDF = VoxelSDF::SmoothMin(IslandSDF, Sdf, BlendK); - } - - // Craggy shells. - if (Params.SurfaceRoughness > 0.0f && IslandSDF < Params.SurfaceRoughness + BlendK + 2.0f) - { - IslandSDF += FractalNoise3D(FVector3f(WorldX * 0.08f, WorldY * 0.08f, WorldZ * 0.08f), VoxelGenLOD::Eff(4)) - * VOXEL_NOISE_SCALE * Params.SurfaceRoughness; - } - - // Fill solid inside islands. - const float Blend = BlendK; - if (IslandSDF < Blend) - { - float Fill = FMath::Clamp((Blend - IslandSDF) / (Blend * 2.0f), 0.0f, 1.0f); - Fill = SmoothStep01(Fill); - Density += Fill * Params.BaseDensity * 2.0f; - } - - ApplyOriginSpine(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - ApplyBoundarySeal(Density, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity); - - if (StrateManager) - { - StrateManager->ApplyPassageModifier( - Density, WorldX, WorldY, WorldZ, - Params.BaseDensity, Params.BoundarySealThickness); - } - ApplyOriginLandingFloor(Density, WorldX, WorldY, WorldZ, - Params.StrateTopWorldZ, Params.StrateBottomWorldZ, - Params.BoundarySealThickness, Params.BaseDensity, OriginSpineRadius); - - VF_ApplyXYEdgeSeal(Density, WorldX, WorldY, - WorldRadiusVoxels, EdgeSealThickness, Params.BaseDensity); - - return -Density; -} diff --git a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index a5f241d..304302e 100644 --- a/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -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* 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 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 GroundIdx; - static thread_local TArray 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 SheetGroundTris; - static thread_local TArray 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& 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& 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; -} diff --git a/Source/VoxelForge/Private/VoxelSeasonManifest.cpp b/Source/VoxelForge/Private/VoxelSeasonManifest.cpp index f029b29..9b015d2 100644 --- a/Source/VoxelForge/Private/VoxelSeasonManifest.cpp +++ b/Source/VoxelForge/Private/VoxelSeasonManifest.cpp @@ -1410,59 +1410,6 @@ namespace VoxelSeasonManifestPrivate return bValid; } - bool SaveJson(const FString& FilePath, const TSharedPtr& 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>> Writer = - TJsonWriterFactory>::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& 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> 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 diff --git a/Source/VoxelForge/Private/VoxelStackSampler.cpp b/Source/VoxelForge/Private/VoxelStackSampler.cpp index 1f49c85..6cf6655 100644 --- a/Source/VoxelForge/Private/VoxelStackSampler.cpp +++ b/Source/VoxelForge/Private/VoxelStackSampler.cpp @@ -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 diff --git a/Source/VoxelForge/Private/VoxelStats.cpp b/Source/VoxelForge/Private/VoxelStats.cpp index d70d2de..94105ce 100644 --- a/Source/VoxelForge/Private/VoxelStats.cpp +++ b/Source/VoxelForge/Private/VoxelStats.cpp @@ -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); diff --git a/Source/VoxelForge/Private/VoxelStrateComposer.cpp b/Source/VoxelForge/Private/VoxelStrateComposer.cpp index 33fa904..9bc9877 100644 --- a/Source/VoxelForge/Private/VoxelStrateComposer.cpp +++ b/Source/VoxelForge/Private/VoxelStrateComposer.cpp @@ -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 SettingsPaths; - if (Settings != nullptr) - { - TSet UniquePaths; - for (const TPair>& Pair : Settings->FixedStrates) - { - const FString Path = Pair.Value.ToSoftObjectPath().ToString(); - if (!Path.IsEmpty()) - { - UniquePaths.Add(Path); - } - } - for (const TSoftObjectPtr& 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 }; diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index 08fbc52..5fc1413 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -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(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(B) << 32) | static_cast(A); - return Result != 0 ? Result : 1; -} diff --git a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp index 85b7ac5..3474b9a 100644 --- a/Source/VoxelForge/Private/VoxelStrateMeasure.cpp +++ b/Source/VoxelForge/Private/VoxelStrateMeasure.cpp @@ -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& 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, diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index aa8588f..a956d42 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -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 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(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(PlayerPawn)) { @@ -4577,12 +4562,11 @@ void AVoxelWorld::Tick(float DeltaTime) const double SpanZ = static_cast(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& 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(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)); + Summary.Max.X, Summary.Max.Y, Summary.Max.Z, + *FString::SanitizeFloat(TileExtentMetres), + *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* ObsoleteFlag) { @@ -7666,7 +7541,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector / static_cast(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,13 +7855,10 @@ 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, - bWantCapture ? &Result.CaptureGrid : nullptr, - BandVoxLo, BandVoxHi, - &Result.MesherDensityCalls); + MeshData = Mesher->GenerateMesh(OriginVoxels, Step, Cells, + bWantCapture ? &Result.CaptureGrid : nullptr, + BandVoxLo, BandVoxHi, + &Result.MesherDensityCalls); TotalMesherDensityCalls.fetch_add( static_cast(Result.MesherDensityCalls), std::memory_order_relaxed); MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0; @@ -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); } diff --git a/Source/VoxelForge/Private/VoxelWormField.cpp b/Source/VoxelForge/Private/VoxelWormField.cpp index dd7d793..229363e 100644 --- a/Source/VoxelForge/Private/VoxelWormField.cpp +++ b/Source/VoxelForge/Private/VoxelWormField.cpp @@ -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, diff --git a/Source/VoxelForge/Public/VoxelBiomeDefinition.h b/Source/VoxelForge/Public/VoxelBiomeDefinition.h index 92ab3ff..4b1d451 100644 --- a/Source/VoxelForge/Public/VoxelBiomeDefinition.h +++ b/Source/VoxelForge/Public/VoxelBiomeDefinition.h @@ -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; /** diff --git a/Source/VoxelForge/Public/VoxelCaveMorphology.h b/Source/VoxelForge/Public/VoxelCaveMorphology.h index 9acfdc1..ffe22c9 100644 --- a/Source/VoxelForge/Public/VoxelCaveMorphology.h +++ b/Source/VoxelForge/Public/VoxelCaveMorphology.h @@ -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 - ); } /** diff --git a/Source/VoxelForge/Public/VoxelContentManager.h b/Source/VoxelForge/Public/VoxelContentManager.h index b9185a8..a41cc5b 100644 --- a/Source/VoxelForge/Public/VoxelContentManager.h +++ b/Source/VoxelForge/Public/VoxelContentManager.h @@ -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. // diff --git a/Source/VoxelForge/Public/VoxelDensityOp.h b/Source/VoxelForge/Public/VoxelDensityOp.h index 9d06bc9..2b82d91 100644 --- a/Source/VoxelForge/Public/VoxelDensityOp.h +++ b/Source/VoxelForge/Public/VoxelDensityOp.h @@ -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 diff --git a/Source/VoxelForge/Public/VoxelDensityOpStack.h b/Source/VoxelForge/Public/VoxelDensityOpStack.h index 7814178..73cc481 100644 --- a/Source/VoxelForge/Public/VoxelDensityOpStack.h +++ b/Source/VoxelForge/Public/VoxelDensityOpStack.h @@ -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 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 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, diff --git a/Source/VoxelForge/Public/VoxelDensityPrimitives.h b/Source/VoxelForge/Public/VoxelDensityPrimitives.h index 893a8ae..7fb17f4 100644 --- a/Source/VoxelForge/Public/VoxelDensityPrimitives.h +++ b/Source/VoxelForge/Public/VoxelDensityPrimitives.h @@ -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; } diff --git a/Source/VoxelForge/Public/VoxelDensityProfile.h b/Source/VoxelForge/Public/VoxelDensityProfile.h index 5502d92..6e9abeb 100644 --- a/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -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. diff --git a/Source/VoxelForge/Public/VoxelDensityVolume.h b/Source/VoxelForge/Public/VoxelDensityVolume.h index f410edb..bedc114 100644 --- a/Source/VoxelForge/Public/VoxelDensityVolume.h +++ b/Source/VoxelForge/Public/VoxelDensityVolume.h @@ -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. diff --git a/Source/VoxelForge/Public/VoxelGenerator.h b/Source/VoxelForge/Public/VoxelGenerator.h index 8e49d68..11405ed 100644 --- a/Source/VoxelForge/Public/VoxelGenerator.h +++ b/Source/VoxelForge/Public/VoxelGenerator.h @@ -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& 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, diff --git a/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index 098644e..32e2f44 100644 --- a/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -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) //========================================================================= diff --git a/Source/VoxelForge/Public/VoxelPassageGeometry.h b/Source/VoxelForge/Public/VoxelPassageGeometry.h index a4b035d..b4c6a12 100644 --- a/Source/VoxelForge/Public/VoxelPassageGeometry.h +++ b/Source/VoxelForge/Public/VoxelPassageGeometry.h @@ -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. diff --git a/Source/VoxelForge/Public/VoxelSeasonAsset.h b/Source/VoxelForge/Public/VoxelSeasonAsset.h index 049173b..7eea906 100644 --- a/Source/VoxelForge/Public/VoxelSeasonAsset.h +++ b/Source/VoxelForge/Public/VoxelSeasonAsset.h @@ -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") diff --git a/Source/VoxelForge/Public/VoxelSeasonManifest.h b/Source/VoxelForge/Public/VoxelSeasonManifest.h index 99d728c..c6a5c5d 100644 --- a/Source/VoxelForge/Public/VoxelSeasonManifest.h +++ b/Source/VoxelForge/Public/VoxelSeasonManifest.h @@ -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( diff --git a/Source/VoxelForge/Public/VoxelSettings.h b/Source/VoxelForge/Public/VoxelSettings.h index d9c5131..31297db 100644 --- a/Source/VoxelForge/Public/VoxelSettings.h +++ b/Source/VoxelForge/Public/VoxelSettings.h @@ -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. diff --git a/Source/VoxelForge/Public/VoxelStackSampler.h b/Source/VoxelForge/Public/VoxelStackSampler.h index c6dfe5b..9310f77 100644 --- a/Source/VoxelForge/Public/VoxelStackSampler.h +++ b/Source/VoxelForge/Public/VoxelStackSampler.h @@ -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(); diff --git a/Source/VoxelForge/Public/VoxelStartupTrace.h b/Source/VoxelForge/Public/VoxelStartupTrace.h index 305e286..3f4a921 100644 --- a/Source/VoxelForge/Public/VoxelStartupTrace.h +++ b/Source/VoxelForge/Public/VoxelStartupTrace.h @@ -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; diff --git a/Source/VoxelForge/Public/VoxelStats.h b/Source/VoxelForge/Public/VoxelStats.h index 170aaa6..9cc9d81 100644 --- a/Source/VoxelForge/Public/VoxelStats.h +++ b/Source/VoxelForge/Public/VoxelStats.h @@ -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); diff --git a/Source/VoxelForge/Public/VoxelStrateComposer.h b/Source/VoxelForge/Public/VoxelStrateComposer.h index a865056..d832a15 100644 --- a/Source/VoxelForge/Public/VoxelStrateComposer.h +++ b/Source/VoxelForge/Public/VoxelStrateComposer.h @@ -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& 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& GetEntries() const { return Entries; } const TArray& GetFieldSpreads() const { return FieldSpreads; } - const TArray& 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); diff --git a/Source/VoxelForge/Public/VoxelStrateDefinition.h b/Source/VoxelForge/Public/VoxelStrateDefinition.h index 8c98433..e0714e6 100644 --- a/Source/VoxelForge/Public/VoxelStrateDefinition.h +++ b/Source/VoxelForge/Public/VoxelStrateDefinition.h @@ -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 diff --git a/Source/VoxelForge/Public/VoxelStrateManager.h b/Source/VoxelForge/Public/VoxelStrateManager.h index 63371dc..ed56bbb 100644 --- a/Source/VoxelForge/Public/VoxelStrateManager.h +++ b/Source/VoxelForge/Public/VoxelStrateManager.h @@ -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; diff --git a/Source/VoxelForge/Public/VoxelStrateMeasure.h b/Source/VoxelForge/Public/VoxelStrateMeasure.h index 8ea6041..f427aa0 100644 --- a/Source/VoxelForge/Public/VoxelStrateMeasure.h +++ b/Source/VoxelForge/Public/VoxelStrateMeasure.h @@ -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, diff --git a/Source/VoxelForge/Public/VoxelStrateTypes.h b/Source/VoxelForge/Public/VoxelStrateTypes.h index e43b88c..b7b514b 100644 --- a/Source/VoxelForge/Public/VoxelStrateTypes.h +++ b/Source/VoxelForge/Public/VoxelStrateTypes.h @@ -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 diff --git a/Source/VoxelForge/Public/VoxelTilePostReach.h b/Source/VoxelForge/Public/VoxelTilePostReach.h index f2acbe9..4abfe8e 100644 --- a/Source/VoxelForge/Public/VoxelTilePostReach.h +++ b/Source/VoxelForge/Public/VoxelTilePostReach.h @@ -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)); - } - } } diff --git a/Source/VoxelForge/Public/VoxelTypes.h b/Source/VoxelForge/Public/VoxelTypes.h index 98a4e3f..358c5bd 100644 --- a/Source/VoxelForge/Public/VoxelTypes.h +++ b/Source/VoxelForge/Public/VoxelTypes.h @@ -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 int32 CHUNK_SIZE = 32; 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) //============================================================================= diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index 3df6cb5..879f1c4 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -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* ObsoleteFlag = nullptr); @@ -933,15 +930,6 @@ public: // result cannot publish and the post-carve result is admitted immediately. TSet 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 bShuttingDown{false}; @@ -1018,8 +1006,7 @@ public: TArray ReadyTransitionVisibilitySamples; TArray 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 TotalWorkerGenerationCycles{0}; std::atomic 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 DesiredStamped; uint32 DesiredStamp = 0; bool IsDesired(const FVoxelTileKey& T) const diff --git a/Source/VoxelForge/Public/VoxelWormField.h b/Source/VoxelForge/Public/VoxelWormField.h index c98406b..68d113e 100644 --- a/Source/VoxelForge/Public/VoxelWormField.h +++ b/Source/VoxelForge/Public/VoxelWormField.h @@ -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, diff --git a/Source/VoxelForge/VoxelForge.Build.cs b/Source/VoxelForge/VoxelForge.Build.cs index ede695e..bf83ca7 100644 --- a/Source/VoxelForge/VoxelForge.Build.cs +++ b/Source/VoxelForge/VoxelForge.Build.cs @@ -26,7 +26,7 @@ public class VoxelForge : ModuleRules // // Precise resolves to "/fp:precise" on MSVC and "-ffp-contract=off" on Clang — both // IEEE-754 compliant with no FMA contraction, so the two toolchains agree BY CONSTRUCTION - // rather than by luck. That is the fix for AUDIT-2026-07.md C9. + // rather than by luck. That is the fix for Docs/archive/AUDIT-2026-07.md C9. // // COST: /fp:precise forbids the reassociation and contraction /fp:fast allowed, on a // noise-heavy hot path. Expect a measurable perf regression and check it against diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 11223a0..d96c435 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -314,7 +314,6 @@ struct FExploreArguments bool bRender = true; bool bWalk = true; bool bExport = true; - bool bUseOperatorStack = true; bool bWormsEnabledOverride = false; bool bWormsEnabled = true; bool bWormStrengthOverride = false; @@ -431,9 +430,6 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr } ModesText.TrimQuotesInline(); } - int32 UseOperatorStack = 1; - FParse::Value(*Params, TEXT("opstack="), UseOperatorStack); - OutArguments.bUseOperatorStack = UseOperatorStack != 0; int32 WormsEnabled = 1; if (FParse::Value(*Params, TEXT("wormsenabled="), WormsEnabled)) { @@ -739,7 +735,7 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr OutError = TEXT("rendermaxdistance must be finite and in (0,2048] voxels."); return false; } - // Render no longer budgets density samples: pixels are rasterised from the one canonical + // Render does not budget density samples: pixels are rasterised from the one canonical // mesh. Keep the legacy renderstep/maxdistance arguments for compatible invocations and use // only maxdistance as the raster depth clip. if (OutArguments.ExportSize < CHUNK_SIZE @@ -1218,9 +1214,6 @@ struct FExploreWorld Definition->GeneratorType = Index == Arguments.Slot ? Arguments.Archetype : ECaveGeneratorType::TunnelNetwork; - // Explicitly select the production switch so the same synthetic layout, seed and - // mesher invocation can be measured through both density branches. - Definition->bUseOperatorStack = Arguments.bUseOperatorStack; if (Arguments.bWormsEnabledOverride) { Definition->bEnableWorms = Arguments.bWormsEnabled; @@ -1376,7 +1369,7 @@ struct FExploreWorld const FStrateGenerationParams& ReportParams = Target.Definition->GenerationParams; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeExplore][OwnerAssets] strate=%s settings=%s slot=%d " - "seed=%d height_chunks=%d archetype=%s opstack=%d " + "seed=%d height_chunks=%d archetype=%s " "floor_switch_ignored=1 floor_terracing=%d floor_step=%.9g max_ledge=%.9g " "gentle_slope_degrees=%.9g gentle_gradient=%.9g route_cap=%.9g " "wind_wave_cap=%d " @@ -1385,7 +1378,7 @@ struct FExploreWorld bUseAuthoredStrate ? *Arguments.StrateReference : TEXT("synthetic"), bUseAuthoredSettings ? *Arguments.SettingsReference : TEXT("synthetic"), Arguments.Slot, Settings->Seed, Target.Definition->StrateHeightInChunks, - ArchetypeName(TargetArchetype), Target.Definition->bUseOperatorStack ? 1 : 0, + ArchetypeName(TargetArchetype), ReportParams.bTunnelFloorTerracingEnabled ? 1 : 0, ReportParams.TunnelFloorTerraceStepHeight, ReportParams.TunnelFloorMaxLedgeHeight, @@ -3610,57 +3603,6 @@ bool RunWalk( OutOutput.bHasArrival = true; OutOutput.bHasDeparture = true; - // Temporary seed-14 probe while diagnosing the fitted VerticalShafts junction. This is - // intentionally narrow and will be removed after the floor/tree seam is corrected. - if (Arguments.Archetype == ECaveGeneratorType::VerticalShafts && Arguments.Seed == 14) - { - const FVector ProbeA = OutOutput.ArrivalVoxels; - const FVector ProbeB(-100.8f, -26.9f, ProbeA.Z); - for (int32 Index = 0; Index <= 10; ++Index) - { - const float T = static_cast(Index) / 10.0f; - const FVector P = FMath::Lerp(ProbeA, ProbeB, T); - const float Dm704 = World.Generator->GetDensityAt(P.X, P.Y, -704.0f); - const float Dm702 = World.Generator->GetDensityAt(P.X, P.Y, -702.0f); - const float Dm7005 = World.Generator->GetDensityAt(P.X, P.Y, -700.5f); - const float Dm700 = World.Generator->GetDensityAt(P.X, P.Y, -700.0f); - const float Dm6995 = World.Generator->GetDensityAt(P.X, P.Y, -699.5f); - const float Dm697 = World.Generator->GetDensityAt(P.X, P.Y, -697.0f); - float MinBody = FLT_MAX; - int32 NumSolidBody = 0; - int32 SolidOffsetX = 0; - int32 SolidOffsetY = 0; - int32 SolidBodyRow = -1; - for (int32 OffsetY = -1; OffsetY <= 1; ++OffsetY) - { - for (int32 OffsetX = -1; OffsetX <= 1; ++OffsetX) - { - for (int32 BodyRow = 0; BodyRow < 8; ++BodyRow) - { - const float BodyDensity = World.Generator->GetDensityAt( - P.X + static_cast(OffsetX), - P.Y + static_cast(OffsetY), - -699.5f + static_cast(BodyRow)); - if (BodyDensity < MinBody) - { - MinBody = BodyDensity; - SolidOffsetX = OffsetX; - SolidOffsetY = OffsetY; - SolidBodyRow = BodyRow; - } - NumSolidBody += BodyDensity <= 0.0f ? 1 : 0; - } - } - } - UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeExplore][TemporaryShaftProbe] t=%.2f xy=(%.2f,%.2f) " - "d[-704,-702,-700.5,-700,-699.5,-697]=[%.3f,%.3f,%.3f,%.3f,%.3f,%.3f] " - "bodyMin=%.3f bodySolid=%d/72 firstMinOffset=(%d,%d) row=%d"), - T, P.X, P.Y, Dm704, Dm702, Dm7005, Dm700, Dm6995, Dm697, - MinBody, NumSolidBody, SolidOffsetX, SolidOffsetY, SolidBodyRow); - } - } - FVoxelStrateMeasureSettings Settings; Settings.SampleStep = 1; Settings.MaxCells = Arguments.MaxWalkCells; @@ -4243,76 +4185,6 @@ bool WriteObjHeader(FArchive& Archive) return bOk && !Archive.IsError(); } -bool WriteObjTile( - FArchive& Archive, - const FVoxelMeshData& MeshData, - int32 VertexOffset, - FString& OutError) -{ - if (!ValidateCanonicalTile(MeshData, OutError)) - { - return false; - } - bool bOk = true; - for (const FVector& Vertex : MeshData.Vertices) - { - bOk = bOk && WriteUtf8(Archive, FString::Printf( - TEXT("v %.9f %.9f %.9f\n"), - Vertex.X / 100.0, - Vertex.Y / 100.0, - Vertex.Z / 100.0)); - } - for (const FVector2D& UV : MeshData.UVs) - { - bOk = bOk && WriteUtf8(Archive, FString::Printf( - TEXT("vt %.9f %.9f\n"), UV.X, UV.Y)); - } - for (const FVector& Normal : MeshData.Normals) - { - bOk = bOk && WriteUtf8(Archive, FString::Printf( - TEXT("vn %.9f %.9f %.9f\n"), Normal.X, Normal.Y, Normal.Z)); - } - - const int32 NumTriangles = MeshData.Triangles.Num() / 3; - const int32 FirstCeilingTriangle = FMath::Clamp( - NumTriangles - MeshData.NumCeilingTriangles, 0, NumTriangles); - if (NumTriangles > 0) - { - bOk = bOk && WriteUtf8(Archive, TEXT("usemtl VoxelGround\n")); - } - for (int32 Triangle = 0; Triangle < NumTriangles; ++Triangle) - { - if (Triangle == FirstCeilingTriangle) - { - bOk = bOk && WriteUtf8(Archive, TEXT("usemtl VoxelCeiling\n")); - } - const int32 Base = Triangle * 3; - const int32 SourceA = MeshData.Triangles[Base]; - const int32 SourceB = MeshData.Triangles[Base + 1]; - const int32 SourceC = MeshData.Triangles[Base + 2]; - if (SourceA < 0 || SourceB < 0 || SourceC < 0 - || SourceA >= MeshData.Vertices.Num() - || SourceB >= MeshData.Vertices.Num() - || SourceC >= MeshData.Vertices.Num()) - { - OutError = TEXT("Canonical mesher returned an out-of-range triangle index."); - return false; - } - const int32 A = SourceA + 1 + VertexOffset; - const int32 B = SourceB + 1 + VertexOffset; - const int32 C = SourceC + 1 + VertexOffset; - bOk = bOk && WriteUtf8(Archive, FString::Printf( - TEXT("f %d/%d/%d %d/%d/%d %d/%d/%d\n"), - A, A, A, B, B, B, C, C, C)); - } - if (!bOk || Archive.IsError()) - { - OutError = TEXT("Could not write a canonical OBJ tile."); - return false; - } - return true; -} - bool WriteObjMesh( FArchive& Archive, const FVoxelMeshData& MeshData, @@ -4574,7 +4446,6 @@ FString BuildManifestJson( Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); - Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("worms_enabled_override"), Arguments.bWormsEnabledOverride); if (Arguments.bWormsEnabledOverride) { @@ -6256,7 +6127,6 @@ FString BuildExploreJson( Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); - Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("worms_enabled_override"), Arguments.bWormsEnabledOverride); if (Arguments.bWormsEnabledOverride) { @@ -6337,12 +6207,11 @@ FString BuildExploreJson( Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory); Writer->WriteValue(TEXT("canonical_invocation"), FString::Printf( TEXT("UnrealEditor-Cmd VoxelM.uproject -run=VoxelForgeExplore -seed=%d -archetype=%s " - "-slot=%d -opstack=%d -modes=%s -blockearlyout=%d " + "-slot=%d -modes=%s -blockearlyout=%d " "-out="), Arguments.Seed, ArchetypeName(Arguments.Archetype), Arguments.Slot, - Arguments.bUseOperatorStack ? 1 : 0, *Arguments.CanonicalModes(), Arguments.bBlockEarlyOut ? 1 : 0)); Writer->WriteObjectEnd(); @@ -7091,29 +6960,6 @@ FString BuildExploreJson( return Writer->Close() ? Json : FString(); } -const TCHAR* ExploreEffectName(EVoxelOpEffect Effect) -{ - switch (Effect) - { - case EVoxelOpEffect::Identity: return TEXT("Identity"); - case EVoxelOpEffect::CarveOnly: return TEXT("CarveOnly"); - case EVoxelOpEffect::FillOnly: return TEXT("FillOnly"); - case EVoxelOpEffect::Both: return TEXT("Both"); - default: return TEXT("Unknown"); - } -} - -const TCHAR* ExploreTileClassName(EVoxelTileClass Class) -{ - switch (Class) - { - case EVoxelTileClass::AllSolid: return TEXT("AllSolid"); - case EVoxelTileClass::AllAir: return TEXT("AllAir"); - case EVoxelTileClass::Mixed: return TEXT("Mixed"); - default: return TEXT("Unknown"); - } -} - // Density-bound APIs use FLT_MAX as the documented "unknown/unbounded" sentinel. It is finite // according to IEEE-754, so FMath::IsFinite alone would misreport the very failure this audit is // intended to expose as a real numeric envelope. @@ -7347,7 +7193,6 @@ bool WriteExploreOpBoundsReport( Writer->WriteValue(TEXT("seed"), Arguments.Seed); Writer->WriteValue(TEXT("archetype"), ArchetypeName(Arguments.Archetype)); Writer->WriteValue(TEXT("slot"), Arguments.Slot); - Writer->WriteValue(TEXT("operator_stack"), Arguments.bUseOperatorStack); Writer->WriteValue(TEXT("sample_step"), 1); Writer->WriteValue(TEXT("block_cells"), 8); Writer->WriteValue(TEXT("classifier_blocks_tested"), BlocksTested); @@ -7455,10 +7300,9 @@ bool RunOpBounds( FExploreBudget& Budget, FString& OutError) { - if (Arguments.Archetype != ECaveGeneratorType::TunnelNetwork - || !Arguments.bUseOperatorStack) + if (Arguments.Archetype != ECaveGeneratorType::TunnelNetwork) { - OutError = TEXT("opbounds requires archetype=TunnelNetwork and opstack=1."); + OutError = TEXT("opbounds requires archetype=TunnelNetwork."); return false; } @@ -8406,8 +8250,6 @@ bool BuildBatchCaseParams( FString Modes = TEXT("export"); JsonString(Case, TEXT("modes"), Modes); - bool bOperatorStack = true; - JsonBool(Case, TEXT("operator_stack"), bOperatorStack); bool bProfileDensity = false; JsonBool(Case, TEXT("profile_density"), bProfileDensity); bool bProfileDensityFull = false; @@ -8437,11 +8279,11 @@ bool BuildBatchCaseParams( Case, TEXT("tunnel_floor_terracing"), bTunnelFloorTerracing); OutParams = FString::Printf( - TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s -opstack=%d " + TEXT("-seed=%d -archetype=%s -slot=%d -modes=%s " "-exportsize=%d -exportstep=%d -densitygridreuse=%d " "-blockearlyout=%d -meshminbatch=%d " "-failurefocus=%d -out=\"%s\""), - Seed, *Archetype, Slot, *Modes, bOperatorStack ? 1 : 0, + Seed, *Archetype, Slot, *Modes, ExportSize, ExportStep, bReuseDensityGrid ? 1 : 0, bBlockEarlyOut ? 1 : 0, MeshMinBatchSize, bFailureFocus ? 1 : 0, *CaseOutDirectory); diff --git a/Tools/VoxelForgeTest.ps1 b/Tools/VoxelForgeTest.ps1 index 1334cab..e53d2d6 100644 --- a/Tools/VoxelForgeTest.ps1 +++ b/Tools/VoxelForgeTest.ps1 @@ -54,7 +54,6 @@ $HorizonMetrics = [ordered]@{ status = 'not_run' level = $HorizonLevel render_distance_chunks = $HorizonRenderDistanceChunks - far_sheet_ring = $false } $HorizonRenderMetrics = [ordered]@{ requested = $Scenario -eq 'horizon-render'; status = 'not_run' } $DeterminismEvidence = [ordered]@{ requested = $Scenario -eq 'determinism'; status = 'not_run' } @@ -634,7 +633,6 @@ function New-CommandletArguments([string]$CommandletOut, [string]$Modes, [bool]$ [void]$args.Add('-seed=0') [void]$args.Add('-archetype=TunnelNetwork') [void]$args.Add("-slot=$Slot") - [void]$args.Add('-opstack=1') [void]$args.Add(('-modes="{0}"' -f $Modes)) [void]$args.Add('-blockearlyout=0') [void]$args.Add('-densitygridreuse=1') @@ -1293,7 +1291,6 @@ try { $tracePath = Join-Path $RunRoot 'horizon_trace.jsonl' $baseCvars['voxel.TestRenderDistanceChunks'] = $HorizonRenderDistanceChunks $baseCvars['voxel.TestMaxClipLevel'] = $HorizonLevel - $baseCvars['voxel.TestFarSheetRing'] = 0 $baseCvars['voxel.StartupTraceFile'] = $tracePath $baseCvars['voxel.StartupTraceMaxTiles'] = 16384 $baseCvars['voxel.TestExitOnSteady'] = 1 @@ -1311,7 +1308,6 @@ try { $tracePath = Join-Path $RunRoot 'horizon_render_trace.jsonl' $baseCvars['voxel.TestRenderDistanceChunks'] = $HorizonRenderDistanceChunks $baseCvars['voxel.TestMaxClipLevel'] = $HorizonLevel - $baseCvars['voxel.TestFarSheetRing'] = 0 $baseCvars['voxel.StartupTraceFile'] = $tracePath $baseCvars['voxel.StartupTraceMaxTiles'] = 16384 $baseCvars['voxel.TestCeilingView'] = 1 @@ -1340,7 +1336,6 @@ try { $determinismBase = [ordered]@{ 'voxel.TestRenderDistanceChunks' = $HorizonRenderDistanceChunks 'voxel.TestMaxClipLevel' = $HorizonLevel - 'voxel.TestFarSheetRing' = 0 'voxel.TestExitOnSteady' = 1 'voxel.StartupTraceMaxTiles' = 16384 'voxel.OuterClassifierMode' = 0 @@ -1369,7 +1364,6 @@ try { $DeterminismEvidence = Compare-DeterminismDumps $defaultDump $crtFma3OffDump $DeterminismEvidence.level = $HorizonLevel $DeterminismEvidence.render_distance_chunks = $HorizonRenderDistanceChunks - $DeterminismEvidence.far_sheet_ring = $false $DeterminismEvidence.default_measurement = $defaultMetrics $DeterminismEvidence.crt_fma3_off_measurement = $crtFma3OffMetrics $GameMetrics = $crtFma3OffMetrics diff --git a/Tools/__pycache__/AnalyzeVoxelForgePerfProbes.cpython-312.pyc b/Tools/__pycache__/AnalyzeVoxelForgePerfProbes.cpython-312.pyc deleted file mode 100644 index b967c3c..0000000 Binary files a/Tools/__pycache__/AnalyzeVoxelForgePerfProbes.cpython-312.pyc and /dev/null differ diff --git a/fable-idea.md b/fable-idea.md index fa15f6f..2b64ef6 100644 --- a/fable-idea.md +++ b/fable-idea.md @@ -1,14 +1,16 @@ # fable-idea.md — performance & feature ideas -*Fable 5, max-effort pass — 2026-06-09. Grounded in a read of the actual code (mesher, apply path, task launch, content manager), not generic advice. Companion to CODEMAP.md §8 — nothing here is implemented; it's a menu.* - -*Note: the strict-whitelist `.gitignore` will ignore this file. Add `!fable-idea.md` if you want it tracked.* +*Fable 5, max-effort pass — 2026-06-09. Grounded in a read of the actual code (mesher, apply path, task launch, content manager), not generic advice. Companion to CODEMAP.md §8. Items marked ✅ are implemented (evidence inline); everything else is still a menu.* --- ## Part I — Performance -### 0. Measure before anything (half a session, directs everything else) +### 0. Measure before anything (half a session, directs everything else) — ✅ DONE + +*Evidence:* `stat VoxelForge` group (`Public/VoxelStats.h`, `STATGROUP_VoxelForge` tile-classification/meshing +counters) and Insights scopes around the stages (`VoxelForge_GenerateMesh`, `VoxelForge_BuildStreams`, +`VoxelForge_ApplyMeshToChunk`, `VoxelForge_Tick` and its sub-steps). Add `TRACE_CPUPROFILER_EVENT_SCOPE` around the four stages of a chunk (density grid sample / MC loop / normals / RMC stream build+upload) plus a `stat VoxelForge` group: chunks pending, applies this frame, avg gen ms, verts/chunk. One Unreal Insights capture then tells us if we're density-bound (my bet) or upload-bound, and every item below gets a before/after number. Cheap insurance against optimizing the wrong thing. @@ -30,8 +32,9 @@ So: normals can cost as much as the entire density grid; a SurfaceWorld chunk do ### Tier 1 — high win / low risk / small-medium effort (do these first) -> **STATUS 2026-07-05: Tier 1 COMPLETE.** T1.a ✅ (surface-column cache, now file-scope `GSurfColCache`); -> T1.b ✅ (grid-based normals — the old per-vertex `ComputeGradientNormal` trio is gone from the mesher); +> **STATUS 2026-07-05: Tier 1 COMPLETE.** T1.a ✅ (surface-column cache; since cleanup/2026-09 the density +> path's copy is the surface stack's `FSurfaceColumnSource` memo and `GSurfColCache` serves `ClassifyTile` only); +> T1.b ✅ (grid-based normals inline in `GenerateMesh`); > T1.c ✅ (`bShouldCreateCollision` = level 0 only, VoxelWorld.cpp ApplyMeshToTile); T1.d ✅ (v2 > `ClassifyTile`, trace-verified −44 % worker CPU); T1.e ✅ (`MaxMeshAppliesPerFrame`, default 4); > T1.f ✅ (`BuildTileStreamSet` runs in the gen task; apply only uploads). @@ -46,7 +49,7 @@ So: normals can cost as much as the entire density grid; a SurfaceWorld chunk do **T1.d — Chunk classification: skip trivially solid/air chunks before sampling.** ✅ DONE 2026-07-05 ⛔ **Turned OFF in streaming 2026-09-13.** After the operator stack, it cost more than it saved on the -game path (WORK-NEXT.md A/B). Do not re-propose tile classification without a design that is +game path (Docs/archive/WORK-NEXT.md A/B). Do not re-propose tile classification without a design that is cheaper than meshing the tile. (v2 — `UVoxelGenerator::ClassifyTile`, see ARCHITECTURE §8.10; a 2026-06-26 v1 with a global ceiling bound was reverted for roof holes. Trigger: trace showed 84 % of GenerateMesh calls produced empty @@ -106,7 +109,7 @@ the Surface|Macro knobs live, then extend with a top-view passage-path overlay ( **F2 — Determinism validator button.** ✅ DONE 2026-07-04 (`AVoxelWorld::ValidateDeterminism`, Live Edit category). CallInEditor: sample a band of densities from two different chunk-window alignments, diff, report max delta. Turns the scariest invariant (§8.4 — window invariance) into a one-click regression test *before* biome code starts landing. -**F3 — `stat VoxelForge` + Insights scopes.** Same as Perf §0 — listed here because it's also the tool that tells us when a feature regressed something. +**F3 — `stat VoxelForge` + Insights scopes. ✅ DONE** (see Perf §0 for the evidence). Same as Perf §0 — listed here because it's also the tool that tells us when a feature regressed something. ### B. The "this becomes a game" features @@ -116,9 +119,15 @@ Live Edit category). CallInEditor: sample a band of densities from two different `ResolveBiomeSampleAt` / `FBiomeContext`, VoxelGenerator.cpp §biomes; deco borders follow the Voronoi field).** Original sketch (kept for the cave-biome extension): Deterministic XY biome map = warped Voronoi/cellular cells (seeded, window-invariant by construction, same family as the relief map — and relief M should be an *input*: mountain biomes live where M is high). Resolution rules to protect §8.10: resolve **per chunk** (dominant biome + ≤2 neighbors + blend weights, stored in the thread-local chunk cache); per **voxel** blend only a handful of scalars (height offset, roughness, terrace, water tint index). Per biome: a *content profile* — decoration set, atmosphere/audio override, material palette index, water level offset. Archetype transitions stay Hard; biomes vary *within* SurfaceWorld first, cave-biomes (crystal/fungal/ice) reuse the identical pattern later. -**F6 — Material identity: vertex-data masks + triplanar palette material.** Geometry variety without *surface* variety still reads samey. At mesh time, pack per-vertex: slope (from the T1.b normal), relative height, biome/material index (from F5) into vertex color channels. One master material: triplanar rock/grass/sand/snow layers selected & blended by those masks + a macro-variation texture. This is the single biggest *visual* multiplier available and it's mostly material-graph work. +**F6 — Material identity: vertex-data masks + triplanar palette material. ✅ DONE (Stage 1)** — the mesher +writes per-vertex masks into `FVoxelMeshData::Colors` (R = dominant biome palette, G = slope, B = border blend, +A = neighbour palette; `VoxelMarchingCubesMesher.cpp`), fed by `UVoxelBiomeDefinition::MaterialPaletteIndex` +(ARCHITECTURE §8.15). Original note: Geometry variety without *surface* variety still reads samey. At mesh time, pack per-vertex: slope (from the T1.b normal), relative height, biome/material index (from F5) into vertex color channels. One master material: triplanar rock/grass/sand/snow layers selected & blended by those masks + a macro-variation texture. This is the single biggest *visual* multiplier available and it's mostly material-graph work. -**F7 — POI / set-piece system.** Noise terrain everywhere = beautiful nowhere. Deterministic destinations: chunk-hash-placed stamps (composed SDF carves/fills + a decoration prefab + optional ambient actor), e.g. buried shrines, crystal gardens at passage mouths, ruins on mesas. Placement uses the same two-region COLLECT discipline as rooms (§8.4). Destinations are what turn wandering into stories ("found a shrine at −400 m"). +**F7 — POI / set-piece system. ◐ PARTLY DONE** — the placement side is built: `FStrateLandmark` with +`AnchorMode` (HashLattice / PassageMouth), exclusion and set-pieces folded in, `FTerrainCondition` aware +placement, and `FDecoCompanion` satellites (ARCHITECTURE §8.5). Not built: composed SDF carve/fill stamps. +Original note: Noise terrain everywhere = beautiful nowhere. Deterministic destinations: chunk-hash-placed stamps (composed SDF carves/fills + a decoration prefab + optional ambient actor), e.g. buried shrines, crystal gardens at passage mouths, ruins on mesas. Placement uses the same two-region COLLECT discipline as rooms (§8.4). Destinations are what turn wandering into stories ("found a shrine at −400 m"). **F8 — Ore veins / diggable resources.** The game's verb is digging; give digging a reward loop. A secondary material-id field (cheap 3D noise threshold, per-strate/biome tables with depth curves) evaluated **only at mesh vertices** (≈ free) → vertex color → material shows veins; on carve, query the field at the brush center → grant resource. No per-voxel density cost, fully deterministic. @@ -126,27 +135,10 @@ Voronoi field).** Original sketch (kept for the cave-biome extension): Determini **F17 — Generator surface-class tag (ceiling/ground/cave material the *right* way). ✅ DONE 2026-07-05** (per-vertex semantic class in the mesher — down-facing verts query a memoized `GetSurfaceHeightAt`, nearer CeilSurf ⇒ sky-cap — per-tri majority → two contiguous polygroup runs → RMC section per group, slot 1 = `CeilingMaterial`, per-section shadow. Trigger: the whole-tile normal vote painted mixed coarse tiles with one material. Cave-roof discrimination hook is in place: down-facing below TerrainZ ⇒ ground/rock. Remaining polish idea: fully sideways cap-fold tris (all 3 verts |N.Z|≤0.1) default to ground.) Original design note: ★ do this when caves land. Today `ApplyMeshToTile` picks one material per tile from a ceiling test — first a height-oracle sample (midpoint), now a worker-side **normal vote** over the tile's mesh normals (down-facing ⇒ `bIsCeiling` ⇒ `CeilingMaterial` + no shadow; gated to SurfaceWorld by one `GetSurfaceHeightAt` probe). That's a **stopgap that only works because down-facing == sky-cap *while no caves exist*.** The moment a mountain-biome cave uses the same density/mesh system, its roof is also down-facing and would wrongly get the sky-cap material — orientation can't tell a cave ceiling from a surface ceiling. **The discriminator is semantic, not geometric, and only the generator knows it:** a sky-cap surface is the `ComputeSurfaceCeiling` (`CeilSurf`) boundary; a cave ceiling is a 3D-noise **carve** below `TerrainZ`. Cheap test the generator already has the inputs for — at a down-facing surface vertex, compare world Z to the column's `TerrainZ`/`CeilSurf` (both from the surface-column cache the mesher already holds): near `CeilSurf` ⇒ sky-cap, below `TerrainZ` ⇒ cave. **Plan:** stamp a discrete *surface class* (ground / sky-cap / cave-ceiling / cave-wall…) per vertex/triangle **at mesh time** in the mesher → carry it as the **polygroup** (already enabled, `Builder.EnablePolyGroups()`, every tri currently group 0) → `ApplyMeshToTile` maps polygroup → material slot (slot 0 terrain, slot 1 sky-cap, slot 2 cave-rock, biome-specific via the F5 palette mask) and sets per-section shadow. Discrete "which material" → polygroup/slot; continuous masks (biome blend, slope — F6) stay in `Colors`. This **subsumes** the current ground/ceiling split (it falls out as a special case), fixes the coarse mixed-tile horizon artifact exactly (per-triangle, not per-tile dominant-wins), and is the only version that survives caves. Pairs naturally with F5/F6/F8 (all want generator-stamped per-vertex material identity). Cost: a per-tri classify in the mesher (cheap, has the cache) + multi-slot setup in the apply path (RMC supports it; confirm the v5 per-section `UpdateSectionConfig` / slot-per-polygroup calls + empty-polygroup = no draw). Until then: the normal vote is fine — it's commented as "no caves yet → down == cap." -**F18 — Far-field per-surface SHEETS (the render-distance ring, cheap). ✅ BUILT & WORKING 2026-07-06** -(marker ticked 2026-07-27). With `RenderDistanceChunks` the -outermost ring can reach many km — as MC tiles that's 600-1000 primitives paying per-frame visibility/VSM -forever, and each far tile runs full 3D marching cubes just to rediscover two heightfields. In an open -strate the far field IS two heightfields the generator already computes per column (`GetSurfaceHeightAt`: -TerrainZ + CeilSurf). So: the extended ring streams SHEET tiles instead (level `MaxClipLevel + -FarSheetSpanLevels`, so one sheet covers 4-16 MC-tile footprints) and the mesher builds each as two -regular displaced grids — ground sheet (polygroup 0) + sky-cap sheet (polygroup 1), tags true **by -construction** (no vote, no classify probes), same materials/UVs/biome color masks as MC, normals from -the height gradient, perimeter skirts per bucket. Gen ~3-6× cheaper per area than band-cut MC; primitives -~10-16× fewer. Caveats accepted: SurfaceWorld only (non-open strates produce empty sheets — their far -ring was invisible rock anyway; FloatingIslands has no far ring beyond MaxClipLevel), carved features -(passages/spine/chasms) don't show at sheet distance, sheets need the strate band armed (in the -inter-strate gap the far ring blanks until you land). Streaming: sheet ring in `BuildDesiredTiles` -(covered-check vs the MaxClipLevel box), `IsTileInClipRange` shares the same outer shell, -`LoadTile` routes `Level > MaxClipLevel` to `GenerateSheetMesh`. **Build-1 fix (XY hole):** a -partially-covered sheet rendered its *whole* footprint, overlaying the near LOD0-1 terrain with its -coarse sampling. So the MC-covered box around the player (level-MaxClipLevel box, shrunk 1 tile for a -seam-overlap ring) is cut from sheets at cell granularity (`AVoxelWorld::SheetHole*Vox` → `GenerateSheetMesh` -hole args; hole moves on a MaxClipLevel-tile crossing → overlapping sheets re-queue via `BandRemeshQueue`; -hole-edge cells emit no skirt). +**F18 — Far-field per-surface SHEETS. ⛔ REMOVED (owner decision, 2026-09 cleanup).** Built 2026-07-06 as a +render-distance ring of heightfield "sheet" tiles past `MaxClipLevel` (meshed by `GenerateSheetMesh`); the +runtime path, its settings and the selector's sheet branch are deleted; the render-distance ring is plain +level-`MaxClipLevel` MC tiles (ARCHITECTURE §8.10). **F19 — AI navigation & agents (function-based). PARKED (no NPCs yet); FOUNDATION BUILT 2026-07-07.** Mobs need two things the world didn't give them: (1) to *exist* away from the local player, (2) to *route*. @@ -169,7 +161,7 @@ Mobs need two things the world didn't give them: (1) to *exist* away from the lo mandatory. AI is server-authoritative (§9.6). ★ Build when NPCs actually land. **F20 — Biome-selected surface terrain ops (terrace / cliff / layer-lines / overhang / spike / hole). SPEC 2026-07-07.** -Terrain ops are cave-only today (per-room in `GetDensityWithParams`; `GetSurfaceDensity` applies NONE — that's +Terrain ops were cave-only at the time (per-room in `GetDensityWithParams`; the SurfaceWorld density applied NONE — that's the "ops don't work on the surface" report). This brings them to the SURFACE, as a **biome** property, **conditioned on local terrain** so they read geological instead of random. (Slots in ahead of the later cave-system redo, which will add biome support cave-side reusing this same op→biome model.) @@ -214,7 +206,7 @@ fade with slope ⇒ seamless at biome borders; placement hashes are pure `(seed, "ops finally work on the surface" win; **(2)** overhang (3D band, slope-conditioned); **(3)** spike/hole (placed + shortlist + ClassifyTile guard) — most cost, do last. -**PHASE 1 — ✅ BUILT & WORKING** (built 2026-07-08; marker ticked 2026-07-27 — confirmed working by Jahni 2026-07-26, see `AUDIT-2026-07.md §0`). Heightfield ops shipped: **Cliff** (slope-gated STEEPENING — +**PHASE 1 — ✅ BUILT & WORKING** (built 2026-07-08; marker ticked 2026-07-27 — confirmed working by Jahni 2026-07-26, see `Docs/archive/AUDIT-2026-07.md §0`). Heightfield ops shipped: **Cliff** (slope-gated STEEPENING — push height from the local mean where steep ⇒ sheer walls; the slope-conditioned one, hugs steep terrain; v1 band-snap was too subtle, reformulated to steepening after Jahni's "doesn't change much"), **Terrace** (relief-gated plateau quantize, now with `TerraceHardness` soft-round↔crisp-mesa), **LayerLines** (sedimentary sine shelves, slope-expressed). *Design @@ -228,10 +220,10 @@ selection AND seamless border blending come for FREE with zero new resolution pa ops (fine — biome-differentiated terrain already implies that), and a strate can also carry ops with no biomes at all (more flexible than biome-only). All fields default OFF ⇒ current world byte-identical. Applied in the single height oracle `ComputeSurfaceTerrainZ` (new `SampleSurfaceStructuralZ` helper = pre-op raw height, re-sampled at -an XY offset for Cliff's slope) so MC/sheets/ClassifyTile/deco/BP-bridge all agree, no T1.d interference. Revisit +an XY offset for Cliff's slope) so MC/ClassifyTile/deco/BP-bridge all agree, no T1.d interference. Revisit the array+condition model for **phase 2 (overhangs)** where per-entry slope-gating earns its keep. -**PHASE 2 — ✅ BUILT & WORKING** (built 2026-07-08; marker ticked 2026-07-27 — confirmed working by Jahni 2026-07-26, see `AUDIT-2026-07.md §0`). Overhang (first VOLUMETRIC op) as +**PHASE 2 — ✅ BUILT & WORKING** (built 2026-07-08; marker ticked 2026-07-27 — confirmed working by Jahni 2026-07-26, see `Docs/archive/AUDIT-2026-07.md §0`). Overhang (first VOLUMETRIC op) as `FSurfaceGenerationParams` fields (`OverhangStrength/Reach/Height/Frequency/ZScale/SlopeThreshold`, default off). **Design NOTE — v1 additive-noise-band was WRONG (Jahni: "does nothing" + sketch of a real cliff lip): band-additive noise can only bump the surface where it already is, never make rock jut OUT over a void.** @@ -267,11 +259,11 @@ Multiplayer — **NO LONGER just "deferred": it's the confirmed direction (liste ## Suggested order (if it were mine to pick) -1. **Perf 0 + T1.c + T1.a + T1.b** — one focused session: measurement, the one-line collision win, the two big density cuts. -2. **T1.e + T1.f + F16** — smooth the game thread (apply budget, worker-side streams, HISM props). -3. **F1 preview tool + F2 validator** — before biome work starts, build the instruments. -4. **F5 biomes + F6 materials** — the look of the game. (F9 audio rides along cheaply.) -5. **F4 save/load** — the moment it feels like a game, players will want to keep one. -6. **F7 POIs + F8 ores** — destinations and rewards. -7. **T2.a SIMD noise** — after content direction settles (it changes seeds), before world-size ambitions grow. -8. **Transvoxel** (already chosen) whenever LOD cracks become the loudest remaining flaw. +1. ✅ **Perf 0 + T1.c + T1.a + T1.b** — one focused session: measurement, the one-line collision win, the two big density cuts. +2. ✅ **T1.e + T1.f + F16** — smooth the game thread (apply budget, worker-side streams, HISM props). +3. ✅ **F1 preview tool + F2 validator** — before biome work starts, build the instruments. +4. ✅ **F5 biomes + F6 materials** — the look of the game. (F9 audio: not built.) +5. **F4 save/load** — the moment it feels like a game, players will want to keep one. *(open)* +6. **F7 POIs + F8 ores** — destinations and rewards. *(F7 placement built, stamps open; F8 open)* +7. ✅ **T2.a SIMD noise** — after content direction settles (it changes seeds), before world-size ambitions grow. +8. **Transvoxel** (already chosen) whenever LOD cracks become the loudest remaining flaw. *(open — skirts fill LOD cracks today)*