Files
VoxelForge/OPSTACK-DECOMPOSITION.md
T
Fr0zka 96e75abe57 feat: port FloatingIslands — the stack that runs backwards
6 of 8 archetypes ported. This one starts from VOID and FILLS where the
other four start from ROCK and CARVE, which is what it was worth doing:
neither end of the pile needed a new operator, only the opposite sign.

  FConstantRockSource -> FConstantFieldSource(+/-Base)   AllSolid <-> AllAir
  FSdfCarveOp         -> FSdfConvertOp(Sign = +/-1)      carve    <-> fill
  FSdfRoughnessMod                                       4th archetype, unchanged

Only the island blob source is new. Multiplying by +/-1 is exact in
IEEE-754, so the three already-green ports are bit-for-bit untouched.

ClassifyBox can return AllAir for the first time in the plugin, and an
island strate is by construction mostly empty — the test counts AllSolid
and AllAir separately so an aggregate cannot hide whether that fired.

Two bounds that would have been holes if assumed rather than derived:
the island bound is one-sided (a hairline thread of matter hangs below
each island down its axis, so only the TOP may reject), and the domain
warp displaces X and Y independently, so the pad needs WarpAmp*sqrt(2).

Also: AUDIT C1 was NOT closed. The 2026-07-27 sweep matched `SeedF * K`
and this archetype's warp spells it `(float)S * K`, so one site survived
— at seed 2e9 the warp flattens and every island snaps back to a perfect
circle. Fixed in both paths in one pass so the equivalence test stays a
valid oracle. Expect island silhouettes to change at large seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:50:36 +02:00

36 KiB
Raw Blame History

VoxelForge — the decomposition map

What this is: every one of the 8 archetypes, read line by line, broken into the four roles of OPSTACK-PLAN.md §2.5, with each existing param traced to the op that will own it. Written 2026-07-27 so no later port has to re-derive it.

Read OPSTACK-PLAN.md §2.5 first. The whole point is that an archetype becomes source + combiners + modifiers, not one opaque op. If a port here collapses into a single FMazeOp, the refactor has failed its own test.

Status: analysis only. No op exists. Public/VoxelDensityOp.h (the contract) is written; the switch in GetDensityAt is untouched.


0. What fell out — read this before the per-archetype sections

Three findings changed how I'd sequence the work. They are the reason this document is worth its length.

0.1 ⚠️ The contract needs an SDF channel, not just a density channel

IVoxelDensityOp::Eval returns density. But look at what TunnelNetwork actually does:

CaveSDF = EvaluateSDFCached(rooms + tunnels)          // SDF space
CaveSDF = SmoothMin(CaveSDF, PitSDF,     Pit.BlendK)  // SDF space
CaveSDF = SmoothMin(CaveSDF, ChimneySDF, Chim.BlendK) // SDF space
→ ONE carve at the end: Density -= CarveFactor · BaseDensity · 2

Maze, VerticalShafts and FloatingIslands do the same shape: build an SDF from several primitives, perturb the SDF with roughness noise, then convert once.

If each primitive becomes a density op with its own carve, the SmoothMin junctions are lost — a pit would meet its room at a hard seam instead of the organic blend the code deliberately builds (the comment at the pit block says exactly this: "SmoothMin at the pit-to-room junction creates the same organic transition as tunnel-to-room (no hard seam at PitTopZ)"). Roughness is worse: three of the four archetypes add noise to the SDF, which displaces the surface; adding the same noise to density scales with the local gradient and is a different effect.

So ops need two channels: float Density and float Sdf, with an explicit SdfToDensity op that converts. Sketch:

struct FVoxelOpSample { float Density; float Sdf; };   // Sdf = FLT_MAX ⇒ "no surface nearby"
virtual void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const = 0;

This is the one part of OPSTACK-PLAN §3 I think is wrong as written, and it matters far beyond fidelity: SDF-space SmoothMin between two different sources is precisely how "a room graph carved into a mountain" produces an organic junction rather than one field punching a hole in the other. The single-channel contract can only ever overwrite. Recommend adopting the two-channel Eval before porting Maze — it is cheaper now than after four ports.

(Left as a recommendation, not a change: the header as committed is single-channel, and this is Jahni's call.)

0.2 ⚠️ Worm tunnels are why TunnelNetwork can never skip a tile — and the fix is a number

AUDIT §6.2 frames "placed vs fielded" as a question about future 3D caves. It is already a live cost. Worms are a pure 3D-noise threshold carve with no bounds:

if (WormStrength > 0 && WormThreshold > 0) { ... Density -= t · WormStrength · NetworkMask; }

Unbounded in space ⇒ EffectOverBox = CarveOnly everywhereAllSolid is dead for every tile of every strate with worms enabled. Direction alone cannot recover it.

But the amplitude is bounded and trivially known: t ∈ [0,1], NetworkMask ∈ [0,1], so the worm op can move density toward air by at most WormStrength. If the stack so far is provably solid by more than the sum of every remaining op's max carve, the box is still AllSolid.

So the first numeric interval bound worth writing is not the SDF Lipschitz bound the plan reaches for — it is a scalar amplitude cap on the fielded-noise carves. Roughly ten lines, and it is what unlocks deep-rock skipping for the plugin's most-used archetype. OPSTACK-PLAN §4 defers all numeric bounds to Phase 3; on this evidence one of them belongs in Phase 2.

0.3 Disturbances already have bounds that ClassifyTile throws away

Today: if (D.ChasmDensity > 0) bCanSolid = false; — strate-wide, for the whole tile.

But chasms/bridges/ridges are hash-placed on an XY lattice of known spacing and radius, and the per-voxel code already builds the 3×3 candidate list. A box that no candidate reaches is Identity. This is a pure win available to SurfaceWorld today, independent of everything else in this plan: any surface strate that enables chasms currently loses AllSolid on every tile in it.


1. The shared primitive library

The 8 archetypes are ~6 functions in costumes (OPSTACK-PLAN §1b). Decomposed, here is what actually repeats. Reuse count is the payoff metric — anything used once is suspicious.

Role 1 — FIELD SOURCES

Op What it lays down Used by IsXYPure ClassifyBox / EffectOverBox
FConstantRockSource Density = BaseDensity (solid) TunnelNetwork, Maze, VerticalShafts, bedrock gaps trivially ClassifyBoxAllSolid, always. Free, exact.
FConstantVoidSource Density = BaseDensity (air) FloatingIslands ClassifyBoxAllAir, always.
FSlabVoidSource floor surface + ceiling surface, Density = min(zfloor, ceilz) FlatPlain, CrystalChamber see §3.1 ClassifyBox by sampling floor/ceiling over the box's XY corners — exact, cheap, and a new skip win
FHeightfieldSource max(TerrainZ z, z CeilSurf) SurfaceWorld (the column) ClassifyBox by sampling columns on the mesher's exact lattice — this is today's TestColumn, moved verbatim
FRoomGraphSource rooms + tunnels (+ pits + chimneys) SDF TunnelNetwork Identity when no room/tunnel bound reaches the box — FCachedRoom/FCachedTunnel already carry the bounds
FLatticeCorridorSource 3D lattice capsule corridors Maze Identity when no open edge's capsule bound reaches the box
FShaftFieldSource vertical cylinders + horizontal connectors VerticalShafts (cylinders are XY-pure; connectors are not) Identity when no shaft cell reaches the box in XY
FIslandBlobSource tapered flat-topped blobs FloatingIslands Identity when no island bound reaches the box
FWormFieldSource 3D-noise threshold carve TunnelNetwork CarveOnly always — see §0.2. The one unbounded source.

Note how much Identity is available and unused: six of nine sources can prove themselves absent from most of the volume, and today none of them do.

Role 2 — COMBINERS

Replace · Union(min) · Subtract(max) · SmoothUnion/SmoothSubtract (reuse VoxelSDF::SmoothMin/Max) · Add · Mask. Plus, from §0.1, the conversion op:

Op Meaning
FSdfCarve(blend) SDF < blendDensity -= smoothstep(...)·BaseDensity·2. The exact same six lines appear in TunnelNetwork, Maze and VerticalShafts.
FSdfFill(blend) the += mirror. FloatingIslands.

That one op deduplicates three copies of the carve formula and one of the fill.

Role 3 — DETAIL MODIFIERS

Everything here is gated on being near a surface, and everything here already exists.

Op Space Used by Notes
FSurfaceRoughnessMod SDF or density — two variants, see below all 4 SDF archetypes the single biggest reuse in the plugin
FTerraceMod (cave) density TunnelNetwork SDF-gradient orientation gate (2 extra SDF evals)
FLayerLineMod density TunnelNetwork sine along Z, cubed
FRibbingMod density TunnelNetwork sine along Z, squared, +=
FCaveOverhangMod density TunnelNetwork low-Z-frequency fBm, positive lobe only
FCaveCliffMod density TunnelNetwork noise-modulated vertical gradient
FScallopMod density TunnelNetwork cellular noise
FArchMod density TunnelNetwork room-relative
FRoomColumnMod density TunnelNetwork room-relative, pre-baked in BuildChunkCache
FDomeMod density TunnelNetwork room-relative
FPinchMod density TunnelNetwork room-relative
FFloorBiasMod density TunnelNetwork only inside cave air
FPitMod / FChimneyMod SDF TunnelNetwork SmoothMin'd into the cave SDF — §0.1's motivating case
FGridColumnMod density FlatPlain, CrystalChamber world-grid cylinders — different op from FRoomColumnMod
FShaftLedgeMod density VerticalShafts banded shelves, half-sided
FChasmMod / FBridgeMod / FRidgeMod density (MC) all archetypes (disturbances) see §0.3

The two roughness variants, because this is the trap:

  • SDF variant (Maze, VerticalShafts, FloatingIslands): Sdf += fBm(x·k, y·k, z·k)·SCALE·Rough. Raw, no fade, no clamp, fixed frequency baked into the call site (0.12, 0.1, 0.08).
  • Density variant (TunnelNetwork): two octave sets (main + fine×3), optional domain warp, four noise types, a min(…, 0) clamp so roughness can never re-fill definite air, and a quadratic fade by distance from surface.

They are not the same op with different params. Port them as one op with a Space enum and let the SDF variant's fixed frequencies become real params — that alone is a small authoring win, and OPSTACK-PLAN §2.6 explicitly permits the re-tune.

Role 4 — STRUCTURAL POST (fixed order, appended by the compiler, never author-omittable)

# Op Today Effect over box
1 FOriginSpineOp ApplyOriginSpine CarveOnly; Identity when the XY circle (R + 3) misses the box, or Z is outside the interior
2 FBoundarySealOp ApplyBoundarySeal ClassifyBox → AllSolid inside its band (forcing — see VoxelDensityOp.h); Identity when the box misses both bands
3 FPassageCarveOp ApplyPassageCarving CarveOnly; Identity via AnyPassageNearBoxalready written
4 FDiffLayerOp the diff block in GetDensityAt Both when mods intersect; Identity via HasAnyModInChunkRangealready written

The order is load-bearing and is the order the code already uses: spine carves the interior only, the seal then re-solidifies its bands (the spine deliberately never touches them), passages punch through everything including the seal, and the player wins last.

A fifth thing the plan doesn't name: FRAME OPS

Two archetypes transform the query coordinates rather than the field:

  • VerticalScaleEffectiveZ = WorldZ / VerticalScale (TunnelNetwork), stretches everything below it.
  • CaveWarpStrength/Frequency — domain-warps the coords the SDF is evaluated at, but not the coords roughness/terrain-ops/columns use. The comment is emphatic about why: "terracing stays horizontal, columns stay vertical".

So a frame op has scope — it applies to some ops below it and not others. Modelling that as "push frame / pop frame" markers in the tape is straightforward; modelling it as a per-op flag is not, because the same op can appear inside and outside a frame. Decide this before the tape format is fixed. It also has a EffectOverBox consequence: any op under a warp frame must inflate its box by the warp amplitude before answering — the existing code already does exactly this (Expansion = CaveWarpStrength + 2).


2. TunnelNetwork (and Underwater — identical rock, a water flag)

GetDensityWithParams, ~1080 lines, the biggest single function in the plugin. Port LAST — it owns BuildChunkCache's two-region window-invariance discipline (§8.4), the most delicate code here.

FRAME  VerticalScale                          (Z pre-divide, wraps everything below)
  ├─ FConstantRockSource            Replace    BaseDensity
  ├─ FRAME CaveWarp                            (SDF queries only — NOT the ops below the frame)
  │    ├─ FRoomGraphSource          → Sdf      rooms + tunnels, cached per chunk
  │    ├─ FPitMod                   → Sdf      SmoothMin, unwarped coords ⚠️
  │    └─ FChimneyMod               → Sdf      SmoothMin, unwarped coords ⚠️
  ├─ FSdfCarve(SDFBlendRadius)      Subtract
  ├─ [gate: bNearCaveSurface = Sdf < SDFBlendRadius·3]
  │    ├─ FSurfaceRoughnessMod      Add        density-space variant
  │    ├─ ── per-room op override ──           ⚠️ see below
  │    ├─ FTerraceMod               Add
  │    ├─ FLayerLineMod             Subtract
  │    ├─ FRibbingMod               Add
  │    ├─ FCaveOverhangMod          Add
  │    ├─ FCaveCliffMod             Add
  │    ├─ FScallopMod               Subtract
  │    ├─ FArchMod                  Add
  │    ├─ FRoomColumnMod            Add
  │    ├─ FDomeMod                  Subtract
  │    ├─ FPinchMod                 Add
  │    └─ FFloorBiasMod             Add
  ├─ FWormFieldSource               Subtract   ⚠️ ungated, unbounded — §0.2
  └─ [structural post ×4]

⚠️ Pits and chimneys are evaluated at UNWARPED coordinates while rooms are evaluated at warped ones, and then SmoothMin'd together. The comment justifies it (pit anchors come from unwarped room centres). Under a frame model this means pits/chimneys must sit outside the warp frame while still writing the same SDF channel. That is expressible, but it is the single fiddliest thing in the whole decomposition — budget for it and do not discover it during the port.

⚠️ The per-room terrain-op override has no clean home. Today: NearestRoomIdx picks a room, that room's hash-rolled UVoxelTerrainOpDefinition is applied onto a copy of the whole param struct, and the copy shadows Params for the rest of the function. In an op stack there is no "whole param struct" to overwrite. Two options:

  • (a) Scope by room. Each modifier gains an optional "only inside room N's influence" predicate, and the room-graph source publishes the nearest-room index per voxel as stack state. Faithful, and it generalises to "this op only inside this region", which is the Mask combiner already in the plan.
  • (b) Drop per-room ops, make modifiers strate-wide, and recover variety with Mask on a hash field. Much simpler, visibly different world.

(a) is right — per-room variety is a real feature and (b) would flatten it — but it is the piece that could blow the Phase-1 timebox if attempted early. It is also the only consumer of NearestRoomIdx, so it can be deferred: port TunnelNetwork's geometry first with strate-wide ops, add room scoping after.

Tile-skipping prize: currently zero. After the port, with §0.2's amplitude bound and the room bounds already in FCachedRoom, deep bedrock below a tunnel network becomes provably AllSolid. This is the largest single perf item in the whole plan.


3. FlatPlain and CrystalChamber

These are one op with two default setsOPSTACK-PLAN §4 calls this the first real win, and reading the code confirms it: GetSlabDensity is called for both types with no branch on which. CrystalChamber is FlatPlain with a bigger CeilingRoughness.

FSlabVoidSource                Replace    floor surface + ceiling surface → void field
FGridColumnMod                 Add        world-grid jittered cylinders, infinite height
[structural post ×4]

That is the entire archetype. Two of the eight collapse into one, and the ceiling's abs(noise) (formations hang down only, never punch up) is a two-line flag on the source.

Observed in-editor 2026-07-27, and it settles the question: Jahni reports FlatPlain and CrystalChamber render identical in the live world. They should — they share FSlabGenerationParams, and nothing in the content sets them apart. The enum promised a difference the data never delivered, in the shipped world as well as in the test fixture. So the merge does not lose a distinction; it reveals that there was none. Making CrystalChamber look like a crystal chamber is a params job — raise CeilingRoughness (6 → ~20, what SlabEquivalence's tuned pass uses) and drop CeilingRelativeHeight a little. That is authoring, which is exactly the outcome the whole refactor is aiming at.

3.1 RESOLVED 2026-07-27 — Jahni: the Z term can go. Removed.

Decision: the Z term was not intentional character. It is gone from GetSlabDensity (both surfaces), FSlabVoidSource is XY-pure, and FlatPlain + CrystalChamber are ported and wired.

What that bought, and what it cost:

  • IsXYPure() == true ⇒ the T1.a column-cache treatment becomes available generically.
  • An exact ClassifyBox with no sampling: VoxelNoise::FBM's contract is [-1,1], so both surfaces live in Z bands with known bounds — a tile entirely below the floor band is provably solid, a tile strictly between the bands is provably air. These two archetypes proved zero tiles before. VoxelForge.OpStack.SlabEquivalence reports the count.
  • Cost: the world re-tunes once. Dropping the term samples a different slice of the noise field, so floor and ceiling shapes change (they do not degrade). Covered by §2.6's explicit permission to re-tune.

The original finding, kept because it explains why the answer mattered:

FSlabVoidSource is not XY-pure, and probably should be. Both surfaces sample noise with a small Z term:

FloorNoise: FractalNoise3D(x·FF, y·FF, WorldZ·FF·0.05f)   // ← Z
CeilNoise:  FractalNoise3D(x·CF, y·CF, WorldZ·CF·0.08f)   // ← Z

A "floor surface height" that depends on the altitude you sample it from is geometrically odd — the floor is at a different height depending on which voxel asks. In practice the coefficient is tiny so it reads as a subtle vertical smear rather than a bug, and it is deterministic, so nothing is broken. But it blocks the T1.a column-cache treatment and it makes an exact ClassifyBox more awkward (the surface must be sampled per Z rather than per column).

Question for Jahni: was the ·0.05f Z term intentional character, or a leftover from copying the 3D-noise call signature? If it can go, FSlabVoidSource becomes XY-pure, gets the column cache for free, and gets an exact box classification — which means FlatPlain and CrystalChamber start skipping trivial tiles, which they never have. That is a large win for a one-character change, so it is worth asking rather than assuming either way.

Answered: it can go. See the resolution above.


4. Maze — the Phase 1 port

The plan picks Maze, and the code justifies the pick completely. ~100 lines, and it decomposes without any of the awkwardness elsewhere.

FConstantRockSource            Replace    BaseDensity
FLatticeCorridorSource         → Sdf      capsules over open lattice edges
FSurfaceRoughnessMod           → Sdf      SDF-space variant, frequency 0.12 (hardcoded today)
FSdfCarve(blend = 2.0)         Subtract
[structural post ×4]

Why it is the right first port, beyond size: edge identity is hash(lower node, axis), so two adjacent chunks cannot disagree. No BuildChunkCache, no COLLECT/STORE region, no window-invariance risk at all (AUDIT §6.4 names this the pattern to prefer). If the source/modifier split does not fall out here, it will not fall out anywhere, and that is exactly what the stop-trigger is for.

EffectOverBox: the open-edge set reachable from a box is the same {-1,0}³ node sweep the per-voxel code already does, at cell granularity. Capsule bound = CorridorRadius + roughness amplitude + blend. Identity when no open edge's capsule reaches the box — which for a sparse BranchProbability is most of the volume. Maze currently skips zero tiles; this is its first.

The proof OPSTACK-PLAN §4 asks for: once FLatticeCorridorSource exists, drop it into a SurfaceWorld strate under the terrain and confirm you get a maze inside a mountain with no C++. Note this requires §0.1's SDF channel to look good (smooth junctions where corridors meet rock); it works but reads harsh without it.


5. SurfaceWorld — biggest payoff, most care

Three XY-pure functions plus a cheap per-voxel combine. The column cache (T1.a) and the exact-lattice ClassifyTile bound must both survive the port — they are the two most valuable pieces of engineering in the file.

FHeightfieldSource             Replace    ← the whole column pipeline, XY-pure
  ├─ FStructuralHeightField                continents + mountains + detail, under a warp frame
  ├─ FCliffHeightMod                       slope-gated steepening (4 structural resamples)
  ├─ FTerraceHeightMod                     relief-gated plateaus
  ├─ FLayerLineHeightMod                   sine bands
  └─ FBeachHeightMod                       flatten toward the water line
FSkyCapSource                  Subtract   ceiling: warp + signed swell + downward-only hang
FOverhangShelfMod              Union      ⚠️ per-voxel, NOT XY-pure — the one 3D op here
[structural post ×4]

⚠️ RESOLVED 2026-07-27 — the height ops needed a SECOND OP FAMILY, not a sub-list

This section says the height ops "operate on Z values in the column, not on density" and then lists them as children of FHeightfieldSource. Writing them made the consequence unavoidable: they do not fit IVoxelDensityOp at all. Its signature is Eval(x, y, z, FVoxelOpSample&) — per voxel, density + SDF. A height op has no input Z (it produces one), is XY-pure (once per column), and writes neither channel.

The two ways to force it were both bad: a per-voxel third channel for what is a column property, or collapsing all five into one opaque op — OPSTACK-PLAN §2.5's explicit failure mode.

So height space got its own contract: VoxelHeightOp.h (FVoxelHeightSample with Height + Relief, IVoxelHeightOp, FVoxelHeightStack). Same lesson as §0.1, one step further: §0.1 found that density needed a second channel; this found that terrain needs a second space. Verified by VoxelForge.OpStack.SurfaceHeightEquivalence before anything was built on top of it — deliberately, so a wrong answer would have cost one test rather than a whole port.

Bonus the type system gives for free: a height stack cannot contain Z-dependent data, because there is no Z in the signature to put there. AUDIT §6.3 warns that Z-dependent data smuggled into FSurfaceColumn silently corrupts every chunk in the vertical stack and that ValidateDeterminism would not catch it. Here the type forbids it rather than a convention.

Critical distinction the port must preserve: the height ops (FCliffHeightMod and friends) operate on Z values in the column, not on density. They are XY-pure and belong in PrepareChunk/the column cache. FOverhangShelfMod operates per voxel and re-samples the structural heightfield at a shifted XY. Mixing those two up puts Z-dependent data in FSurfaceColumn, which AUDIT §6.3 warns silently corrupts every chunk in the vertical stack — and ValidateDeterminism, which samples along an X boundary, would not catch it.

This is the archetype where IsXYPure() earns its place in the contract: it turns an implicit convention that has to be remembered into a declaration the compiler routes on.

Biome blending: the heightfield is evaluated for the dominant biome and its nearest neighbour and the two heights are lerped. In stack terms that is the Mask combiner with a biome-weight field — which is exactly the mechanism OPSTACK-PLAN §4 Phase 3 wants for unifying strates and biomes. So SurfaceWorld's existing biome blend is the prototype for the whole Phase 3 idea, and porting it is how that gets validated.


6. VerticalShafts

FConstantRockSource            Replace
FShaftFieldSource              → Sdf      infinite cylinders (XY-only) + hash-gated connectors
FSurfaceRoughnessMod           → Sdf      SDF variant, frequency 0.1
FSdfCarve(blend = 2.0)         Subtract
FShaftLedgeMod                 Union      banded shelves, +X/+Y half only so a climb path remains
[structural post ×4]

Nearly identical in shape to Maze — same source → roughness → carve spine, different primitive. That similarity is the evidence the abstraction is real: two archetypes that look unrelated in the switch are the same three ops with a different source.

Split worth making: the shafts are XY-pure infinite cylinders; the connectors are not. Two ops (FShaftColumnSource XY-pure + FShaftConnectorSource) let the cylinder half get the column-cache treatment and answer ClassifyBox exactly in XY. Keeping them as one op forfeits that.


7. FloatingIslands

The only archetype whose source is air, which is what makes it a good composition test.

FConstantVoidSource            Replace    BaseDensity (open void)
FRAME IslandWarp                          XY domain warp (lobed outlines, amplitude ~0.35·meanR)
  └─ FIslandBlobSource         → Sdf      tapered flat-top blobs, SmoothMin'd together
FSurfaceRoughnessMod           → Sdf      SDF variant, frequency 0.08, 4 octaves
FSdfFill(SDFBlendRadius)       Union
[structural post ×4]

Note the warp here is applied to the query (WX,WY computed once per voxel and shared by all nearby islands) exactly like TunnelNetwork's cave warp — same frame concept, third instance. Three uses is enough to make frames a first-class part of the model rather than a special case.

FIslandBlobSource is the cleanest Identity opportunity in the plugin: islands are hash-placed with a known XY radius and explicit TopZ/BotZ. A box that no island's AABB reaches is provably untouched — and since a floating-island strate is mostly empty void, that is most tiles. Combined with FConstantVoidSource's ClassifyBox → AllAir, a FloatingIslands strate could go from skipping zero tiles to skipping the large majority of them.

PORTED 2026-07-28 — three deviations from the sketch above, all deliberate

  1. FConstantVoidSource and FSdfFill are not new classes. Each is the class it mirrors, with the opposite sign: FConstantFieldSource(±Base) and FSdfConvertOp(Sign = ±1), two factories each. The table above listed them as separate ops; writing them separately would have duplicated the classifier and the six-line formula for nothing. Multiplying by ±1 is exact in IEEE-754, so the three already-green ports are bit-for-bit untouched by the generalisation. This is the port's actual result: reuse by inversion rather than by identity — evidence that the abstract axis (the sign of the internal density) is the right one, not just that two archetypes happened to look alike.
  2. The warp stays inside the source; no FRAME op was built. Frames are worth building at the second real user, and two of the three (TunnelNetwork's cave warp, its tunnel warp) are not ported yet. Designing the abstraction against a single example is what this refactor has avoided throughout — cf. IVoxelBiomeField, which was born from a concrete second need. Revisit with TunnelNetwork.
  3. AUDIT §C1's last surviving site was in this archetype and was fixed in both paths in the same pass (the warp's (float)S * 0.0007f; the 2026-07-27 sweep matched SeedF * K and missed the (float)S spelling).

The Identity bound is one-sided, and that matters: Sdf ≥ WorldZ TopSurf bounds an island from above only. Below BotZ the SDF degenerates to ≈ DistXY, so a hairline thread of matter hangs down each island's axis to the strate floor. Rejecting a box because it sits below an island would be a hole. Only the top rejects.


8. Underwater

GetDensityAt routes Underwater to GetDensityWithParams with a comment: "Underwater shares tunnel rock (water table is a render-side overlay)." There is no density difference at all.

So Underwater is not an archetype, it is TunnelNetwork plus WaterLevelRelative consumed by the water render system. When ECaveGeneratorType finally disappears, this one vanishes for free — it never needed to exist as a generator type.


9. Q2 — the param audit: every field, and who claims it

FStrateGenerationParams via the VF_STRATE_PARAM_FIELDS X-macro. Every field is claimed except where flagged.

Field(s) Destination op
BaseDensity stack-global — read by every source and all four structural-post ops. Not any single op's param.
VerticalScale FRAME VerticalScale
WormFrequency, WormHorizontalBias, WormThreshold, WormStrength, WormNetworkRange FWormFieldSource
RoomSpacing, RoomDensity, MinRoomRadius, MaxRoomRadius, RoomHeightRatio, RoomShapeVariety, RoomFloorCutMin, RoomFloorCutMax, FloorReliefStrength, FloorReliefFrequency FRoomGraphSource
OriginRoomRadius, OriginRoomMaxConnections FRoomGraphSource⚠️ but see §10.3, they couple to the spine
TunnelMinRadius, TunnelMaxRadius, TunnelDensity, MaxTunnelLength, TunnelWarpStrength, TunnelHorizontalBias, bTunnelsFlowTowardOrigin, TunnelEndpointZOffset FRoomGraphSource
SDFBlendRadius FSdfCarve / FSdfFillshared, also the bNearCaveSurface gate width
CaveWarpStrength, CaveWarpFrequency FRAME CaveWarp
SurfaceRoughness, RoughnessFrequency, RoughnessNoiseType, DomainWarpStrength, DomainWarpFrequency FSurfaceRoughnessMod (density variant)
BoundarySealThickness FBoundarySealOp — and read by the spine, disturbances, shaft connectors, island spread
StrateTopWorldZ, StrateBottomWorldZ FVoxelOpContext, not op params. Runtime-injected, never author-set.
FloorBias FFloorBiasMod
TerraceStepHeight, TerraceHardness, TerraceNoiseDisplacement FTerraceMod
LayerLineSpacing, LayerLineDepth FLayerLineMod
OverhangStrength, OverhangDepth, OverhangFrequency FCaveOverhangMod ⚠️ name collision, §9.1
RibbingSpacing, RibbingDepth FRibbingMod
CliffStrength FCaveCliffMod ⚠️ name collision, §9.1
ScallopStrength, ScallopFrequency FScallopMod
ArchDensity, ArchMinRadius, ArchMaxRadius FArchMod
ColumnDensity, ColumnMinRadius, ColumnMaxRadius FRoomColumnMod ⚠️ name collision, §9.1
PitDensity, PitMinRadius, PitMaxRadius, PitDepth FPitMod
ChimneyDensity, ChimneyMinRadius, ChimneyMaxRadius, ChimneyHeight FChimneyMod
DomeDensity, DomeMinRadius, DomeMaxRadius, DomeHeightRatio FDomeMod
PinchDensity, PinchStrength, PinchLength FPinchMod
WaterLevelRelative ⚠️ claimed by no density op. See §9.2.

9.1 Three names mean different things in different structs

FStrateGenerationParams and FSurfaceGenerationParams both define CliffStrength, OverhangStrength/OverhangFrequency, TerraceHardness and ColumnDensity-family fields — and they are genuinely different operations:

Name in FStrateGenerationParams (cave) in FSurfaceGenerationParams (surface)
CliffStrength noise-modulated vertical density gradient near a cave wall slope-gated steepening of a height value
OverhangStrength fBm lobe adding rock into a cave F20 warped-terrain union making a real 3D shelf
TerraceHardness staircase edge width on a cave wall plateau/riser ratio of a height quantiser
ColumnDensity room-anchored columns (slab struct) world-grid cylinders

Today the type system keeps them apart. Once ops are data assets in one list, nothing doesDA_Op_Cliff would be ambiguous. Name them for what they operate on from day one: FCaveWallCliffMod vs FHeightSlopeCliffMod, FCaveShelfMod vs FTerrainOverhangMod. Cheap now, a rename with authored assets in the field later.

9.2 The one unclaimed field: WaterLevelRelative

It lives in FStrateGenerationParams (and is Lerp'd at every strate boundary along with the density params), but nothing in the density path reads it. Its consumers are GetWaterLevelWorldZForChunk (the water render system) and ComputeSurfaceTerrainZ's beach flattening — which reads the surface struct's copy, not this one.

Not dead, but misfiled: it is a content/render property riding in a density struct, and it is being interpolated across strate boundaries where a water plane should probably be a hard property of one strate. Report, don't delete — but when ops become assets it should move to the strate itself rather than to any op.

9.3 Fields that are context, not params

StrateTopWorldZ / StrateBottomWorldZ are runtime-injected by UVoxelStrateManager::Get*ParamsForChunk, never author-set, and every archetype's first line is a degenerate check on them. They belong in FVoxelOpContext (where the header already puts them) and should be removed from the per-op param structs so an author cannot see or set them. That deletes a whole class of "I set the Z bounds and nothing happened".


10. Ordering rules the compiler must enforce

10.1 Structural post is appended, always, in order

FOriginSpineOpFBoundarySealOpFPassageCarveOpFDiffLayerOp. Verified identical in all six density functions. An author cannot omit, reorder or insert between them.

10.2 Disturbances run after the archetype, before the diff layer

ApplyDisturbances is called in GetDensityAt after the archetype function returns (so after that function's own spine/seal/passages) and before the diff layer. So the true global order is:

[archetype stack] → spine → seal → passages → disturbances → diff layer

Disturbances self-limit to the seal interior (if (Z <= InnerBot || Z >= InnerTop) return;), which is why running them after the seal is safe. Preserve this or bridges will punch through seals.

10.3 The spine and the origin room are two systems aimed at the same place

ApplyOriginSpine carves an unconditional column at XY (0,0) in every strate; OriginRoomRadius makes the room graph put a big room there too; bOpenSurfaceEntry opens a shaft from above. Three mechanisms, one location, and AUDIT C8 already flags an unchecked invariant between OriginRoomRadius and the COLLECT margin. When these become ops, the coupling becomes visible and should be either unified or documented — right now it works by everyone independently agreeing that (0,0) is special.

  1. Maze — cleanest split, no connectivity decision, cheapest mistake. §4.
  2. FlatPlain + CrystalChamber — two archetypes → one op. Ask §3.1 first.
  3. FloatingIslands — the biggest Identity win, and the first air-source stack.
  4. VerticalShafts — same spine as Maze, validates the source-swap claim.
  5. SurfaceWorld — biggest payoff; the column cache and exact-lattice bound must survive.
  6. TunnelNetwork — last. §8.4, the warp/pit coordinate split, and the per-room op override.
  7. Underwater — falls out of 6 for free.

11. Open questions for Jahni

Ranked by how much they change the work.

  1. Two-channel Eval (density + SDF)? §0.1. Changes the contract. Cheapest to decide now. My recommendation: yes — without it, cross-source SmoothMin is impossible and "a maze inside a mountain" reads as a hole punched in rock rather than a cave that belongs there.
  2. Is FSlabVoidSource's Z-term intentional? §3.1. One character; unlocks XY-purity, the column cache and exact tile classification for two archetypes.
  3. Per-room terrain ops: scope-by-room (faithful) or strate-wide + Mask (simpler, flatter)? §2. Recommend scope-by-room, but after the geometry port, not during it.
  4. Amplitude bound on the worm carve in Phase 2 rather than Phase 3? §0.2. It is the difference between TunnelNetwork skipping tiles and never skipping tiles.
  5. Rename the colliding cave/surface op names before any asset is authored? §9.1.

Written 2026-07-27 by Opus 5, unattended, as build-free queue item Q1. Companion to OPSTACK-PLAN.md (the plan) and OPSTACK-PROGRESS.md (what is actually built).