fix(gen): the phantom terracing is the PASSAGE support writer -- plus a region verdict cache

The owner played the build and reported four things. Three are answered with evidence,
one is disproven, and my own leading hypothesis was wrong.

⭐ 1. THE PHANTOM TERRACING IS NOT MARCHING-CUBES ALIASING
He reported stairs surviving with bTunnelFloorTerracingEnabled = false. I suspected MC
lattice aliasing on a near-planar floor; I was wrong. An A/B over the same mesher,
counted in pixels:

  graph-tunnel floor removed     2,813 / 147,456 px changed   1.91%
  passage support removed       25,695 / 147,456 px changed  17.43%

⇒ The bands are the INTER-STRATE PASSAGE SUPPORT WRITER, which is independent of the
tunnel terracing flag. That is exactly why turning terracing off did nothing: he was
disabling the wrong system. FCaveTerraceMod is inactive (TerraceStepHeight=0) and was
never involved.

⚠️ It cannot simply be switched off. With it disabled:
  fit 20,804 -> 20,511, walk-reachable 10,884 -> 7,525, components 260 -> 272.
Route still connected, but a third of reachable space gone. It stays as a correctness
backstop. The native fix is the same pattern we just applied to tunnels: author the
inter-strate passage floor profile during passage CONSTRUCTION. Codex judged that
refactor too large to risk this round and said so rather than attempting it.

MC aliasing remains theoretically possible at shallow angles (Lorensen & Cline 1987),
but the same-mesher A/B proves our writer dominates. Dual Contouring (Ju et al.) was
considered and rejected as a disproportionate mesher/topology change.

2. THE ROUGHNESS COST DEFECT DOES NOT EXIST -- my hypothesis, cleanly disproven
  FBM 2.91s / 66.92% skips · Ridged 2.85s / 66.53% · Mixed 2.92s / 66.82%
  Cellular 2.99s / 66.43% · FBM frequency 0.025-0.5: 2.88-2.99s, 66.80-67.04%
I predicted a shared bound across noise types collapsing the classifier. The code
already carries separate conservative bounds per type. No roughness code was changed.
⇒ Whatever the owner saw is not steady-state export cost. Most likely the in-editor
regeneration a parameter change triggers, which is the full-rebuild cost, not the
per-sample cost. Worth confirming with him.

3. ROOM MOUTH RISE -- his idea, implemented, off by default
  RoomMouthRiseBlendVoxels, default 8.0
  blend 8   fit 20,804  reach 10,884  40,429 v / 75,416 t   capability-NEUTRAL
  blend 24  fit 20,792  reach 10,874  40,337 v / 75,232 t   REJECTED, loses capability
Relief preserved at blend 8 (dihedral p50 2.5584, p90 33.7191). The chosen camera did
not see a mouth bench, so no visual claim is made.

4. ⭐ VERDICT CACHE WITH REGION REUSE -- answers "why re-prove what we proved?"
  130 classification calls -> 1 store, 129 hits, of which 128 ENCLOSING-REGION hits.
The region level is the one that matters: deep rock is empty thousands of chunks at a
time, and one proof over a large box now serves all of them.

Soundness, which is the whole risk (a stale "empty" silently deletes geometry):
512-entry bounded LRU; stores ONLY AllAir/AllSolid, never Mixed; key includes seed,
layout/lifetime id, params fingerprint, diff version, density/seal settings, support
mode, lattice step, origin and extent; dynamic season/composer/terrain-op/biome
configs FAIL CLOSED and bypass reuse. Deliberately session-resident, not on disk --
cross-process persistence cannot serialise all asset/layout/edit state safely yet.

VERIFIED BY ME: build 0 errors; export 1.353 / 1.430 / 1.432 s (2260c4d was
1.320 / 1.424 / 1.439); geometry SHA-256 b3e5f4c3..9b377 IDENTICAL to 2260c4d.
Peak RSS ~1.75 GB. Determinism repeat equal.

⚠️ My composer hypothesis was also wrong: I suspected
`bTunnelFloorTerracingEnabled = CategoryRng.FRand() < 0.75f` in the composer was
overriding his setting. It is not the cause here. The design question it raises is
still open and worth the owner's view: should a composer roll be allowed to overwrite
an explicitly authored value? Otherwise "I turned it off" never reliably means anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-11 13:13:28 +02:00
1 parent 2260c4d47a
commit 4f8b4a9a27
10 files changed
+958 -16

No files matched your search

@@ -20,6 +20,7 @@
#include "VoxelDensityProfile.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"
#include "HAL/PlatformTime.h"
#include "Misc/ScopeLock.h"
#if WITH_EDITOR
#include "VoxelStrateComposer.h"
@@ -1314,6 +1315,169 @@ UVoxelGenerator::UVoxelGenerator()
{
}
void UVoxelGenerator::InvalidateTileVerdictCache() const
{
FScopeLock Lock(&TileVerdictCacheLock);
TileVerdictCache.Reset();
TileVerdictCacheClock = 0;
}
bool UVoxelGenerator::FindTileVerdict(
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion,
uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites,
EVoxelTileClass& OutVerdict, bool& bOutRegionHit) const
{
bOutRegionHit = false;
FScopeLock Lock(&TileVerdictCacheLock);
++TileVerdictCacheClock;
const int64 RequestMaxX = static_cast<int64>(OriginVoxels.X)
+ static_cast<int64>(CellsPerAxis) * Step;
const int64 RequestMaxY = static_cast<int64>(OriginVoxels.Y)
+ static_cast<int64>(CellsPerAxis) * Step;
const int64 RequestMaxZ = static_cast<int64>(OriginVoxels.Z)
+ static_cast<int64>(CellsPerAxis) * Step;
int32 BestIndex = INDEX_NONE;
bool bBestExact = false;
for (int32 Index = 0; Index < TileVerdictCache.Num(); ++Index)
{
FTileVerdictCacheEntry& Entry = TileVerdictCache[Index];
if (Entry.GeneratorSeed != Seed
|| Entry.ManagerWorldSeed != ManagerWorldSeed
|| Entry.OriginSpineRadius != OriginSpineRadius
|| Entry.WorldRadiusVoxels != WorldRadiusVoxels
|| Entry.EdgeSealThickness != EdgeSealThickness
|| Entry.ManagerLifetimeId != (StrateManager ? StrateManager->GetCacheLifetimeId() : 0)
|| Entry.LayoutVersion != LayoutVersion
|| Entry.DiffVersion != DiffVersion
|| Entry.ParamsFingerprint != ParamsFingerprint
|| Entry.PassageSupportFloorWrites != PassageSupportFloorWrites
|| Entry.Step != Step
|| Entry.CellsPerAxis < CellsPerAxis)
{
continue;
}
const int64 EntryMaxX = static_cast<int64>(Entry.Origin.X)
+ static_cast<int64>(Entry.CellsPerAxis) * Step;
const int64 EntryMaxY = static_cast<int64>(Entry.Origin.Y)
+ static_cast<int64>(Entry.CellsPerAxis) * Step;
const int64 EntryMaxZ = static_cast<int64>(Entry.Origin.Z)
+ static_cast<int64>(Entry.CellsPerAxis) * Step;
const int64 DeltaX = static_cast<int64>(OriginVoxels.X) - Entry.Origin.X;
const int64 DeltaY = static_cast<int64>(OriginVoxels.Y) - Entry.Origin.Y;
const int64 DeltaZ = static_cast<int64>(OriginVoxels.Z) - Entry.Origin.Z;
if (DeltaX < 0 || DeltaY < 0 || DeltaZ < 0
|| (DeltaX % Step) != 0 || (DeltaY % Step) != 0 || (DeltaZ % Step) != 0
|| RequestMaxX > EntryMaxX || RequestMaxY > EntryMaxY || RequestMaxZ > EntryMaxZ)
{
continue;
}
const bool bExact = Entry.Origin == OriginVoxels
&& Entry.CellsPerAxis == CellsPerAxis;
if (BestIndex == INDEX_NONE || (bExact && !bBestExact)
|| (bExact == bBestExact
&& Entry.CellsPerAxis < TileVerdictCache[BestIndex].CellsPerAxis))
{
BestIndex = Index;
bBestExact = bExact;
}
}
if (BestIndex == INDEX_NONE)
{
return false;
}
FTileVerdictCacheEntry& Entry = TileVerdictCache[BestIndex];
Entry.LastUse = TileVerdictCacheClock;
OutVerdict = Entry.Verdict;
bOutRegionHit = !(Entry.Origin == OriginVoxels
&& Entry.CellsPerAxis == CellsPerAxis);
return OutVerdict != EVoxelTileClass::Mixed;
}
void UVoxelGenerator::StoreTileVerdict(
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion,
uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites,
EVoxelTileClass Verdict) const
{
if (Verdict == EVoxelTileClass::Mixed)
{
return;
}
FScopeLock Lock(&TileVerdictCacheLock);
++TileVerdictCacheClock;
int32 SlotIndex = INDEX_NONE;
int32 VictimIndex = INDEX_NONE;
uint32 OldestUse = MAX_uint32;
for (int32 Index = 0; Index < TileVerdictCache.Num(); ++Index)
{
FTileVerdictCacheEntry& Entry = TileVerdictCache[Index];
if (Entry.GeneratorSeed == Seed
&& Entry.ManagerWorldSeed == ManagerWorldSeed
&& Entry.OriginSpineRadius == OriginSpineRadius
&& Entry.WorldRadiusVoxels == WorldRadiusVoxels
&& Entry.EdgeSealThickness == EdgeSealThickness
&& Entry.ManagerLifetimeId == (StrateManager ? StrateManager->GetCacheLifetimeId() : 0)
&& Entry.LayoutVersion == LayoutVersion
&& Entry.DiffVersion == DiffVersion
&& Entry.ParamsFingerprint == ParamsFingerprint
&& Entry.PassageSupportFloorWrites == PassageSupportFloorWrites
&& Entry.Origin == OriginVoxels
&& Entry.Step == Step
&& Entry.CellsPerAxis == CellsPerAxis)
{
SlotIndex = Index;
break;
}
if (Entry.LastUse < OldestUse)
{
OldestUse = Entry.LastUse;
VictimIndex = Index;
}
}
if (SlotIndex == INDEX_NONE)
{
if (TileVerdictCache.Num() < TileVerdictCacheCapacity)
{
SlotIndex = TileVerdictCache.AddDefaulted();
}
else if (VictimIndex != INDEX_NONE)
{
SlotIndex = VictimIndex;
}
else
{
return;
}
}
FTileVerdictCacheEntry& Entry = TileVerdictCache[SlotIndex];
Entry.Origin = OriginVoxels;
Entry.Step = Step;
Entry.CellsPerAxis = CellsPerAxis;
Entry.GeneratorSeed = Seed;
Entry.ManagerWorldSeed = ManagerWorldSeed;
Entry.OriginSpineRadius = OriginSpineRadius;
Entry.WorldRadiusVoxels = WorldRadiusVoxels;
Entry.EdgeSealThickness = EdgeSealThickness;
Entry.ManagerLifetimeId = StrateManager ? StrateManager->GetCacheLifetimeId() : 0;
Entry.LayoutVersion = LayoutVersion;
Entry.DiffVersion = DiffVersion;
Entry.ParamsFingerprint = ParamsFingerprint;
Entry.PassageSupportFloorWrites = PassageSupportFloorWrites;
Entry.Verdict = Verdict;
Entry.LastUse = TileVerdictCacheClock;
}
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
{
// Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas
@@ -1322,6 +1486,7 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 14.0f;
WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f;
EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 64.0f;
InvalidateTileVerdictCache();
}
float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) const
@@ -4571,6 +4736,92 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels,
EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, int32 Step,
int32 CellsPerAxis,
FVoxelTileClassificationStats* OutStats) const
{
if (OutStats)
{
*OutStats = FVoxelTileClassificationStats();
}
const int32 NormalizedStep = FMath::Max(1, Step);
const int32 NormalizedCellsPerAxis = FMath::Clamp(CellsPerAxis, 2, CHUNK_SIZE);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileClassifyCalls);
const uint64 ParamsFingerprint = StrateManager
? StrateManager->GetGenerationParamsFingerprint() : 0;
const bool bCacheable = ParamsFingerprint != 0;
const uint32 LayoutVersion = StrateManager ? StrateManager->GetLayoutVersion() : 0;
const uint32 DiffVersion = DiffLayer ? DiffLayer->GetModsVersion() : 0;
const int32 ManagerWorldSeed = StrateManager ? StrateManager->GetWorldSeed() : 0;
const uint8 PassageSupportFloorWrites = StrateManager
&& StrateManager->ArePassageSupportFloorWritesEnabledForDiagnostics() ? 1u : 0u;
EVoxelTileClass CachedVerdict = EVoxelTileClass::Mixed;
bool bRegionHit = false;
if (bCacheable
&& FindTileVerdict(
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis,
ManagerWorldSeed, LayoutVersion, DiffVersion, ParamsFingerprint,
PassageSupportFloorWrites, CachedVerdict, bRegionHit))
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileVerdictCacheHits);
if (bRegionHit)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileVerdictRegionHits);
}
if (CachedVerdict == EVoxelTileClass::AllSolid)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileClassifyAllSolid);
}
else if (CachedVerdict == EVoxelTileClass::AllAir)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileClassifyAllAir);
}
else
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileClassifyMixed);
}
return CachedVerdict;
}
const EVoxelTileClass Verdict = ClassifyTileUncached(
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, OutStats);
if (Verdict == EVoxelTileClass::AllSolid)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileClassifyAllSolid);
}
else if (Verdict == EVoxelTileClass::AllAir)
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileClassifyAllAir);
}
else
{
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileClassifyMixed);
}
if (bCacheable && Verdict != EVoxelTileClass::Mixed)
{
StoreTileVerdict(
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis,
ManagerWorldSeed, LayoutVersion, DiffVersion, ParamsFingerprint,
PassageSupportFloorWrites, Verdict);
VoxelDensityProfile::AddCounter(
VoxelDensityProfile::ECounter::TileVerdictCacheStores);
}
return Verdict;
}
EVoxelTileClass UVoxelGenerator::ClassifyTileUncached(
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
FVoxelTileClassificationStats* OutStats) const
{
if (OutStats) { *OutStats = FVoxelTileClassificationStats(); }
// Mêmes clamps que GenerateMesh — le verdict doit couvrir le treillis réellement échantillonné.