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:
Fr0zkaandClaude Opus 5 committed 2026-09-04 04:30:45 +02:00
1 parent 4f11ce81a4
commit 7b982ad5ae
7 files changed
+600 -19

No files matched your search

@@ -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)
+50
View File
@@ -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.
+56 -10
View File
@@ -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;
};
//=============================================================================