fix(correctness): cold-audit fixes -- tile-cache determinism bug, cache identities, lifetimes

Correctness round on AUDIT-COLD-2026-09-14.md (Codex; key fixes reviewed by me).

#7 (determinism, introduced by the tile-cache work): the refetch guard in GetDensityAt now
rebuilds on `bTileCacheContext != CP_UsesTileCacheWindow` or when the worker-local stack/core were
moved into a cache entry (`CP_ActiveTunnelDensityCache != nullptr`). A cache miss can no longer
evaluate a moved-from, empty operator stack. Test (VoxelForgeCorrectnessRoundTest): the pre-fix
binary CRASHED in the context-switch case (negative control); the fixed binary passes with 0/5
coarse and fine mismatches, and warm vs fresh origin-tile meshes are identical.

Also fixed, each with a focused test that passes:
- #3/#8: worker diff/surface/biome caches carry owner/layer identity (no cross-world reuse);
  #9: the shaft key gains LedgeSpacing/LedgeDepth and the island key BoundarySealThickness;
  #14: clearing passages bumps PassagesVersion; #21: HasAnyModInChunkRange is proportional to
  the queried range (the sealed-rock proof calls it per candidate).
- #1: the density-volume fill thread is paused/drained with generation; #2: each content manager
  joins its own decoration tasks before destruction; #15: non-finite/invalid modification
  geometry is rejected; #20: zero-width transition blend is guarded at runtime.
- Also #4-#6, #10-#13, #16-#18, #22. #19 is valid C++20 and unchanged.

Verified: canonical export b3e5f4c3..9b377; capability 20,830 / 10,909; fresh trace 841/841
identical, 0 origin-column mismatches; static mesher calls 18,078,130. Performance: two pairs
disagree in sign, and the means are final -2.5% static / -0.8% moving; no repeatable regression.

Known and NOT caused by this round: ClassifyTileSoundness and PassageLandsInOpenSpace were already
failing (15/17 in the grouped suite). Queued for investigation.

Ledger: first determinism launch hit the DDC fatal (crash folder 57DB6ADC, 23:21); the retry
exited 1 on the two known failures; one malformed multi-test launch was inconclusive; one
malformed trace launch was stopped; one launch with an unquoted project path found no test and its
retry passed. Everything else passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-15 00:29:46 +02:00
1 parent 39c306b524
commit 8879045627
14 files changed
+1365 -139

No files matched your search

+127 -38
View File
@@ -231,6 +231,14 @@ public:
if (World->ContentManager && !World->ContentManager->WaitForDecorationTasks(Deadline)) return;
// DensityVolume owns a separate FRunnableThread; the world task counter and decoration
// counter cannot observe it. Stop/join it before mutating the generator or manager.
if (World->DensityVolume)
{
World->DensityVolume->PauseForGenerationChange();
bDensityVolumePaused = true;
}
bAcquired = true;
}
@@ -238,6 +246,10 @@ public:
{
if (World)
{
if (bDensityVolumePaused && World->DensityVolume)
{
World->DensityVolume->ResumeAfterGenerationChange();
}
World->bGenerationPaused.store(false, std::memory_order_release);
}
}
@@ -247,6 +259,7 @@ public:
private:
AVoxelWorld* World = nullptr;
bool bAcquired = false;
bool bDensityVolumePaused = false;
};
#if WITH_EDITOR
@@ -561,6 +574,7 @@ void AVoxelWorld::RegenerateAllChunks()
void AVoxelWorld::RebuildStrates()
{
bool bLayoutRebuilt = true;
bool bCreatedStrateManager = false;
{
FScopedGenerationPause Guard(this);
if (!Guard.Acquired())
@@ -569,14 +583,73 @@ void AVoxelWorld::RebuildStrates()
return;
}
// A live Settings reference can change from no strata to a populated pool/season. Create
// the manager lazily in that case; Initialize itself is transactional, so an existing
// valid manager remains untouched when the new asset is invalid.
if (!StrateManager && Settings
&& (!Settings->Season.IsNull() || Settings->StratePool.Num() > 0
|| Settings->FixedStrates.Num() > 0))
{
StrateManager = NewObject<UVoxelStrateManager>(this);
bCreatedStrateManager = true;
}
if (StrateManager && Settings)
{
// Re-applies layout + inter-strate gap + passage/spine settings from VoxelSettings.
bLayoutRebuilt = StrateManager->Initialize(
Settings, Settings->GetEffectiveWorldSeed());
}
if (AtmosphereManager) AtmosphereManager->Reset();
if (ContentManager) ContentManager->ClearAll();
if (!bLayoutRebuilt)
{
// A newly-created manager has no valid previous state to preserve. Do not leave the
// generator wired to a failed temporary object when this was the first live setup.
if (bCreatedStrateManager) { StrateManager = nullptr; }
}
else
{
// Rebind every consumer of the settings snapshot while generation is paused. The
// manager layout alone is not sufficient: world radius, edge sealing, seed, mesher
// policy, edit budget, decoration state, and the optional density volume all cache
// settings-derived values independently.
if (Generator)
{
Generator->InitializeSettings(Settings);
Generator->SetStrateManager(StrateManager);
Generator->SetDiffLayer(DiffLayer);
}
if (Mesher && Settings)
{
Mesher->bGenerateSkirts = Settings->bGenerateSkirts;
Mesher->SkirtCells = Settings->SkirtCells;
Mesher->LODOctaveDrop = Settings->LODOctaveDrop;
Mesher->bUseBlockEarlyOut = GVoxelForgeUseBlockEarlyOut != 0;
}
if (DiffLayer && Settings)
{
DiffLayer->SetBudget(Settings->MaxModifications,
Settings->MaxBrushRadius,
Settings->MaxTotalVolume);
}
if (ContentManager)
{
ContentManager->Initialize(
this, StrateManager, Generator, Settings,
Settings ? Settings->GetEffectiveWorldSeed() : 0);
ContentManager->ClearAll();
}
if (Settings && Settings->bEnableDensityVolume && !DensityVolume)
{
DensityVolume = NewObject<UVoxelDensityVolume>(this);
}
if (DensityVolume && Settings)
{
DensityVolume->Initialize(this, Generator, Settings);
DensityVolume->Reset();
}
if (AtmosphereManager) AtmosphereManager->Reset();
}
}
if (!bLayoutRebuilt)
@@ -896,12 +969,21 @@ void AVoxelWorld::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedE
{
Super::PostEditChangeProperty(PropertyChangedEvent);
// During PIE with live edit on, regenerate when the actor's own properties change
// (e.g., Settings reference, bLiveEditStrates toggle, etc.)
// Data asset edits (strate definitions) are handled separately by OnObjectModifiedInEditor.
// During PIE with live edit on, a Settings reference change needs the full rebind path: the
// manager, generator, mesher, diff budget, decorations and density volume each own a snapshot.
// Data asset edits (and the settings asset itself) are handled separately by
// OnObjectModifiedInEditor.
if (bLiveEditStrates && GetWorld() && GetWorld()->IsPlayInEditor())
{
RegenerateAllChunks();
const FName ChangedProperty = PropertyChangedEvent.GetPropertyName();
if (ChangedProperty == GET_MEMBER_NAME_CHECKED(AVoxelWorld, Settings))
{
RebuildStrates();
}
else
{
RegenerateAllChunks();
}
}
}
@@ -910,13 +992,20 @@ void AVoxelWorld::OnObjectModifiedInEditor(UObject* ModifiedObject)
// Only react during PIE with live edit enabled
if (!bLiveEditStrates || !GetWorld() || !GetWorld()->IsPlayInEditor()) return;
// Only care about strate definition and terrain op definition edits.
// (This delegate fires for EVERY UObject modification in the editor.)
// Only care about generation-owned assets. This delegate fires for every UObject modification
// in the editor.
bool bIsRelevant = false;
FString AssetName;
// Case 1: A strate definition was modified
if (UVoxelStrateDefinition* ModifiedStrate = Cast<UVoxelStrateDefinition>(ModifiedObject))
// Case 1: the settings asset itself was modified
if (UVoxelSettings* ModifiedSettings = Cast<UVoxelSettings>(ModifiedObject))
{
if (ModifiedSettings != Settings) return;
bIsRelevant = true;
AssetName = ModifiedSettings->GetName();
}
// Case 2: A strate definition was modified
else if (UVoxelStrateDefinition* ModifiedStrate = Cast<UVoxelStrateDefinition>(ModifiedObject))
{
if (!Settings) return;
AssetName = ModifiedStrate->GetName();
@@ -935,7 +1024,7 @@ void AVoxelWorld::OnObjectModifiedInEditor(UObject* ModifiedObject)
}
}
}
// Case 2: A terrain op definition was modified — check if any strate references it
// Case 3: A terrain op definition was modified — check if any strate references it
else if (UVoxelTerrainOpDefinition* ModifiedOp = Cast<UVoxelTerrainOpDefinition>(ModifiedObject))
{
if (!Settings || !StrateManager) return;
@@ -966,33 +1055,31 @@ void AVoxelWorld::OnObjectModifiedInEditor(UObject* ModifiedObject)
}
}
}
// Case 4: a biome definition can change both terrain parameters and decoration/material
// resolution. Treat it like the owning strate definition, but only when this world references it.
else if (UVoxelBiomeDefinition* ModifiedBiome = Cast<UVoxelBiomeDefinition>(ModifiedObject))
{
if (!Settings || !StrateManager) return;
AssetName = ModifiedBiome->GetName();
for (const FStrateSlot& Slot : StrateManager->GetLayout())
{
if (Slot.Definition && Slot.Definition->Biomes.Contains(ModifiedBiome))
{
bIsRelevant = true;
break;
}
}
}
if (!bIsRelevant) return;
UE_LOG(LogTemp, Log, TEXT("[VoxelWorld] Live edit: '%s' modified, regenerating..."),
*AssetName);
// Re-initialize the strate manager so it picks up the changed definition values,
// then regenerate all chunks with the updated params.
{
FScopedGenerationPause Guard(this);
if (!Guard.Acquired())
{
UE_LOG(LogTemp, Error, TEXT("[VoxelWorld] OnObjectModifiedInEditor: generation pause timed out; no mutation applied."));
return;
}
if (StrateManager)
{
StrateManager->Initialize(Settings, Settings->GetEffectiveWorldSeed());
}
if (Generator)
{
Generator->InitializeSettings(Settings);
}
}
RegenerateAllChunks();
// Use the same transactional full rebind as a Settings-reference change. In particular, do not
// ignore Initialize's return value: an invalid live asset must leave the previous field intact.
RebuildStrates();
}
#endif
@@ -1311,7 +1398,8 @@ void AVoxelWorld::BeginPlay()
// Système de strates — a cooked season takes precedence; otherwise the authored pool path is
// exactly the legacy one.
if (!Settings->Season.IsNull() || Settings->StratePool.Num() > 0)
if (!Settings->Season.IsNull() || Settings->StratePool.Num() > 0
|| Settings->FixedStrates.Num() > 0)
{
StrateManager = NewObject<UVoxelStrateManager>(this);
if (!StrateManager->Initialize(Settings, Settings->GetEffectiveWorldSeed()))
@@ -5550,13 +5638,14 @@ void AVoxelWorld::ApplyModification(const FVoxelModification& Modification)
bSyncedCenter ? 1 : 0);
}
// Remove decorations inside the modified volume so grass doesn't float over a dug hole (or bury under a
// fill). Instant + flicker-free (only the affected instances go); the placer already skips carved columns
// on any future rebuild. Center/Radius are in voxels → world cm. Box/capsule use their bounding sphere.
// Invalidate the complete decoration build state after a terrain edit. This removes live instances,
// abandons in-flight pre-edit marches, and forces the next stationary update to resample the edited
// field. Clearing all regions is deliberately conservative: FVoxelModification is actor-local voxel
// geometry and may be a box/capsule, while decoration transforms are world-space; a partial sphere
// conversion here would risk leaving floaters or allowing an old result to resurrect one.
if (ContentManager && AffectedChunks.Num() > 0)
{
const FVector WorldCenter = Modification.Center * VOXEL_SIZE; // Center was WorldPos/VOXEL_SIZE
ContentManager->RemoveDecorationsInSphere(WorldCenter, Modification.Radius * VOXEL_SIZE);
ContentManager->InvalidateDecorationBuilds();
}
}