feat(composer): lateral REGIONS -- built, measured, and gated OFF
Jahni's own idea: "archetype could even mix within the same strate at some
point, tunnel network leading to a big chamber". An archetype becomes a
spatial REGION inside a strate, not a label on it.
Built by extending the mechanism that already exists rather than inventing
one: the stack blends VERTICALLY between strates, so regions blend the same
way LATERALLY. Deterministic jittered Voronoi sites on a 256-voxel lattice,
1-3 regions per strate from (seed, strateIndex), 24-voxel symmetric blend
band, per-chunk lookup cache. Parameters are never blended -- a shaft's
ShaftSpacing means nothing to a slab -- only the resulting DENSITY is.
Structural posts stay global and unconditional, so an invented region
cannot switch off the primordial law.
single-region path BIT-IDENTICAL to legacy, 256 probes, native + recipe
cross-boundary boxes 20 of 40, 199,800 voxels brute-forced, 0 VIOLATIONS
(every seam-spanning box correctly returned Mixed)
blend-band cost 663 ns vs 338 ns per voxel, over 16.0% of voxels
~1.15x field-weighted
survival 16/16 valid, non-vacuous, largest share >= .50
⛔ AND IT IS GATED OFF, because the seam breaks the one law that matters:
arrival -> departure across a region boundary passes 9/16 seeds, against
11/16 same-region controls in the same harness. Density blending alone does
not guarantee a player can cross a seam. A strate that mixes archetypes and
strands you at the boundary is strictly worse than one that does not mix.
VF_LateralRegionsAreShippable() returns false and the test ASSERTS THE GATE
IS SHUT while the rate is imperfect -- so flipping it without fixing
connectivity turns the suite red by design. The rate is a warning, not an
assertion: it is the measured state of an off-by-default feature, not a
regression in a live world, and a permanently red suite stops being read,
which is how real failures get missed.
Everything that ships is still hard-guarded: single-region bit-identity,
zero cross-boundary box violations, and every shipping-path law test
untouched.
The fix is an explicit CROSS-SEAM CORRIDOR CONTRACT -- a design decision,
recorded rather than made unilaterally. Codex diagnosed this correctly and
declined to tune the threshold, which was the right call.
Build clean, 29 succeeded / 0 failed, run and verified by me.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
42e1f0ec8c
commit
dd38832bb6
14 files changed
+3040
-48
No files matched your search
@@ -741,9 +741,11 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// cache on the same versioned refetch as the native params; no worker reads mutable editor
|
||||
// state while the world is applying a new candidate.
|
||||
thread_local bool CP_UseComposerRecipe = false;
|
||||
thread_local bool CP_UseComposerRegions = false;
|
||||
thread_local int32 CP_ComposerSeed = 0;
|
||||
thread_local FVoxelStrateArchetypeParams CP_ComposerParams;
|
||||
thread_local FVoxelOpStackRecipe CP_ComposerRecipe;
|
||||
thread_local FVoxelStrateRegionManifest CP_ComposerRegions;
|
||||
#endif
|
||||
|
||||
const uint32 LayoutVersion = StrateManager->GetLayoutVersion();
|
||||
@@ -763,9 +765,20 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
CP_GenType = StrateManager->GetGeneratorTypeForChunk(ChunkCoord);
|
||||
#if WITH_EDITOR
|
||||
CP_UseComposerRecipe = false;
|
||||
CP_UseComposerRegions = false;
|
||||
ECaveGeneratorType ComposerArchetype = ECaveGeneratorType::TunnelNetwork;
|
||||
bool bComposerUseRecipe = false;
|
||||
if (StrateManager->GetComposerOverrideForChunk(
|
||||
if (StrateManager->GetComposerRegionOverrideForChunk(
|
||||
ChunkCoord, CP_ComposerRegions))
|
||||
{
|
||||
CP_UseComposerRegions = true;
|
||||
CP_ComposerSeed = CP_ComposerRegions.Seed;
|
||||
if (CP_ComposerRegions.Regions.Num() > 0)
|
||||
{
|
||||
CP_GenType = CP_ComposerRegions.Regions[0].Archetype;
|
||||
}
|
||||
}
|
||||
else if (StrateManager->GetComposerOverrideForChunk(
|
||||
ChunkCoord, CP_ComposerSeed, ComposerArchetype, CP_ComposerParams,
|
||||
bComposerUseRecipe, CP_ComposerRecipe))
|
||||
{
|
||||
@@ -804,11 +817,37 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co
|
||||
// Une seule branche ajoutée au chemin densité, et elle est FROIDE : la construction est
|
||||
// par chunk (comme le refetch de params juste au-dessus), jamais par voxel.
|
||||
CP_UseOpStack = StrateManager->UsesOperatorStackForChunk(ChunkCoord);
|
||||
#if WITH_EDITOR
|
||||
// A composer override is already an explicit, validated stack description. It must
|
||||
// be evaluated even when the authored definition had the normal operator-stack opt-in
|
||||
// disabled; otherwise a multi-region candidate would be installed in the manager but
|
||||
// silently fall back to the old one-archetype switch.
|
||||
CP_UseOpStack = CP_UseOpStack || CP_UseComposerRegions || CP_UseComposerRecipe;
|
||||
#endif
|
||||
if (CP_UseOpStack)
|
||||
{
|
||||
CP_OpStack = FVoxelOpStack(); // move-assign : libère l'ancienne pile
|
||||
#if WITH_EDITOR
|
||||
if (CP_UseComposerRecipe)
|
||||
if (CP_UseComposerRegions)
|
||||
{
|
||||
FVoxelOpContext RegionContext;
|
||||
if (VF_BuildStrateRegionStack(
|
||||
CP_ComposerRegions, OriginSpineRadius, StrateManager,
|
||||
CP_OpStack, RegionContext, nullptr))
|
||||
{
|
||||
RegionContext.ChunkCoord = ChunkCoord;
|
||||
RegionContext.Step = 1;
|
||||
RegionContext.LayoutVersion = LayoutVersion;
|
||||
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
RegionContext.EdgeSealThickness = EdgeSealThickness;
|
||||
CP_OpStack.PrepareChunk(RegionContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
CP_UseOpStack = false;
|
||||
}
|
||||
}
|
||||
else if (CP_UseComposerRecipe)
|
||||
{
|
||||
FVoxelOpContext ComposerContext;
|
||||
if (VF_BuildStackFromRecipe(
|
||||
@@ -2897,6 +2936,94 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
if (!StrateManager) return EVoxelTileClass::Mixed;
|
||||
|
||||
#if WITH_EDITOR
|
||||
// A multi-region composer override is already a complete parent stack: it owns the lateral
|
||||
// blend and the four global structural posts. Classify it as one field only when the complete
|
||||
// mesher box stays inside that one strate; otherwise the Z-category pass below deliberately
|
||||
// returns Mixed. Disturbances and the diff layer remain outside the stack, so either one also
|
||||
// vetoes a skip.
|
||||
FVoxelStrateRegionManifest ComposerRegions;
|
||||
const FIntVector RegionProbeChunk(
|
||||
FloorDivC(MinX, CHUNK_SIZE), FloorDivC(MinY, CHUNK_SIZE), FloorDivC(MinZ, CHUNK_SIZE));
|
||||
if (StrateManager->GetComposerRegionOverrideForChunk(RegionProbeChunk, ComposerRegions))
|
||||
{
|
||||
int32 RegionTopChunkZ = 0;
|
||||
int32 RegionBottomChunkZ = 0;
|
||||
const int32 RegionMinChunkZ = FloorDivC(MinZ, CHUNK_SIZE);
|
||||
const int32 RegionMaxChunkZ = FloorDivC(MaxZ, CHUNK_SIZE);
|
||||
if (!StrateManager->GetStrateChunkZBounds(RegionProbeChunk.Z,
|
||||
RegionTopChunkZ, RegionBottomChunkZ)
|
||||
|| RegionMinChunkZ < RegionBottomChunkZ
|
||||
|| RegionMaxChunkZ > RegionTopChunkZ)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
// A representative chunk is not enough for a proof: the mesher box can cross an XY
|
||||
// chunk edge, and disturbances are chunk-local. Require every touched chunk to remain
|
||||
// in the same slot and to have no active disturbance before delegating the proof to the
|
||||
// complete lateral stack. Any uncertainty is Mixed; a false uniform answer here can
|
||||
// skip a real seam or carve.
|
||||
const int32 RegionMinChunkX = FloorDivC(MinX, CHUNK_SIZE);
|
||||
const int32 RegionMaxChunkX = FloorDivC(MaxX, CHUNK_SIZE);
|
||||
const int32 RegionMinChunkY = FloorDivC(MinY, CHUNK_SIZE);
|
||||
const int32 RegionMaxChunkY = FloorDivC(MaxY, CHUNK_SIZE);
|
||||
for (int32 CZ = RegionMinChunkZ; CZ <= RegionMaxChunkZ; ++CZ)
|
||||
{
|
||||
int32 TouchedTopChunkZ = 0;
|
||||
int32 TouchedBottomChunkZ = 0;
|
||||
if (!StrateManager->GetStrateChunkZBounds(
|
||||
CZ, TouchedTopChunkZ, TouchedBottomChunkZ)
|
||||
|| TouchedTopChunkZ != RegionTopChunkZ
|
||||
|| TouchedBottomChunkZ != RegionBottomChunkZ)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
for (int32 CY = RegionMinChunkY; CY <= RegionMaxChunkY; ++CY)
|
||||
{
|
||||
for (int32 CX = RegionMinChunkX; CX <= RegionMaxChunkX; ++CX)
|
||||
{
|
||||
const FIntVector TouchedChunk(CX, CY, CZ);
|
||||
FVoxelStrateRegionManifest TouchedRegions;
|
||||
if (!StrateManager->GetComposerRegionOverrideForChunk(
|
||||
TouchedChunk, TouchedRegions)
|
||||
|| TouchedRegions.StrateIndex != ComposerRegions.StrateIndex
|
||||
|| TouchedRegions.Seed != ComposerRegions.Seed
|
||||
|| TouchedRegions.RegionCount != ComposerRegions.RegionCount
|
||||
|| TouchedRegions.PartitionSeed != ComposerRegions.PartitionSeed
|
||||
|| TouchedRegions.LatticeCellSize != ComposerRegions.LatticeCellSize
|
||||
|| TouchedRegions.BlendWidth != ComposerRegions.BlendWidth)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
|
||||
const FStrateDisturbanceParams Disturbances =
|
||||
StrateManager->GetDisturbanceParamsForChunk(TouchedChunk);
|
||||
if (Disturbances.ChasmDensity > 0.0f
|
||||
|| Disturbances.BridgeDensity > 0.0f
|
||||
|| Disturbances.RidgeDensity > 0.0f)
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FVoxelOpStack RegionStack;
|
||||
FVoxelOpContext RegionContext;
|
||||
if (!VF_BuildStrateRegionStack(ComposerRegions, OriginSpineRadius,
|
||||
StrateManager, RegionStack, RegionContext, nullptr))
|
||||
{
|
||||
return EVoxelTileClass::Mixed;
|
||||
}
|
||||
RegionContext.ChunkCoord = RegionProbeChunk;
|
||||
RegionContext.Step = Step;
|
||||
RegionContext.LayoutVersion = StrateManager->GetLayoutVersion();
|
||||
RegionContext.WorldRadiusVoxels = WorldRadiusVoxels;
|
||||
RegionContext.EdgeSealThickness = EdgeSealThickness;
|
||||
RegionStack.PrepareChunk(RegionContext);
|
||||
return RegionStack.ClassifyBox(TileVoxelBox, RegionContext);
|
||||
}
|
||||
|
||||
// A custom recipe is deliberately not fed to the native ClassifyBox proof yet. Returning
|
||||
// Mixed keeps the live walk-through hole-safe: GenerateMesh samples the exact recipe stack,
|
||||
// while no optimistic AllAir/AllSolid result can skip a candidate tile.
|
||||
|
||||
Reference in new issue
Block a user