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

@@ -71,6 +71,7 @@ void UVoxelDensityVolume::Initialize(AActor* InOwner, UVoxelGenerator* InGenerat
Generator = InGenerator;
Settings = InSettings;
bShuttingDown.store(false, std::memory_order_relaxed);
bGenerationPaused.store(false, std::memory_order_relaxed);
bInitialized = true;
// Arrays are allocated lazily on the first Update (EnsureAllocated) so a settings change
// (resolution / level count) before play picks up cleanly.
@@ -80,6 +81,7 @@ void UVoxelDensityVolume::BeginDestroy()
{
// Backstop — EndPlay → NotifyShutdown should already have stopped the fill thread.
bShuttingDown.store(true, std::memory_order_release);
bGenerationPaused.store(true, std::memory_order_release);
StopFillThread();
Super::BeginDestroy();
}
@@ -87,6 +89,7 @@ void UVoxelDensityVolume::BeginDestroy()
void UVoxelDensityVolume::NotifyShutdown()
{
bShuttingDown.store(true, std::memory_order_release);
bGenerationPaused.store(true, std::memory_order_release);
// Stop the dedicated fill thread — Kill(true) blocks until Run() returns, so it can't read the
// Generator after this (the owner tears UObjects down next). Then drop any queued/finished work.
@@ -100,6 +103,10 @@ void UVoxelDensityVolume::NotifyShutdown()
void UVoxelDensityVolume::Reset()
{
// Reset can be called by a live-edit/regeneration path, not only after EndPlay. Join first so
// no fill thread can still read Generator while the old epoch and queued work are discarded.
StopFillThread();
// Bump the epoch so any in-flight fill lands stale and is dropped in DrainResults.
++VolumeEpoch;
PendingFills.Reset();
@@ -118,6 +125,20 @@ void UVoxelDensityVolume::Reset()
LastPlayerVoxel = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
}
void UVoxelDensityVolume::PauseForGenerationChange()
{
bGenerationPaused.store(true, std::memory_order_release);
StopFillThread();
}
void UVoxelDensityVolume::ResumeAfterGenerationChange()
{
if (!bShuttingDown.load(std::memory_order_acquire))
{
bGenerationPaused.store(false, std::memory_order_release);
}
}
//=============================================================================
// Helpers
//=============================================================================
@@ -284,7 +305,8 @@ bool UVoxelDensityVolume::GetLevelShaderParams(int32 Level, FIntVector& OutOrigi
void UVoxelDensityVolume::Update(const FVector& PlayerWorldPos)
{
if (!bInitialized || !Settings || !Settings->bEnableDensityVolume || !Generator) return;
if (bGenerationPaused.load(std::memory_order_acquire)
|| !bInitialized || !Settings || !Settings->bEnableDensityVolume || !Generator) return;
AActor* O = Owner.Get();
if (!O) return;
@@ -557,6 +579,8 @@ void UVoxelDensityVolume::IngestTileCapture(const FIntVector& L0TileCoord, TArra
void UVoxelDensityVolume::EnsureFillThread()
{
if (FillThread) return;
if (bGenerationPaused.load(std::memory_order_acquire)
|| bShuttingDown.load(std::memory_order_acquire)) return;
if (!Settings || !Settings->bEnableDensityVolume) return;
bFillThreadStop.store(false, std::memory_order_release);
if (!FillWakeEvent) { FillWakeEvent = FPlatformProcess::GetSynchEventFromPool(false); } // auto-reset
@@ -588,6 +612,8 @@ void UVoxelDensityVolume::StopFillThread()
void UVoxelDensityVolume::LaunchPendingFills()
{
if (PendingFills.Num() == 0) return;
if (bGenerationPaused.load(std::memory_order_acquire)
|| bShuttingDown.load(std::memory_order_acquire)) return;
EnsureFillThread();
for (FPendingFill& F : PendingFills)
{