From 3128852d4e079591d9119e4bbcb3ff909288b975 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 27 Jul 2026 01:39:21 +0200 Subject: [PATCH] chore: track the design docs in git (.gitignore !*.md) AUDIT P1: every markdown design doc except CODEMAP.md was untracked, so ARCHITECTURE / AUDIT / OPSTACK-PLAN / fable-idea / REVIEW_FINDINGS lived only on disk. Replaces the single !CODEMAP.md exception with !*.md. Also makes OPSTACK-PROGRESS.md commits actually record something, which the unattended crash-safety discipline depends on. Co-Authored-By: Claude Opus 5 --- .gitignore | 4 +- ARCHITECTURE.md | 754 ++++++++++++++++++++++++++++++++++++++++++++ AUDIT-2026-07.md | 677 +++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 43 +++ OPSTACK-PLAN.md | 434 +++++++++++++++++++++++++ OPSTACK-PROGRESS.md | 36 +++ OPSTACK-PROMPT.md | 258 +++++++++++++++ REVIEW_FINDINGS.md | 117 +++++++ fable-idea.md | 267 ++++++++++++++++ 9 files changed, 2589 insertions(+), 1 deletion(-) create mode 100644 ARCHITECTURE.md create mode 100644 AUDIT-2026-07.md create mode 100644 CLAUDE.md create mode 100644 OPSTACK-PLAN.md create mode 100644 OPSTACK-PROGRESS.md create mode 100644 OPSTACK-PROMPT.md create mode 100644 REVIEW_FINDINGS.md create mode 100644 fable-idea.md diff --git a/.gitignore b/.gitignore index ad44886..b86a9aa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ !/Source/** !VoxelForge.uplugin -!CODEMAP.md \ No newline at end of file + +# Keep every design / doc markdown at any depth (AUDIT P1 — these were untracked) +!*.md \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..cf265c8 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,754 @@ +# VoxelForge — Architecture & Design Deep-Dive + +> The 2026 redesign in detail: archetypes, the (0,0) spine, disturbances, content/atmosphere, +> biomes, and the **performance invariants** (the `§8.10` "don't regress" list). Read this +> before touching generation / strates / passages. +> Navigation, file index & conventions live in [CODEMAP.md](CODEMAP.md). Section numbers (`§8.x`) +> are preserved so existing cross-references keep resolving. + +## 8. Archetypes, spine, disturbances, content & carving (2026 redesign) + +A large A-to-Z expansion. The world is a stack of strates the player descends through; +each strate can be a fundamentally different *archetype*, connected at (0,0). + +### 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`. + +| Archetype | Params struct | Density fn | Idea | +|-----------|---------------|------------|------| +| TunnelNetwork | `FStrateGenerationParams` | `GetDensityWithParams` | rooms+tunnels (original) | +| FlatPlain / CrystalChamber | `FSlabGenerationParams` | `GetSlabDensity` | floor/ceiling void (original) | +| Maze | `FMazeGenerationParams` | `GetMazeDensity` | tight corridors on a 3D lattice (per-voxel, no cache; edge = lower node + axis hash) | +| 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 | + +All density fns share the convention: internal **positive=solid**, apply origin spine → +boundary seal → inter-strate passages, then `return -Density` (MC: negative=solid). +StrateManager provides params per chunk via `GetMaze/Surface/VerticalShaft/FloatingIslandParamsForChunk` +(macro `VF_ARCHETYPE_PARAMS_GETTER`) — no cross-boundary blend (Hard transitions between archetypes). +On top of the archetype, an optional **biome** layer (§8.14) modulates terrain & content WITHIN a +strate via a window-invariant XY field — currently wired into SurfaceWorld. + +### 8.2 (0,0) spine & hybrid connections +- `ApplyOriginSpine` (VoxelGenerator.cpp, static helper) carves a guaranteed open vertical + column at XY (0,0) in every strate's **interior** (seals untouched). Radius = + `UVoxelGenerator::OriginSpineRadius` ← `VoxelSettings::OriginSpineRadius`. Called before + every `ApplyBoundarySeal`. +- Descent is **player-dug** through the thin seals at (0,0). The single auto-opened + connection is the **surface entry shaft** at (0,0) through the top of strate 0 + (`GeneratePassages`, `bOpenSurfaceEntry`). +- **Hybrid extras:** auto-carved *shortcut* passages per boundary, placed away from (0,0). + Now fully **per-strate** — see §8.8 (the upper strate's `PassageConfig` drives count/style/shape). + +### 8.3 Disturbance layer (the "wow" post-process) +`FStrateDisturbanceParams` (on the definition, all archetypes). `ApplyDisturbances` +(VoxelGenerator.cpp static, **MC convention**) runs in `GetDensityAt` after dispatch: +chasms (carve air), bridges (solid spans), ridges (solid blades). Stays inside seal bands. +Provided per chunk by `StrateManager::GetDisturbanceParamsForChunk`. + +### 8.4 Cross-chunk determinism (the seam-prevention invariant) +`BuildChunkCache` (VoxelCaveMorphology.cpp) uses **two regions**: a wide *COLLECT* region +(`2*MaxTunnelLength + MaxInfluence`) over which connectivity is decided (NN filtered to +`<= MaxTunnelLength`, origin cap = deterministic top-N by hash), and a tight *STORE* region +(`+MaxInfluence`) kept for per-voxel eval. This makes the room/tunnel graph window-invariant. +**If you add a connectivity rule with longer edges, the COLLECT region must still cover the +max edge reach, and decisions must not depend on the stored window.** + +### 8.5 Content scatter & water — `VoxelContentManager.h/.cpp` (NEW) +`UVoxelContentManager` (owned by `AVoxelWorld`, game-thread). TWO INDEPENDENT subsystems: + +**(A) DECORATIONS — distance-based WORLD GRID (the no-pop system, 2026-06-17).** Decorations are placed +on a fixed world XY cell grid (**1 cell = 1 chunk footprint**, `DECO_CELL_VOXELS = CHUNK_SIZE`) and streamed +by DISTANCE from the player, **fully decoupled from clipmap tiles / LOD**. **THE MARCH RUNS ASYNC ON WORKER +THREADS** (mirrors mesh gen — `GetDensityAt` is thread-safe; the synchronous-on-game-thread first cut was a +perf disaster + starved streaming → seams, so it was moved off-thread). Driven by `AVoxelWorld::Tick → +UpdateDecorations(playerWorldPos)`, three phases: **(1)** recompute the desired cell set (`RebuildDesiredCells`) +only when the player crosses a cell boundary OR changes strate — clears out-of-range loaded cells, queues +cells that are NOT loaded and NOT in flight (`PendingLaunch`, nearest-first). A loaded region is NEVER +re-streamed in place while it stays in range (only cleared when it leaves), so decorations don't FLICKER as +the player moves. **TWO STREAMING GRIDS (`FStrateDecoration::StreamTier`, 2026-06-26):** the stream RADIUS is a +property of the GRID, never of an entry — mixing radii inside one grid would force an in-place re-stream when +the player crosses an entry's radius (the original tier system's flicker bug). So there are exactly two +self-contained grids (`FDecoGrid`, each with its own region map + builds + queues + HISMs): **NearGrid** +(`DecorationNearRadiusChunks`, FINE `DecorationSpacingVoxels` column grid — dense groundcover near the player) +and **FarGrid** (`DecorationRadiusChunks`, COARSE `DecorationFarSpacingVoxels` column grid — trees/landmarks/RARE +props visible everywhere; the coarse grid is what makes a rare prop cheap, since the worker march cost scales +with column count). An entry picks a grid via `StreamTier` (default Far); the palette is partitioned by tier so +each grid marches only its own subset. A given world XY is covered by a Far region always, plus a Near region +when close (separate HISMs → crossing the near boundary never touches the far region). **(2)** `LaunchDecoTasks` +(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). +`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 +heightfield+biome on every sample. **TWO column strategies by archetype:** +**(a) SurfaceWorld → HEIGHT ORACLE (`Ctx.bSurfaceWorld`), NO marching.** `Generator::GetSurfaceHeightAt(x,y, +chunkZ → TerrainZ, CeilSurf)` returns the heightfield surface + sky-cap ceiling in O(1) (it shares the density +path's `ResolveSurfaceChunkParams`/`ComputeSurfaceColumn` via its own thread_local per-chunk cache, so it's +bit-identical to the rendered ground). Per column: query centre + 4 neighbours (gradient → floor/ceiling +normals), place a Floor crossing at `TerrainZ` and a Ceiling crossing at `CeilSurf` (if open space below). A +single `GetDensityAt` at the surface verifies the column isn't CARVED (passage/spine/diff make it air → skip; +the oracle is the raw heightfield and doesn't know carving). ~5 height evals + 1-2 density samples/column vs +hundreds marched. **(b) other archetypes (caves/shafts/islands) → ray-march** the strate Z-band +(`GetStrateUnrealZRange`, voxel coords) via `GetDensityAt` at a COARSE step (`DecorationMarchStepVoxels`), each +air↔solid sign change **bisection-refined** (4 iters → accuracy independent of step). Either way **a prop sits at +the SAME world position at every LOD → no pop**. (march) The top cap/seal + the open air are always marched first; the scan only stops after +`DecorationColumnDepthVoxels` of CONTIGUOUS solid once it has ENTERED the open space (trims dead bedrock below +the ground without ever stopping short of it — a "below the first crossing" cap was wrong: on a surface world +the first crossing is the high CEILING, so it stopped mid-air before reaching the ground = no floor props). +Outward normal = normalized density gradient (solid→air, matches the mesher), classified +Floor/Wall/Ceiling by `normal.Z`. Each crossing rolls every `FStrateDecoration` independently: surface-type, +density gate (`DecoHash(cell,column,crossing,entry,seed)`), water-relative, align/yaw/scale, per-cell +`MaxPerChunk` + global actor cap → a `FDecoSpawn{EntryIdx, bInstanced, Xf}`. The game thread spawns from the +result's `Entries` snapshot. `DecorationMaxCrossingsPerColumn` caps cave columns (surface worlds have 1). +**Shutdown:** `NotifyShutdown()` (called from `AVoxelWorld::EndPlay`) flags + spin-waits on the in-flight +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). +**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 +grass affordable: placed thickly, drawn only near → GPU cost bounded by area-within-cull, NOT the stream radius), +`bCastShadow` (default true; turn OFF for grass — dense instanced shadows are the dominant foliage cost), +`MaxSlopeAngle` (deg from flat = acos(|N.Z|); 90 = no filter, ~35 keeps grass off cliffs — applied in the worker's +`PlaceAtCrossing`). **Placement-constraint gates (all in `PlaceAtCrossing`, deterministic, zero-cost at defaults):** +`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` +(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). +**F7 AWARE PLACEMENT (2026-07-06):** `FPlacementProfile::Conditions` = a list of `FTerrainCondition` +DERIVED PREDICATES (relief / moisture / biome-border weight, each an inclusive [Min,Max] band, optional +invert), AND-ed and evaluated by `Generator::EvaluateTerrainConditions` at the candidate XY — "conditions, +not annotations": nothing is stored, the phenomenon is queried from the analytic fields (SampleRelief / +SampleMoisture / SampleBiomeAt) on demand, so it's deterministic + worker-safe (the caller hands over the +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 +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` +(satellites: rocks/mushrooms around a tree). When `PlaceAtCrossing` emits a parent spawn, it immediately rolls +each companion (probability → count in `[CountMin,CountMax]` → disk offset `RadiusMin/MaxVox`), all hashed off +the PARENT's hash `H` → a pure function of the parent, so NO "did a tree spawn here?" search. Each satellite offsets from the parent in the XY plane (voxel space) and then, by default (`bSnapToSurface`), +**re-snaps to the real surface at its OWN XY** via `FindLandmarkColumn` (cheap `GetSurfaceHeightAt` oracle on +SurfaceWorld, a short ray-march in caves; worker-safe — reads only `Gen`) — this kills floaters on uneven +ground; a satellite that finds no surface at its spot is skipped. Turn `bSnapToSurface` off to inherit the +parent's exact height+normal (cheapest, flat ground only). Satellites can also be **gated by their own +`Profile.Conditions`** evaluated at the satellite XY (e.g. a unique mushroom only at a biome border). They carry +their own `Profile` transform/render. `FDecoSpawn` gained `CompanionIdx` (−1 = the entry; else index into +`Companions`) so `MergeCellResult`/`ApplyRegion` resolve the satellite's mesh/actor + render tuning from +`Companions[ci].Profile` (the region mesh bucket now stores an `FPlacementProfile`, not a whole entry). **Two levels:** a companion may carry `SubCompanions` (`FDecoSubCompanion` — a distinct type, since UHT can't +reflect a self-recursive `FDecoCompanion`) that spawn ON each level-1 satellite (moss on a rock); level-2 +**inherits the L1 satellite's snapped point** (no re-snap — the cost lever that keeps nesting cheap) and can +still gate on its own `Conditions`. `FDecoSpawn::SubIdx` routes L2 back to `Companions[ci].SubCompanions[sj]`. +A hard **per-parent budget** (`GMaxCompanionsPerParent`, 256) caps the total L1+L2 count so a misconfiguration +can't blow up regardless of authored counts. Bounded (count × parents, ≤ budget), deterministic, streams inside +the existing two-grid deco system untouched. Depth beyond 2 (and per-entry references for arbitrary nesting) is +the remaining flagged follow-up. + +**F7 SET-PIECES — FOLDED INTO LANDMARKS (2026-07-06).** Set-pieces (ruins/shrines/monuments) and landmarks +(mini-suns) had no real reason to be separate once both shared the spawn core and gained `Conditions`, so +`FStrateSetPiece`/`UpdateSetPieces` were MERGED into `FStrateLandmark`/`UpdateLandmarks` — one primitive, one +`UVoxelStrateDefinition::Landmarks` list. Each entry has an `AnchorMode`: **HashLattice** (scatter on a coarse +lattice, `SpacingChunks`) or **PassageMouth** (enumerate `StrateManager->GetPassages()`, keep endpoints whose +Upper/LowerStrateIndex == the current strate — `bAtDescentMouths` = the hole going down, `bAtArrivalMouths` = +where you land; endpoints are global VOXEL coords). Feature-conditioning is just `Profile.Conditions` on either +mode. **Exclusion (self-awareness, optional):** `ExclusionRadiusChunks` (0 = OFF, the default — mini-suns don't +exclude) + `Priority`; a candidate is suppressed if a HIGHER-RANKED one's disk covers it (rank = Priority, then +hash — deterministic). `UpdateLandmarks` now gathers all candidates → resolves exclusion (SKIPPED entirely when +no entry opts in, so pure scatter keeps its old O(n) cost; HashLattice keeps the original hash salt 0x1A2D5u so +mini-sun positions are unchanged) → spawns survivors via `SpawnFromProfile` (+ the orb wrapper). +**Exclusion is now POP-FREE:** each entry is gathered in `StreamRadius + MaxExcl` (MaxExcl = the strate's largest +`ExclusionRadiusChunks`); the extra "ring" candidates carry `bSpawnable = false` and only SUPPRESS (never spawn), +so every conflictor of an in-range candidate is always present regardless of player position → a candidate's fate +is a pure function of (seed, layout), no edge-of-radius flicker. +**Decoration footprint (`bSuppressDecorationsUnder` + `SuppressRadiusChunks`):** a landmark mesh doesn't change +density, so the deco placer can't see it — instead, on spawn it calls `RemoveDecorationsInSphere` to clear grass +in its footprint, stores the footprint on `FLandmarkInstance`, and `ApplyRegion` re-clears under any loaded +suppressing landmark when a deco region streams in fresh (so temple floors stay clear as you leave/return). +**PLAYER DIG → GRASS REMOVAL:** `AVoxelWorld::ApplyModification` (the single funnel for every carve/fill brush) +calls `RemoveDecorationsInSphere(Center·VOXEL_SIZE, Radius·VOXEL_SIZE)` after the diff+remesh — instant, +flicker-free (only the affected HISM instances go, via `GetInstancesOverlappingSphere`+`RemoveInstances`; nothing +is cleared+rebuilt), so grass never floats over a dug hole. This only patches the LIVE instances; the placer's +existing density check (`D(VX,VY,hC) <= 0.5f`) already keeps any future natural rebuild correct. (Editing a +Static-mobility HISM re-caches its proxy — fine for player-paced digging.) Not handled by immediate removal: new +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. +**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 +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 +(`Voxel|Content`):** `DecorationRadiusChunks` (6 — FAR reach in cells), `DecorationNearRadiusChunks` (3 — NEAR +reach), `DecorationSpacingVoxels` (4 → 8×8 cols/cell — NEAR/fine grid), `DecorationFarSpacingVoxels` (4 by default +→ raise to 8–16 for a cheap coarse FAR grid; the rare-prop lever), `DecorationRegionSizeCells` (4 — RxR cells per +region/HISM), `DecorationMarchStepVoxels` (2 — coarse, bisection-refined; cave march only), `DecorationMaxCrossingsPerColumn` +(4 — cave march only), `DecorationColumnDepthVoxels` (160 — bedrock march cap; cave march only), +`MaxDecorationCellsPerFrame` (2 — apply/spawn budget), `MaxConcurrentDecorationTasks` (4 — in-flight task cap; +0 disables decorations). **COST:** surface worlds now use the O(1) oracle (cheap); caves ray-march. The work is +OFF the frame (worker threads) — game thread only pays the budgeted spawn. If streaming slows, lower +`MaxConcurrentDecorationTasks` / raise `DecorationMarchStepVoxels` / shrink radii/spacing. Default +`DecorationRadiusChunks=6` ≈ props ~48 m out — raise for far flora (cost ~r²). + +**(A2) LANDMARKS — rare large objects on a COARSE HASH LATTICE (`UpdateLandmarks`, 2026-06-26).** The right +primitive for sparse, far-visible objects like the underground "mini-suns" — where the per-chunk decoration +grid fails: at a 2048-chunk radius that grid enumerates ~13M cells per cell-crossing on the game thread and +FREEZES. Landmarks instead live on a per-entry hash lattice (cell = `FStrateLandmark::SpacingChunks` chunks), +so a radius-R disk holds only ~(R/Spacing)² candidates (≈16 at R=2048, Spacing=512). Listed strate-wide on +`UVoxelStrateDefinition::Landmarks`. Each Tick, for each entry, walk the small lattice box around the player +within `StreamRadiusChunks`: `DecoHash(cell,entry,seed)` rolls existence (`SpawnProbability`), a jittered XY +(`JitterFraction` — effective min spacing ≈ Spacing·(1−Jitter)), then a SINGLE-column surface find +(`FindLandmarkColumn`: SurfaceWorld height oracle, else one density ray-march) snaps to the chosen +`SurfacePlacement` (default Ceiling = sky-cap). Gates mirror decorations (biome via `GetDominantBiomeAt`, +slope band, water-relative). Foliage-style transform tweaks: `LocationOffset` (world XYZ), `RotationOffset` + +per-axis `RandomRotation`, `Min/MaxScale`, `bAlignToSurface`. Spawned as a real actor (`ActorClass`, for a +sun's light) or one Static `UStaticMeshComponent` (`InstancedMesh`, `CullDistance`=0 → never cull). All +SYNCHRONOUS on the game thread (so few candidates it never hitches); a cell's surface-find runs only the +first frame it enters range, then is cached in `LandmarkInstances` (keyed `FIntVector(cellX,cellY,entry)`, +null entry = "evaluated, nothing placed" so it isn't retried). Deterministic (hash → no pop, infinite reach). +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). + +`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 +`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. + +### 8.6 Atmosphere — `VoxelAtmosphereManager.h/.cpp` (NEW) +`UVoxelAtmosphereManager` (owned by `AVoxelWorld`, gated by `bManageAtmosphere`). +`UpdateForPlayer(pos)` each Tick, reacts only on strate change. Drives a managed +`UExponentialHeightFogComponent` + movable `USkyLightComponent` from the player's strate +(`FogColor/FogDensity/bVolumetricFog/AmbientLightColor/AmbientLightIntensity`), and spawns +PERSISTENT ceiling/floor "layer" actors (`Def->CeilingLayerActor`/`FloorLayerActor` + ZOffsets ++ rotations) that follow the player in XY — the sky-island sea-of-clouds / two-sided fog. +`Def->AtmosphereActor` (a full BP with your own fog/sky/postprocess) OVERRIDES the managed +fog+sky for that strate. `Reset()` on ChangeSeed/EndPlay. (Skylight ambient underground is +weak — captures a dark scene; fog is the strong visual.) +**Per-biome atmosphere (§8.14):** `UpdateForPlayer` also resolves the player's dominant biome and, +when the biome has `bOverrideAtmosphere`, its fog/sky beats the strate's (reacts on biome change, not +just strate change). `ApplyFogSky(Def, Biome)` is the shared path; layer actors + the full `AtmosphereActor` +BP stay strate-level. Needs the generator injected (`Initialize(..., Generator)`). + +### 8.7 Inter-strate bedrock gap +`VoxelSettings::InterStrateGapChunks` (N) inserts N chunks of SOLID bedrock between consecutive +strates (`StrateManager::Initialize` leaves the gap in the layout). `IsGapChunk` detects it; +`GetDensityAt` renders gap chunks as solid + passages only (no caves/spine/seal) so the player +digs (0,0) through the gap to descend. `GetStrateUnrealZRange` gives a strate's cm Z range. + +### 8.8 Inter-strate passages — PER-STRATE (`FStratePassageConfig` on the definition) +Each strate's `PassageConfig` (VoxelStrateTypes.h) controls its descent tunnels to the layer +below: `Connections`, `Style` (`EVoxelPassageStyle`: Straight/Worm/Spiral/Cascading), +`MouthRadius`/`MidRadius` (tapered width → `FVoxelPassage::ControlRadii` + `VoxelSDF::TaperedCapsule`), +`ReachMin/Max` (depth into each strate), `DistanceMin/Max` (from the (0,0) spine), `Wander`, +`Segments`, `VerticalWobble`, Spiral/Cascade params. Built in `StrateManager::GeneratePassages` +as control-point chains. **Worm = independent fBM per horizontal axis** (`PassageFBM` static) +with a flat-top envelope → organic squirm (NOT a 1D zigzag, NOT a same-freq 2-channel spiral). +`EvaluateModifierSDF` (per voxel) first builds a `thread_local` **per-chunk shortlist** of passages +whose bounds reach this chunk (rebuilt on chunk change / `PassagesVersion` bump) — chunks with no +passage near return `FLT_MAX` immediately — then **bounding-sphere-culls** each shortlisted passage +(`FVoxelPassage::BoundCenter/BoundRadiusSq`). Both are perf-critical (§8.10). The (0,0) surface entry +is a simple straight tube. Global passage settings were removed from `VoxelSettings`. + +### 8.9 Carving — brush shapes + editor controls +`FVoxelModification` has `EVoxelBrushShape {Sphere,Box,Capsule}` + `BoxExtent`/`CapsuleEnd`/ +`Falloff` + `GetWorldBounds`. `UVoxelDiffLayer::GetDensityOffset` switches per shape; chunk +overlap uses the shape AABB. `AVoxelWorld`: `CarveBox/FillBox/CarveCapsule/FillCapsule/ +ApplyModification` (BlueprintCallable) + `EditorCarveSphere/EditorFillSphere` (CallInEditor) +driven by `EditorBrush*` props. + +### 8.10 Performance invariants (DON'T regress) +- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set ONLY when the + player crosses a chunk boundary (`LastUpdateCenter`); use the `DesiredSet` TSet for the cull; + idle via `bAllChunksLoaded`. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.) +- **LOD** changes HOT-SWAP (`LoadChunk` only, never unload-first) → no holes. LOD + reconciliation lives in the PERSISTENT per-frame submit loop (same loop as new-chunk loads), + NOT as a one-shot on the boundary-cross frame — a one-shot drops every chunk past the task + budget and strands it at a stale LOD. Idle (`bAllChunksLoaded`) only when a full scan finds + no loads AND no LOD mismatches outstanding. +- **SDF cache** (`GetDensityWithParams`): search-BOX validity, not chunk-key — gradient ±1 + sampling must not thrash the (expensive) rebuild. +- **Per-chunk param cache** in `GetDensityAt`: GenType + param struct + disturbance cached + thread-locally per chunk; don't move the fetch/blend back to per-voxel. +- **Biome cache** (`ResolveBiomeSampleAt`/`FChunkBiomeCache`, §8.14): validity is a world-XY BOX + + ChunkZ + Seed, NOT a chunk key — same reason as the SDF cache. The cell classification is + noise-heavy; a chunk-key would thrash it on gradient-normal / +X/+Y boundary samples. Keep + the box halo (≥ CHUNK_SIZE) + cell margin (warp + CellSize) so the 3x3 lookup never misses. +- **Passage cull** (§8.8) + **morphology two-region** (§8.4): both are per-voxel-cost critical. +- **Per-chunk passage shortlist** (`EvaluateModifierSDF`): runs per voxel and is called from every + archetype's `ApplyPassageCarving`. Keeps a `thread_local` shortlist (passage INDICES) of passages + whose bounds reach the current chunk, rebuilt only on chunk change or `PassagesVersion` bump + (incremented in `GeneratePassages`). Most chunks have NO passage near → instant `FLT_MAX` return + instead of walking the whole `Passages` array per voxel. Conservative superset (chunk bounding + sphere vs passage bound) ⇒ bit-identical carve. Store indices + version, never pointers (the array + is rebuilt on `RebuildStrates`). +- **Gen tasks run at `UE::Tasks::ETaskPriority::BackgroundNormal`** (`LoadTile`): worker gen yields + to foreground game/render tasks. Without it, raising `MaxConcurrentTasks` past the spare-core count + saturates the scheduler and starves the frame (the "concurrency > ~12 = stutter" symptom). Keep gen + at background priority so the frame keeps its cores. +- **Mesher density grid + margin ring** (`GenerateMesh`): sample each grid point ONCE into a flat + `(CHUNK_SIZE/Step + 1 + 2)³` array (the `+2` is a 1-point MARGIN ring, indices −1..GridDim, for + T1.b normals). The cell loop reads 8 corners from it; per-cell sampling would call `GetDensityAt` + ~8× too often. Geometry is bit-identical (edge positions unchanged). Don't refactor back to + per-corner `GetDensity` and don't drop the margin ring (normals + seamless borders need it). + The cell loop is **two-pass**: pass 1 reads the 8 corner densities + builds the MC case index and + `continue`s on no-surface cells (≈70% of cells); pass 2 computes the 8 positions + grid-gradients + ONLY for surface cells. Don't hoist position/gradient back above the case-index test. The + `DensityGrid` and vertex-dedup `TMap` are `thread_local` and reused per worker (Reset / keep + capacity) — don't make them per-call locals (re-allocates ~170 KB + a hash map every tile). + **CAPTURE-DURING-MESHING (sanctioned reuse, doesn't regress the above):** `GenerateMesh`'s optional + `OutCaptureGrid` copies the already-filled `DensityGrid` interior (`CHUNK_SIZE³`, quantized via + `VF_QuantizeDensity`) out for the density clipmap (mini-sun shadows) — a PURE READ added after the + grid loop. It does NOT touch the grid shape, the two-pass loop, the margin ring, or the thread_local + reuse. Only level-0 full-res tiles request it (`Step==1<