feat(opstack): every op declares its channels -- ordering becomes derivable
The composer must assemble stacks nobody wrote by hand. Today the legal order of operators lives in a human's head and in the hand-written builders. COMPOSER-NOTES §3.2: an op's legal position is fixed by which channel it READS and which it WRITES, so dependencies form a DAG and any topological sort of that DAG is a legal stack. EVoxelOpChannel names the two fields FVoxelOpSample actually carries (Density, Sdf) -- read from the struct, not assumed. All 29 concrete density ops declare reads, writes and additive/transformative, filled in by reading each Eval rather than trusting its name. The declarations are PURE VIRTUAL so a new op cannot silently omit them; the composer's safety depends on every op answering. A declaration nobody checks is a comment, so ValidateChannelOrder exists and VoxelForge.OpStack.ChannelDAG runs all eight shipping stacks through it. All pass: TunnelNetwork/Underwater 20 ops, Maze 8, VerticalShafts 9, FloatingIslands 8, FlatPlain/CrystalChamber 6, SurfaceWorld 6. Metadata is snapshotted in FVoxelOpStack::Add, so no declaration virtual is ever called per voxel -- ARCHITECTURE §8.10 respected. ⚠️ THE FUSE WAS DELIBERATELY NOT APPLIED, and the reason is the finding I asked for instead. §3.2 assumed FFloorBiasMod sits next to FCaveRoughnessMod. It does not: TEN ops separate them (Terrace, LayerLine, Ribbing, Overhang, Cliff, Scallop, Arch, RoomColumn, Dome, Pinch), and all ten read and write Density. Moving FloorBias would reorder the live density accumulation, and IEEE-754 addition is not bitwise commutative -- so the note's claim that fusing "leaves the field bit-identical" is false as written. That leaves a real open question for the composer, recorded not resolved: channel rules alone say any Density->Density order is legal, but they cannot express "this op CORRECTS that one". Either the fuse proceeds and accepts a changed field (legitimate -- peer determinism never required bit-identity across builds), or corrective intent needs its own declaration. Settling it needs FFloorBiasMod's actual math read, which is the next question I put to Codex. Build clean, 22 succeeded / 0 failed, verified in VoxelM.log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
4f11ce81a4
commit
7b982ad5ae
7 files changed
+600
-19
No files matched your search
@@ -378,6 +378,11 @@ driven by `EditorBrush*` props.
|
||||
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.
|
||||
- **Operator channel metadata** (`FVoxelOpStack`): `ChannelReads`, `ChannelWrites`, and
|
||||
`IsAdditive` are called only when an operator is added to a stack and their values are cached in
|
||||
the stack entry. `ValidateChannelOrder` is assembly/diagnostic-only; no declaration virtual may
|
||||
enter `Eval`'s per-voxel loop. If `FVoxelOpSample` gains a field, add its channel bit and update
|
||||
every operator declaration and the validator together.
|
||||
- **VerticalShafts field cache**: shaft rolls and connector decisions, including the structural
|
||||
tree and structural-spine connector endpoint, are rebuilt only when the thread-local centre cell
|
||||
or a geometry-affecting parameter changes. Each rebuild rolls a wider direct-indexed geometric
|
||||
|
||||
+5
-3
@@ -102,10 +102,11 @@ reference path for opt-out strates. See [OPSTACK-PLAN.md](OPSTACK-PLAN.md) for t
|
||||
| Symbol | Notes |
|
||||
|--------|-------|
|
||||
| `EVoxelOpRole` | The four roles: `FieldSource` (what the field IS) · `Combiner` (how fields merge) · `DetailModifier` (today's `UVoxelTerrainOpDefinition`) · `StructuralPost` (spine→vertical seal→passage→XY edge seal→diff, appended automatically, never author-omittable). |
|
||||
| `EVoxelOpChannel` / `EVoxelOpChannelMask` | The explicit `FVoxelOpSample` channel set: `Density` and `Sdf` (plus `None`/`All` masks). Every density op declares the channels it reads and writes. |
|
||||
| `EVoxelOpCombine` | `Replace`/`Union`(min)/`Subtract`(max)/`SmoothUnion`/`SmoothSubtract`/`Add`/`Mask`. Sign reminder: negative = solid, so "add solid" is `min`. |
|
||||
| `EVoxelOpEffect` | `Identity`/`CarveOnly`/`FillOnly`/`Both`. Conservative: `Both` is always safe, the wrong one is a hole. |
|
||||
| `FVoxelOpContext` | Chunk-constant inputs. Carries `LayoutVersion` plus the copied global `WorldRadiusVoxels`/`EdgeSealThickness`, so a new op cannot forget them. |
|
||||
| `IVoxelDensityOp` | `PrepareChunk` / `Eval` / `EffectOverBox` / `ClassifyBox` / `IsXYPure`. |
|
||||
| `IVoxelDensityOp` | `ChannelReads` / `ChannelWrites` / `IsAdditive` plus `PrepareChunk` / `Eval` / `EffectOverBox` / `ClassifyBox` / `IsXYPure`. The three declarations are pure virtual and are snapshotted during stack assembly; none is called per voxel. |
|
||||
| `IVoxelDensityOp::ClassifyBox` | ⚠️ **not source-only.** Forcing ops (the vertical seal and XY edge seal inside their proven bands) overwrite the input entirely, which pure direction cannot express. |
|
||||
| `FVoxelOpSample` | The state threaded through the stack: **two** channels, `Density` (INTERNAL convention, **positive = SOLID**, negated to MC once by the caller) and `Sdf` (standard SDF, negative = inside). ⚠️ `min()` therefore means opposite things on the two channels. |
|
||||
| `FVoxelBoxHypotheses` + `VF_ForceHypotheses` / `VF_FoldEffect` / `VF_FoldOp` | The fold that turns a stack into an `EVoxelTileClass`. Reproduces today's hand-written `ClassifyTile` line for line — the mapping is written out in the header. |
|
||||
@@ -142,7 +143,8 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
||||
|
||||
| Symbol | Role | Notes |
|
||||
|--------|------|-------|
|
||||
| `FVoxelOpStack` | — | Ordered `TUniquePtr` list. `PrepareChunk` / `EvalInternal` / `EvalMC` / `ClassifyBox` (the fold lets a later forcing post overwrite a dead hypothesis). |
|
||||
| `FVoxelOpStack` | — | Ordered move-only list of `TUniquePtr` entries. `Add` snapshots channel metadata; `PrepareChunk` / `EvalInternal` / `EvalMC` / `ClassifyBox` (the fold lets a later forcing post overwrite a dead hypothesis). |
|
||||
| `FVoxelOpStack::ValidateChannelOrder` | — | Assembly/diagnostic-only channel DAG validator. It requires prior producers, rejects write-only clobbers and invalid additive declarations, and permits the one root SDF identity fold used by Maze. `VoxelForge.OpStack.ChannelDAG` validates all 8 shipping stacks. |
|
||||
| `FVoxelOpStack::AppendStructuralPost` | 4 | Appends spine → vertical seal → passage → XY edge seal **in that fixed order**. An author cannot omit or reorder them. The diff layer is NOT here yet — it still lives in `GetDensityAt` after the MC negate, with disturbances. |
|
||||
| `VoxelDensityOps::MakeConstantRockSource` | 1 | `Density = BaseDensity`. `ClassifyBox` → **AllSolid**, exact and free. Shared by TunnelNetwork, Maze, VerticalShafts and bedrock gaps. Class is `FConstantFieldSource` (one class, two factories). |
|
||||
| `VoxelDensityOps::MakeConstantVoidSource` | 1 | The **same class, negated**: `Density = -BaseDensity`, and `ClassifyBox` → **AllAir** — the first source in the plugin that can prove it. FloatingIslands' root; that verdict is what makes a mostly-empty island strate skippable. |
|
||||
@@ -159,7 +161,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar
|
||||
| `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. |
|
||||
| `FRoomGraphSource` (internal) | 1 | TunnelNetwork's SDF spine. **CALLS `BuildChunkCache`/`EvaluateSDFCached` — does not transcribe them**: that is where §8.4's two-region discipline lives and a copy would fork it. Owns the cave warp (scope = this op alone; pits/chimneys read *unwarped* coords, which is why no FRAME op was needed). Its cache key adds a **params CRC + LayoutVersion**; the original lacked them until AUDIT §C2 was fixed (2026-07-28) and now carries them too. **`EffectOverBox` ANSWERS SPATIALLY** since 2026-07-28 — this is the T1.d switch (measured: **6 of 40 tiles proved AllSolid at production defaults, 7986 voxels brute-forced, 0 violations**). It builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), then applies a **disjunction** per primitive: it doesn't matter if it **fails its cull** *or* if **its own SDF stays ≥ `T+K`** over the box. Cull wins for rooms (`Rmax+3K` < `Rmax+T+K`); the threshold wins hugely for tunnels, whose cull is a capsule *bounding sphere* (~107 radius for a 200-long tube of radius 7). ⚠️ **`Identity` therefore means `Sdf ≥ T`, not `Sdf == FLT_MAX`**, with `T = max(3·SDFBlendRadius, WormNetworkRange)` — **any new consumer of the `Sdf` channel must have a threshold ≤ T or be added to that max**, or it gets tiles with no geometry and no collision. The `−K` slack covers any number of primitives because `SmoothMin`'s penalty is exactly 0 once `\|A−B\| ≥ K`. Pits/chimneys use the cull only; columns are **not** tested (their sole consumer gates on `Sdf`, so the test was redundant). Verdict memoised per box; warp dilation uses a **provable** `\|Perlin3D\| ≤ 2`. |
|
||||
| `FWormFieldSource` (internal) | 1 | Fielded 3D-noise threshold carve, masked by distance to the room network (reads `InOut.Sdf` *after* pits/chimneys). **`EffectOverBox` INHERITS `FRoomGraphSource`'s verdict** since 2026-07-28 — its `Eval` sets `NetworkMask = 0` when `CaveSDF >= WormNetworkRange`, which `FLT_MAX` always satisfies, so where the room source proves `Identity` the worm doesn't execute at all. ⚠️ This was **the** blocker: `BaseDensity = 8` < `WormStrength = 10` **by default** (the field comment requires it), so an unconditional `CarveOnly` drove `SolidMargin` negative on every tile in the world and no room-source proof could survive behind it. Deliberately **not** `VF_NoCaveOverBox` — that helper answers "identity" for a null `Rooms`, which is wrong for an op that could sit behind a different SDF writer. |
|
||||
| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **COMPLETE, 20 ops** — the biggest port in the plugin (~1080 lines), done in three stages: SDF spine (A) → the twelve detail modifiers of 4b–4h (B) → the per-room op override (C) → 4 structural posts. Serves **TunnelNetwork and Underwater** from one builder. Operator order is the original's, line for line, and it is load-bearing (`FFloorBiasMod` exists to undo what `FCaveRoughnessMod` did to floors). |
|
||||
| `VoxelDensityOps::BuildTunnelNetworkStack` | — | **COMPLETE, 20 ops** — the biggest port in the plugin (~1080 lines), done in three stages: SDF spine (A) → the twelve detail modifiers of 4b–4h (B) → the per-room op override (C) → 4 structural posts. Serves **TunnelNetwork and Underwater** from one builder. Operator order is the original's, line for line, and it is load-bearing (`FFloorBiasMod` exists to undo what `FCaveRoughnessMod` did to floors). The requested 4b→4h fuse is pending: all 10 intervening ops touch `Density`, so moving the corrective phase fails the bit-identity precondition. |
|
||||
| `FCaveRoughnessMod` (internal) | 3 | STEP 4b, **density space** — a different op from `MakeSdfRoughnessMod`: two octave sets, optional domain warp, four noise types, an anti-fill clamp inside definite air, quadratic fade. ⚠️ **Reads STRATE params, not the per-room copy** — the original's shadow is declared *after* step 4b. Eleven of twelve modifiers read the room copy; this one does not. |
|
||||
| `FCaveTerraceMod` (internal) | 3 | STEP 4c. The only modifier that **re-queries the SDF** (Z±1, through `FRoomGraphSource::ProbeSdfUnwarped`) for its horizontality gate — which is why the room source's cache is exposed at all. ⚠️ Those probes use unwarped X/Y and raw Z although the field was evaluated warped: transcribed as-is, see OPSTACK-PROGRESS. |
|
||||
| `FLayerLineMod` / `FRibbingMod` (internal) | 3 | The same sine along Z: cubed and subtracted (grooves) vs quarter-phase-shifted, squared and added (ribs). `CarveOnly` / `FillOnly` — two of the few detail modifiers that keep a usable direction for the fold. |
|
||||
|
||||
+34
-6
@@ -88,12 +88,16 @@ terrain from those, deterministically, exactly as today. **No runtime compositio
|
||||
|
||||
### 3.2 Structure: how a stack gets assembled
|
||||
|
||||
The three real builders, for reference:
|
||||
The current shipping builders, for reference (structural posts included; TunnelNetwork's builder is
|
||||
also the Underwater builder):
|
||||
|
||||
```
|
||||
TunnelNetwork (19) rock source -> room graph (SDF) -> SdfCarve -> 12 density modifiers -> worm source -> structural
|
||||
Maze (7) rock source -> lattice corridors (SDF) -> SdfRoughness (SDF) -> SdfCarve -> structural
|
||||
FloatingIslands (7) VOID source -> island blobs (SDF) -> SdfRoughness (SDF) -> SdfFill -> structural
|
||||
TunnelNetwork / Underwater (20) rock source -> room graph (SDF) -> SdfCarve -> 12 density modifiers -> worm source -> structural
|
||||
FlatPlain / CrystalChamber (6) slab-void source -> grid columns -> structural
|
||||
Maze (8) rock source -> lattice corridors (SDF) -> SdfRoughness (SDF) -> SdfCarve -> structural
|
||||
SurfaceWorld (6) surface column -> overhang -> structural
|
||||
VerticalShafts (9) rock source -> shaft field (SDF) -> SdfRoughness (SDF) -> SdfCarve -> ledges -> structural
|
||||
FloatingIslands (8) VOID source -> island blobs (SDF) -> SdfRoughness (SDF) -> SdfFill -> structural
|
||||
```
|
||||
|
||||
**⭐ Most ordering is MECHANICAL — derive it, do not author it.** Roughness sits on the *SDF* before
|
||||
@@ -103,6 +107,13 @@ which it writes**.
|
||||
⇒ **Every operator declares its channel reads/writes.** Dependencies form a DAG and **any topological
|
||||
sort of that DAG is a legal stack** — no authored order, and it produces orderings nobody wrote down.
|
||||
|
||||
**Built status — 2026-09-04:** `EVoxelOpChannel` now names the two fields actually carried by
|
||||
`FVoxelOpSample` (`Density` and `Sdf`); all 29 concrete density operators declare reads, writes, and
|
||||
additive/transformative behavior. `FVoxelOpStack::Add` snapshots that metadata, and
|
||||
`ValidateChannelOrder` checks the resulting channel DAG without entering the voxel loop.
|
||||
`VoxelForge.OpStack.ChannelDAG` builds all eight shipping stacks and validates them: **22 tests
|
||||
succeeded, 0 failed, 0 not run**.
|
||||
|
||||
**⭐ Root polarity is a one-bit identity lever.** FloatingIslands is Maze's op classes rooted in a
|
||||
**void** source with a **fill** instead of a rock source with a carve — and you get islands instead of
|
||||
tunnels.
|
||||
@@ -114,8 +125,17 @@ into one op with two internal phases. That removes the invalid state instead of
|
||||
ops that compose in any legal order beat eighteen with a footnote. (Fusing with identical order and
|
||||
math leaves the field bit-identical.)
|
||||
|
||||
**Current implementation finding:** the fuse is not landed. Ten operators intervene between 4b and
|
||||
4h — `FCaveTerraceMod`, `FLayerLineMod`, `FRibbingMod`, `FCaveOverhangMod`, `FCaveCliffMod`,
|
||||
`FScallopMod`, `FCaveArchMod`, `FRoomColumnMod`, `FDomeMod`, and `FPinchMod` — and every one reads
|
||||
and writes `Density`. Moving the floor phase next to roughness would therefore reorder the live
|
||||
density updates; the required bit-identical gate cannot be assumed. The current 20-op stack remains
|
||||
in its original order until a fusion that preserves that order and math is available.
|
||||
|
||||
**One declaration covers the rest:** an op is **additive** (small displacement, commutes freely — shuffle
|
||||
at will) or **transformative** (clamps, multiplies, gates — position matters, needs explicit placement).
|
||||
“Commutes” here is algebraic: IEEE-754 float accumulation is not generally bitwise commutative, so a
|
||||
shuffle still needs the equivalence gate.
|
||||
|
||||
**The structure roll:**
|
||||
`root polarity -> shape source -> conversion (follows from polarity) -> draw k modifiers legal in the
|
||||
@@ -485,8 +505,10 @@ they became "findings" (§13).
|
||||
(fall exposure, openness, traversal mix, tortuosity, corpus-centroid distance).
|
||||
|
||||
### Tier 3 — op-system prerequisites for free composition
|
||||
- **Channel read/write declarations** on every op, so ordering derives from a DAG (§3.2).
|
||||
- **Fuse corrective ops** (`FFloorBiasMod` into `FCaveRoughnessMod`).
|
||||
- ✅ **Channel read/write declarations** on every op, plus the stack DAG validator (§3.2; built
|
||||
2026-09-04).
|
||||
- ⏸ **Fuse corrective ops** (`FFloorBiasMod` into `FCaveRoughnessMod`) — blocked by the ten
|
||||
intervening `Density` readers/writers under the bit-identity requirement; see §3.2.
|
||||
- ⚠️ **`EffectOverBox` correct in ISOLATION** (§6.1) — the gate on everything after.
|
||||
- **The XY edge seal** (§6.7).
|
||||
|
||||
@@ -560,6 +582,12 @@ DiffLayerContention · FloatingIslandEquivalence · LargeSeedSurvives · **Layou
|
||||
LiveEditInvalidation · MazeEquivalence · SlabEquivalence · **StrateParamBlendCoverage** ·
|
||||
SurfaceHeightEquivalence · TunnelNetworkSpineEquivalence · VerticalShaftEquivalence.
|
||||
|
||||
**2026-09-04 — Tier 3b validation run.** The specified UE 5.7 `VoxelMEditor Win64 Development`
|
||||
build succeeded (14 actions). The full headless suite reported **22 succeeded, 0 failed, 0 not run,
|
||||
0 succeeded-with-warnings**, including `VoxelForge.OpStack.ChannelDAG`; the existing equivalence,
|
||||
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
|
||||
commits `4d33321` (Sol's boundary fold) and `91585ea` as *built but NOT re-verified*, with two
|
||||
acceptance conditions. Both now met:
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
// VoxelForgeOpStackChannelTest.cpp
|
||||
// The channel declarations are executable stack-assembly metadata, not comments.
|
||||
|
||||
#if WITH_DEV_AUTOMATION_TESTS
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||
FVoxelForgeOpStackChannelTest,
|
||||
"VoxelForge.OpStack.ChannelDAG",
|
||||
EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
namespace
|
||||
{
|
||||
/** A deliberately small declaration-only op for the validator's negative controls. */
|
||||
class FDeclarationProbeOp final : public IVoxelDensityOp
|
||||
{
|
||||
public:
|
||||
FDeclarationProbeOp(EVoxelOpChannelMask InReads,
|
||||
EVoxelOpChannelMask InWrites,
|
||||
bool bInAdditive)
|
||||
: Reads(InReads), Writes(InWrites), bAdditive(bInAdditive)
|
||||
{}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return Reads; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return Writes; }
|
||||
bool IsAdditive() const override { return bAdditive; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
void Eval(float, float, float, FVoxelOpSample&) const override {}
|
||||
EVoxelOpEffect EffectOverBox(const FBox&, const FVoxelOpContext&) const override
|
||||
{
|
||||
return EVoxelOpEffect::Identity;
|
||||
}
|
||||
|
||||
const TCHAR* DebugName() const override { return TEXT("DeclarationProbeOp"); }
|
||||
|
||||
private:
|
||||
EVoxelOpChannelMask Reads;
|
||||
EVoxelOpChannelMask Writes;
|
||||
bool bAdditive;
|
||||
};
|
||||
}
|
||||
|
||||
bool FVoxelForgeOpStackChannelTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
using namespace VoxelForgeTest;
|
||||
|
||||
FTestWorld World;
|
||||
World.Build();
|
||||
if (!World.IsValid())
|
||||
{
|
||||
AddError(World.WhyInvalid());
|
||||
return false;
|
||||
}
|
||||
|
||||
const UVoxelGenerator* Generator = World.Generator.Get();
|
||||
const int32 Seed = World.Settings->Seed;
|
||||
|
||||
auto MidChunkForSlot = [&](int32 Slot, const TCHAR* Name, int32& OutMidChunkZ) -> bool
|
||||
{
|
||||
int32 TopVoxelZ = 0;
|
||||
int32 BottomVoxelZ = 0;
|
||||
if (!World.GetSlotVoxelZRange(Slot, TopVoxelZ, BottomVoxelZ))
|
||||
{
|
||||
AddError(FString::Printf(
|
||||
TEXT("The fixture has no %s slot; check FTestWorld::Build's Archetypes[] and "
|
||||
"the corresponding slot constant."), Name));
|
||||
return false;
|
||||
}
|
||||
OutMidChunkZ = ((TopVoxelZ + BottomVoxelZ) / 2) / CHUNK_SIZE;
|
||||
return true;
|
||||
};
|
||||
|
||||
auto Validate = [&](FVoxelOpStack& Stack, const TCHAR* Name, int32 ExpectedNum)
|
||||
{
|
||||
TestEqual(*FString::Printf(TEXT("%s shipping stack op count"), Name),
|
||||
Stack.Num(), ExpectedNum);
|
||||
|
||||
FString Error;
|
||||
const bool bValid = Stack.ValidateChannelOrder(&Error);
|
||||
if (!bValid)
|
||||
{
|
||||
AddError(FString::Printf(TEXT("%s shipping stack failed channel validation: %s"),
|
||||
Name, *Error));
|
||||
}
|
||||
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.
|
||||
int32 MidChunkZ = 0;
|
||||
|
||||
if (MidChunkForSlot(FTestWorld::SlotTunnelNetwork, TEXT("TunnelNetwork"), MidChunkZ))
|
||||
{
|
||||
const FStrateGenerationParams Params =
|
||||
World.StrateManager->GetGenerationParams(FIntVector(0, 0, MidChunkZ));
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(
|
||||
Stack, Params, Seed, Generator->OriginSpineRadius, World.StrateManager.Get());
|
||||
Validate(Stack, TEXT("TunnelNetwork"), 20);
|
||||
}
|
||||
|
||||
if (MidChunkForSlot(FTestWorld::SlotFlatPlain, TEXT("FlatPlain"), MidChunkZ))
|
||||
{
|
||||
const FSlabGenerationParams Params =
|
||||
World.StrateManager->GetSlabParamsForChunk(FIntVector(0, 0, MidChunkZ));
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildSlabStack(
|
||||
Stack, Params, Seed, Generator->OriginSpineRadius, World.StrateManager.Get());
|
||||
Validate(Stack, TEXT("FlatPlain"), 6);
|
||||
}
|
||||
|
||||
if (MidChunkForSlot(FTestWorld::SlotCrystalChamber, TEXT("CrystalChamber"), MidChunkZ))
|
||||
{
|
||||
const FSlabGenerationParams Params =
|
||||
World.StrateManager->GetSlabParamsForChunk(FIntVector(0, 0, MidChunkZ));
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildSlabStack(
|
||||
Stack, Params, Seed, Generator->OriginSpineRadius, World.StrateManager.Get());
|
||||
Validate(Stack, TEXT("CrystalChamber"), 6);
|
||||
}
|
||||
|
||||
if (MidChunkForSlot(FTestWorld::SlotMaze, TEXT("Maze"), MidChunkZ))
|
||||
{
|
||||
const FMazeGenerationParams Params =
|
||||
World.StrateManager->GetMazeParamsForChunk(FIntVector(0, 0, MidChunkZ));
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildMazeStack(
|
||||
Stack, Params, Seed, Generator->OriginSpineRadius, World.StrateManager.Get());
|
||||
Validate(Stack, TEXT("Maze"), 8);
|
||||
}
|
||||
|
||||
if (MidChunkForSlot(FTestWorld::SlotSurfaceWorld, TEXT("SurfaceWorld"), MidChunkZ))
|
||||
{
|
||||
const FSurfaceGenerationParams Params =
|
||||
World.StrateManager->GetSurfaceParamsForChunk(FIntVector(0, 0, MidChunkZ));
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildSurfaceStack(
|
||||
Stack, Params, Seed, Generator->OriginSpineRadius, World.StrateManager.Get());
|
||||
Validate(Stack, TEXT("SurfaceWorld"), 6);
|
||||
}
|
||||
|
||||
if (MidChunkForSlot(FTestWorld::SlotVerticalShafts, TEXT("VerticalShafts"), MidChunkZ))
|
||||
{
|
||||
const FVerticalShaftParams Params =
|
||||
World.StrateManager->GetVerticalShaftParamsForChunk(FIntVector(0, 0, MidChunkZ));
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildVerticalShaftStack(
|
||||
Stack, Params, Seed, Generator->OriginSpineRadius, World.StrateManager.Get());
|
||||
Validate(Stack, TEXT("VerticalShafts"), 9);
|
||||
}
|
||||
|
||||
if (MidChunkForSlot(FTestWorld::SlotFloatingIsland, TEXT("FloatingIslands"), MidChunkZ))
|
||||
{
|
||||
const FFloatingIslandParams Params =
|
||||
World.StrateManager->GetFloatingIslandParamsForChunk(FIntVector(0, 0, MidChunkZ));
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildFloatingIslandStack(
|
||||
Stack, Params, Seed, Generator->OriginSpineRadius, World.StrateManager.Get());
|
||||
Validate(Stack, TEXT("FloatingIslands"), 8);
|
||||
}
|
||||
|
||||
if (MidChunkForSlot(FTestWorld::SlotUnderwater, TEXT("Underwater"), MidChunkZ))
|
||||
{
|
||||
const FStrateGenerationParams Params =
|
||||
World.StrateManager->GetGenerationParams(FIntVector(0, 0, MidChunkZ));
|
||||
FVoxelOpStack Stack;
|
||||
VoxelDensityOps::BuildTunnelNetworkStack(
|
||||
Stack, Params, Seed, Generator->OriginSpineRadius, World.StrateManager.Get());
|
||||
Validate(Stack, TEXT("Underwater"), 20);
|
||||
}
|
||||
|
||||
// A declaration that reads an unavailable channel must fail, and a second write-only
|
||||
// replacement must not silently erase the first producer. These controls pin the validator's
|
||||
// two most important safety rules independently of the shipping builders.
|
||||
{
|
||||
FVoxelOpStack MissingProducer;
|
||||
MissingProducer.Add(MakeUnique<FDeclarationProbeOp>(
|
||||
VoxelOpChannels::Density, VoxelOpChannels::Density, true));
|
||||
FString Error;
|
||||
TestFalse(TEXT("a read before its first producer is rejected"),
|
||||
MissingProducer.ValidateChannelOrder(&Error));
|
||||
}
|
||||
|
||||
{
|
||||
FVoxelOpStack Clobber;
|
||||
Clobber.Add(MakeUnique<FDeclarationProbeOp>(
|
||||
VoxelOpChannels::None, VoxelOpChannels::Density, false));
|
||||
Clobber.Add(MakeUnique<FDeclarationProbeOp>(
|
||||
VoxelOpChannels::None, VoxelOpChannels::Density, false));
|
||||
FString Error;
|
||||
TestFalse(TEXT("a second write-only replacement is rejected"),
|
||||
Clobber.ValidateChannelOrder(&Error));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_DEV_AUTOMATION_TESTS
|
||||
@@ -226,6 +226,9 @@ namespace
|
||||
explicit FConstantFieldSource(float InValue) : Value(InValue) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
bool IsXYPure() const override { return true; } // constant ⇒ trivialement sans Z
|
||||
|
||||
@@ -291,6 +294,9 @@ namespace
|
||||
{}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Sdf; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -464,6 +470,9 @@ namespace
|
||||
}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
// ⚠️⚠️ CORRIGÉ 2026-07-27 : c'était `true`, ET C'ÉTAIT FAUX.
|
||||
@@ -901,6 +910,9 @@ namespace
|
||||
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; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
bool IsXYPure() const override { return false; } // voir le bloc ci-dessus
|
||||
|
||||
/**
|
||||
@@ -1029,6 +1041,9 @@ namespace
|
||||
: P(InP), SeedU((uint32)Seed), Column(InColumn) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
bool IsXYPure() const override { return false; } // franchement non : voir `Frac`
|
||||
|
||||
@@ -1097,6 +1112,9 @@ namespace
|
||||
, BaseOctaves(InBaseOctaves), ApplyWithin(InApplyWithin) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Sdf; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
|
||||
bool IsAdditive() const override { return false; } // gate depends on the SDF it writes
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -1163,6 +1181,9 @@ namespace
|
||||
{}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
bool IsXYPure() const override { return true; } // cylindres de hauteur infinie
|
||||
|
||||
@@ -1303,6 +1324,12 @@ namespace
|
||||
: Blend(InBlend), BaseDensity(InBaseDensity), Sign(InSign), MinDivisor(InMinDivisor) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::Combiner; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float, float, float, FVoxelOpSample& InOut) const override
|
||||
@@ -1341,6 +1368,9 @@ namespace
|
||||
: TopZ(InTopZ), BotZ(InBotZ), Seal(InSeal), Base(InBase), Radius(InRadius) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override
|
||||
@@ -1383,6 +1413,9 @@ namespace
|
||||
: TopZ(InTopZ), BotZ(InBotZ), Thickness(InThickness), Base(InBase) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float, float, float Z, FVoxelOpSample& InOut) const override
|
||||
@@ -1452,6 +1485,9 @@ namespace
|
||||
: Base(InBase) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
|
||||
void PrepareChunk(const FVoxelOpContext& Ctx) override
|
||||
{
|
||||
@@ -1511,6 +1547,9 @@ namespace
|
||||
: Manager(InManager), Base(InBase), Seal(InSeal) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::StructuralPost; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::Density; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float X, float Y, float Z, FVoxelOpSample& InOut) const override
|
||||
@@ -1569,6 +1608,9 @@ namespace
|
||||
, ExtraReach(InExtraReach), SpineRadius(InSpineRadius) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
struct FShaft { float X, Y, R; int32 CellX, CellY; bool bOriginSpine; };
|
||||
@@ -2193,6 +2235,12 @@ namespace
|
||||
: P(InP), Field(InField) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -2276,6 +2324,9 @@ namespace
|
||||
, ExtraReach(InExtraReach) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
struct FIsland { float X, Y, Rxy, TopHalf, TopZ, BotZ, TaperEnd; };
|
||||
@@ -2510,6 +2561,9 @@ namespace
|
||||
{}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
EVoxelOpChannelMask ChannelReads() const override { return VoxelOpChannels::None; }
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Sdf; }
|
||||
bool IsAdditive() const override { return false; }
|
||||
|
||||
void PrepareChunk(const FVoxelOpContext& Ctx) override
|
||||
{
|
||||
@@ -3318,6 +3372,12 @@ namespace
|
||||
: P(InP), SeedU((uint32)Seed), RoomsForBox(InRoomsForBox) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -3489,6 +3549,12 @@ namespace
|
||||
: P(InP), SeedU((uint32)Seed), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -3587,6 +3653,12 @@ namespace
|
||||
: P(InP), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -3663,6 +3735,12 @@ namespace
|
||||
: P(InP), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -3727,6 +3805,12 @@ namespace
|
||||
: P(InP), SeedU((uint32)Seed), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -3807,6 +3891,12 @@ namespace
|
||||
: P(InP), SeedU((uint32)Seed), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -3870,6 +3960,12 @@ namespace
|
||||
: P(InP), SeedU((uint32)Seed), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -3942,6 +4038,12 @@ namespace
|
||||
: P(InP), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -4048,6 +4150,12 @@ namespace
|
||||
: P(InP), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float, FVoxelOpSample& InOut) const override
|
||||
@@ -4099,6 +4207,12 @@ namespace
|
||||
: P(InP), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -4195,6 +4309,12 @@ namespace
|
||||
: P(InP), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -4290,6 +4410,10 @@ namespace
|
||||
// ⚠️ DERNIER DE LA CHAÎNE, ET CE N'EST PAS INTERCHANGEABLE : il corrige ce que la rugosité (4b)
|
||||
// a fait. Le déplacer avant elle le rendrait sans objet. C'est la raison pour laquelle l'ordre
|
||||
// des opérateurs dans `BuildTunnelNetworkStack` est celui de l'original, ligne pour ligne.
|
||||
//
|
||||
// TIER 3b AUDIT (2026-09-04): this op is intentionally still separate. 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
|
||||
{
|
||||
public:
|
||||
@@ -4297,6 +4421,12 @@ namespace
|
||||
: P(InP), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float, float, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -4357,6 +4487,12 @@ namespace
|
||||
: P(InP), SeedU((uint32)Seed), Rooms(InRooms) {}
|
||||
|
||||
EVoxelOpRole GetRole() const override { return EVoxelOpRole::FieldSource; }
|
||||
EVoxelOpChannelMask ChannelReads() const override
|
||||
{
|
||||
return VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
}
|
||||
EVoxelOpChannelMask ChannelWrites() const override { return VoxelOpChannels::Density; }
|
||||
bool IsAdditive() const override { return true; }
|
||||
void PrepareChunk(const FVoxelOpContext&) override {}
|
||||
|
||||
void Eval(float WorldX, float WorldY, float WorldZ, FVoxelOpSample& InOut) const override
|
||||
@@ -4546,6 +4682,116 @@ VoxelDensityOps::FRoomBoxDiagnostic VoxelDensityOps::GetLastRoomBoxDiagnostic()
|
||||
// FVoxelOpStack
|
||||
//=============================================================================
|
||||
|
||||
bool FVoxelOpStack::ValidateChannelOrder(FString* OutError) const
|
||||
{
|
||||
if (OutError != nullptr) { OutError->Reset(); }
|
||||
|
||||
auto DescribeMask = [](EVoxelOpChannelMask Mask) -> FString
|
||||
{
|
||||
FString Result;
|
||||
if ((Mask & VoxelOpChannels::Density) != 0) { Result += TEXT("Density"); }
|
||||
if ((Mask & VoxelOpChannels::Sdf) != 0)
|
||||
{
|
||||
if (!Result.IsEmpty()) { Result += TEXT(", "); }
|
||||
Result += TEXT("Sdf");
|
||||
}
|
||||
return Result.IsEmpty() ? TEXT("none") : Result;
|
||||
};
|
||||
|
||||
auto Fail = [&](int32 Index, const FOpEntry* Entry, const TCHAR* Rule) -> bool
|
||||
{
|
||||
if (OutError != nullptr)
|
||||
{
|
||||
const TCHAR* Name = (Entry != nullptr && Entry->Op.Get() != nullptr)
|
||||
? Entry->Op->DebugName() : TEXT("(null op)");
|
||||
const EVoxelOpChannelMask Reads = Entry != nullptr ? Entry->Reads : VoxelOpChannels::None;
|
||||
const EVoxelOpChannelMask Writes = Entry != nullptr ? Entry->Writes : VoxelOpChannels::None;
|
||||
*OutError = FString::Printf(
|
||||
TEXT("op %d (%s) violates channel DAG: %s; reads=[%s], writes=[%s]"),
|
||||
Index, Name, Rule, *DescribeMask(Reads), *DescribeMask(Writes));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// The two array slots are deliberately explicit: FVoxelOpSample has exactly two fields, and
|
||||
// adding a third field requires extending EVoxelOpChannel and this validator together.
|
||||
int32 LastWriter[2] = { INDEX_NONE, INDEX_NONE };
|
||||
|
||||
auto ChannelIndex = [](EVoxelOpChannelMask Channel) -> int32
|
||||
{
|
||||
return Channel == VoxelOpChannels::Density ? 0 : 1;
|
||||
};
|
||||
|
||||
for (int32 Index = 0; Index < Ops.Num(); ++Index)
|
||||
{
|
||||
const FOpEntry& Entry = Ops[Index];
|
||||
if (Entry.Op.Get() == nullptr) { return Fail(Index, &Entry, TEXT("null operator")); }
|
||||
|
||||
if ((Entry.Reads & VoxelOpChannels::All) != Entry.Reads)
|
||||
{
|
||||
return Fail(Index, &Entry, TEXT("reads an unknown sample channel"));
|
||||
}
|
||||
if ((Entry.Writes & VoxelOpChannels::All) != Entry.Writes)
|
||||
{
|
||||
return Fail(Index, &Entry, TEXT("writes an unknown sample channel"));
|
||||
}
|
||||
|
||||
if (Entry.bAdditive && Entry.Writes == VoxelOpChannels::None)
|
||||
{
|
||||
return Fail(Index, &Entry, TEXT("an additive operator must publish a channel"));
|
||||
}
|
||||
if (Entry.bAdditive
|
||||
&& (Entry.Writes & static_cast<EVoxelOpChannelMask>(~Entry.Reads)) != 0)
|
||||
{
|
||||
return Fail(Index, &Entry,
|
||||
TEXT("an additive operator must read every channel it writes"));
|
||||
}
|
||||
|
||||
const EVoxelOpChannelMask Channels[] = {
|
||||
VoxelOpChannels::Density, VoxelOpChannels::Sdf
|
||||
};
|
||||
for (const EVoxelOpChannelMask Channel : Channels)
|
||||
{
|
||||
if ((Entry.Reads & Channel) == 0) { continue; }
|
||||
|
||||
const int32 Writer = LastWriter[ChannelIndex(Channel)];
|
||||
const bool bRootIdentityFold = Entry.Op->GetRole() == EVoxelOpRole::FieldSource
|
||||
&& (Entry.Writes & Channel) != 0
|
||||
&& Writer == INDEX_NONE;
|
||||
if (Writer == INDEX_NONE && !bRootIdentityFold)
|
||||
{
|
||||
return Fail(Index, &Entry,
|
||||
TEXT("reads a channel before a producer has published it"));
|
||||
}
|
||||
}
|
||||
|
||||
for (const EVoxelOpChannelMask Channel : Channels)
|
||||
{
|
||||
if ((Entry.Writes & Channel) == 0 || (Entry.Reads & Channel) != 0) { continue; }
|
||||
|
||||
// A write-only op is an assignment/replacement. It may establish the first version,
|
||||
// but it may not erase a previously produced channel without declaring a read of it.
|
||||
if (LastWriter[ChannelIndex(Channel)] != INDEX_NONE)
|
||||
{
|
||||
return Fail(Index, &Entry,
|
||||
TEXT("a write-only replacement would clobber an existing channel"));
|
||||
}
|
||||
}
|
||||
|
||||
for (const EVoxelOpChannelMask Channel : Channels)
|
||||
{
|
||||
if ((Entry.Writes & Channel) != 0)
|
||||
{
|
||||
// Every read above has either consumed this writer's predecessor or the explicit
|
||||
// root identity. Updating the version here makes all later consumer edges forward.
|
||||
LastWriter[ChannelIndex(Channel)] = Index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FVoxelOpStack::AppendStructuralPost(float StrateTopWorldZ, float StrateBottomWorldZ,
|
||||
float SealThickness, float BaseDensity, float SpineRadius,
|
||||
const UVoxelStrateManager* StrateManager)
|
||||
|
||||
@@ -113,6 +113,34 @@ enum class EVoxelOpRole : uint8
|
||||
StructuralPost,
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
// CANAUX DE L'ÉCHANTILLON / SAMPLE CHANNELS
|
||||
//=============================================================================
|
||||
// `FVoxelOpSample` is deliberately a small, explicit state record. Keep its channel set here,
|
||||
// next to the interface that declares which fields an operator consumes and publishes. The enum
|
||||
// values are bits so a declaration can name more than one channel without introducing another
|
||||
// per-voxel object or a dynamic container.
|
||||
//
|
||||
// `Density` is the internal positive=solid field; `Sdf` is the standard negative=inside field.
|
||||
// If a future field is added to `FVoxelOpSample`, it must also be added here and to every operator
|
||||
// declaration before the composer can treat that field as part of the dependency graph.
|
||||
enum class EVoxelOpChannel : uint8
|
||||
{
|
||||
None = 0,
|
||||
Density = 1 << 0,
|
||||
Sdf = 1 << 1,
|
||||
};
|
||||
|
||||
using EVoxelOpChannelMask = uint8;
|
||||
|
||||
namespace VoxelOpChannels
|
||||
{
|
||||
static constexpr EVoxelOpChannelMask None = 0;
|
||||
static constexpr EVoxelOpChannelMask Density = static_cast<EVoxelOpChannelMask>(EVoxelOpChannel::Density);
|
||||
static constexpr EVoxelOpChannelMask Sdf = static_cast<EVoxelOpChannelMask>(EVoxelOpChannel::Sdf);
|
||||
static constexpr EVoxelOpChannelMask All = Density | Sdf;
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// COMPOSITION / COMBINERS
|
||||
//=============================================================================
|
||||
@@ -270,6 +298,28 @@ public:
|
||||
|
||||
virtual EVoxelOpRole GetRole() const = 0;
|
||||
|
||||
/**
|
||||
* Channel contract for the composer. A read is a field inspected from `InOut` before this op
|
||||
* publishes its result; a write is a field whose value may differ when the op returns. A
|
||||
* read/write declaration therefore means "transform the current version". `+=` and `-=` are
|
||||
* both reads and writes; an assignment that ignores the old field is a write-only producer /
|
||||
* replacement. Context, coordinates, caches and referenced objects are not sample channels.
|
||||
*
|
||||
* These are graph metadata, not voxel work: the stack snapshots them while it is assembled and
|
||||
* the validator reads the snapshot. They must never be called from `Eval`.
|
||||
*/
|
||||
virtual EVoxelOpChannelMask ChannelReads() const = 0;
|
||||
virtual EVoxelOpChannelMask ChannelWrites() const = 0;
|
||||
|
||||
/**
|
||||
* True when this op contributes a delta to its written channels without replacing, clamping,
|
||||
* selecting, or otherwise depending on the previous value of those written channels. Such an
|
||||
* op may be shuffled with another compatible additive delta; a false result means its position
|
||||
* is explicit. A gate on a channel the op itself writes is transformative even if its final
|
||||
* arithmetic contains `+=`.
|
||||
*/
|
||||
virtual bool IsAdditive() const = 0;
|
||||
|
||||
/**
|
||||
* Hisser ici TOUT le travail constant sur le chunk : listes de salles, grilles de biome,
|
||||
* caches de colonnes, cuissons de treillis. Appelé une fois par chunk et par worker.
|
||||
|
||||
@@ -59,6 +59,17 @@ class UVoxelStrateManager;
|
||||
*/
|
||||
class FVoxelOpStack
|
||||
{
|
||||
private:
|
||||
/** Metadata is snapshotted when an op enters the stack so no declaration virtual is ever
|
||||
* dispatched from `EvalSample`'s per-voxel loop. */
|
||||
struct FOpEntry
|
||||
{
|
||||
TUniquePtr<IVoxelDensityOp> Op;
|
||||
EVoxelOpChannelMask Reads = VoxelOpChannels::None;
|
||||
EVoxelOpChannelMask Writes = VoxelOpChannels::None;
|
||||
bool bAdditive = false;
|
||||
};
|
||||
|
||||
public:
|
||||
// DÉPLAÇABLE, PAS COPIABLE — et c'est la bonne sémantique, pas un contournement de compilateur :
|
||||
// une pile POSSÈDE ses opérateurs de façon unique. La copier voudrait dire cloner des opérateurs
|
||||
@@ -66,13 +77,13 @@ public:
|
||||
//
|
||||
// ⚠️ NOTE COMPILATEUR : ne PAS remettre `VOXELFORGE_API` sur la classe. Sous MSVC, dllexport sur
|
||||
// une classe force l'instanciation de TOUS ses membres implicites, y compris l'opérateur
|
||||
// d'affectation par copie — impossible à générer pour un `TArray<TUniquePtr<...>>`, d'où
|
||||
// d'affectation par copie — impossible à générer pour un `TArray<FOpEntry>`, d'où
|
||||
// l'erreur C2280 « fonction supprimée ». L'export va sur la seule méthode hors-ligne.
|
||||
//
|
||||
// MOVE-ONLY, and that is the correct semantics rather than a compiler workaround: a stack
|
||||
// uniquely OWNS its operators. Do NOT put VOXELFORGE_API back on the class — under MSVC,
|
||||
// dllexport forces instantiation of every implicit member including copy-assignment, which
|
||||
// cannot be generated for a TArray<TUniquePtr<...>> (error C2280). Export the out-of-line
|
||||
// cannot be generated for a TArray<FOpEntry> (error C2280). Export the out-of-line
|
||||
// method instead.
|
||||
FVoxelOpStack() = default;
|
||||
FVoxelOpStack(FVoxelOpStack&&) = default;
|
||||
@@ -80,7 +91,20 @@ public:
|
||||
FVoxelOpStack(const FVoxelOpStack&) = delete;
|
||||
FVoxelOpStack& operator=(const FVoxelOpStack&) = delete;
|
||||
|
||||
void Add(TUniquePtr<IVoxelDensityOp> Op) { Ops.Add(MoveTemp(Op)); }
|
||||
void Add(TUniquePtr<IVoxelDensityOp> Op)
|
||||
{
|
||||
FOpEntry Entry;
|
||||
Entry.Op = MoveTemp(Op);
|
||||
if (Entry.Op.Get() != nullptr)
|
||||
{
|
||||
// Assembly-time metadata only. ValidateChannelOrder reuses this snapshot, and Eval
|
||||
// never asks an operator for its declaration.
|
||||
Entry.Reads = Entry.Op->ChannelReads();
|
||||
Entry.Writes = Entry.Op->ChannelWrites();
|
||||
Entry.bAdditive = Entry.Op->IsAdditive();
|
||||
}
|
||||
Ops.Add(MoveTemp(Entry));
|
||||
}
|
||||
|
||||
int32 Num() const { return Ops.Num(); }
|
||||
|
||||
@@ -89,7 +113,7 @@ public:
|
||||
* mensonge utile qui finirait par masquer une course. */
|
||||
void PrepareChunk(const FVoxelOpContext& Ctx)
|
||||
{
|
||||
for (const TUniquePtr<IVoxelDensityOp>& Op : Ops) { Op->PrepareChunk(Ctx); }
|
||||
for (const FOpEntry& Entry : Ops) { Entry.Op->PrepareChunk(Ctx); }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +134,7 @@ public:
|
||||
FVoxelOpSample EvalSample(float WorldX, float WorldY, float WorldZ) const
|
||||
{
|
||||
FVoxelOpSample S;
|
||||
for (const TUniquePtr<IVoxelDensityOp>& Op : Ops) { Op->Eval(WorldX, WorldY, WorldZ, S); }
|
||||
for (const FOpEntry& Entry : Ops) { Entry.Op->Eval(WorldX, WorldY, WorldZ, S); }
|
||||
return S;
|
||||
}
|
||||
|
||||
@@ -128,9 +152,9 @@ public:
|
||||
EVoxelTileClass ClassifyBox(const FBox& VoxelBox, const FVoxelOpContext& Ctx) const
|
||||
{
|
||||
FVoxelBoxHypotheses H;
|
||||
for (const TUniquePtr<IVoxelDensityOp>& Op : Ops)
|
||||
for (const FOpEntry& Entry : Ops)
|
||||
{
|
||||
VF_FoldOp(H, *Op, VoxelBox, Ctx);
|
||||
VF_FoldOp(H, *Entry.Op, VoxelBox, Ctx);
|
||||
// Do not early-out on a dead hypothesis: a later forcing structural post may
|
||||
// deliberately overwrite it (the XY edge seal is appended after passage carving).
|
||||
}
|
||||
@@ -165,7 +189,7 @@ public:
|
||||
const bool bSolidBefore = H.bCanBeAllSolid;
|
||||
const bool bAirBefore = H.bCanBeAllAir;
|
||||
|
||||
VF_FoldOp(H, *Ops[i], VoxelBox, Ctx);
|
||||
VF_FoldOp(H, *Ops[i].Op, VoxelBox, Ctx);
|
||||
|
||||
if (bSolidBefore && !H.bCanBeAllSolid && OutSolidKiller == INDEX_NONE) { OutSolidKiller = i; }
|
||||
if (bAirBefore && !H.bCanBeAllAir && OutAirKiller == INDEX_NONE) { OutAirKiller = i; }
|
||||
@@ -177,9 +201,31 @@ public:
|
||||
/** Nom lisible d'un opérateur, pour les rapports de test. Voir `IVoxelDensityOp::DebugName`. */
|
||||
const TCHAR* GetOpDebugName(int32 Index) const
|
||||
{
|
||||
return Ops.IsValidIndex(Index) ? Ops[Index]->DebugName() : TEXT("(none)");
|
||||
return Ops.IsValidIndex(Index) ? Ops[Index].Op->DebugName() : TEXT("(none)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the stack's channel dependency DAG without evaluating a voxel.
|
||||
*
|
||||
* Rule: each channel has a current version. A channel is unavailable until an earlier op
|
||||
* writes it. The one deliberate exception is a root `FieldSource` that reads and writes the
|
||||
* same channel as an identity fold (for example `min(FLT_MAX, Sdf)`); it may consume that
|
||||
* channel's `FVoxelOpSample` initial identity while publishing the first real version. An op
|
||||
* may read only the version visible on entry; a read/write op consumes that version and
|
||||
* publishes the next one. A write-only op is a replacement/producer, so it is legal only as
|
||||
* the first producer of that channel; allowing a second write-only producer would silently
|
||||
* clobber the earlier field. An additive op must read every channel it writes, because an
|
||||
* additive delta cannot introduce or replace a channel. All producer-to-consumer edges
|
||||
* therefore point forward in the list, and the list is a legal topological ordering of the
|
||||
* channel DAG. A later read deliberately sees the writer's post-op version; a consumer that
|
||||
* needs a pre-write value must be placed before that writer. The declarations do not invent
|
||||
* dependencies from names, roles, or Eval side effects.
|
||||
*
|
||||
* This is assembly/diagnostic work only. It must not be called from Eval or any voxel loop.
|
||||
* Returns false and optionally describes the first violation.
|
||||
*/
|
||||
VOXELFORGE_API bool ValidateChannelOrder(FString* OutError = nullptr) const;
|
||||
|
||||
/**
|
||||
* RÔLE 4 — ajoute les invariants de monde, dans l'ordre fixe, à la fin de la pile.
|
||||
* spine (0,0) → seal de frontière → carve de passage → seal de limite XY.
|
||||
@@ -201,7 +247,7 @@ public:
|
||||
const UVoxelStrateManager* StrateManager);
|
||||
|
||||
private:
|
||||
TArray<TUniquePtr<IVoxelDensityOp>> Ops;
|
||||
TArray<FOpEntry> Ops;
|
||||
};
|
||||
|
||||
//=============================================================================
|
||||
|
||||
Reference in new issue
Block a user