fix(streaming): no stale geometry over its replacement -- edit ghost 40s -> 0.17s

The owner reported a wall staying visible after he carved through it, and overlapping LODs when crossing slopes at speed. Two causes: an edit only re-meshed the level-0 tiles of the touched chunks, so coarser tiles kept pre-edit geometry until something else re-queued them; and superseded tiles stayed drawn until the budgeted teardown reached them.

Now: edits fence late pre-edit async results and re-mesh every affected loaded/pending level; a replaced tile is hidden as soon as its replacement is fully covered, while destruction and collision removal stay budgeted (the anti-spike rule that made teardown budgeted in the first place). Load-before-unload is unchanged, so no holes.

Measured: edit ghost 40.095 s (unresolved at test exit) -> 0.172 s; 6000 cm/s slope overlap 19.952 s with 153 overlapping pairs -> 0.000 s with zero events. Gate stress PASS (113.68 m, 2 holds, 0 full stops). Apply-frame cost peaked at 0.979 ms against a 2 ms budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-23 21:10:20 +02:00
1 parent f36bc1f9b2
commit 7486c299d4
3 files changed
+615 -38

No files matched your search

+44 -6
View File
@@ -129,6 +129,7 @@ struct FChunkResult
bool bAborted = false; // Worker observed shutdown or cancellation; never mark this tile loaded
bool bObsolete = false; // Worker observed per-tile desired-set cancellation
uint32 DesiredEpoch = 0; // Desired-set epoch captured at request time
uint32 ModificationEpoch = 0; // Diff-layer revision captured at request time
bool bRecordStreamingLatency = true;
bool bEmpty = true; // true ⇒ no surface (uniform all-air/all-solid or empty band); still loaded
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
@@ -557,6 +558,8 @@ private:
void MaybeFinishHeadlessSurfaceFallTest();
void MaybeFinishHeadlessCollisionGateStressTest();
void MaybeFinishHeadlessStreamingTest();
void UpdateVisualStalenessAudit();
void LogVisualStalenessAudit();
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
bool bSheetTile, const FVoxelMeshData& MeshData);
@@ -918,12 +921,11 @@ public:
int32 MeshBandChunkHi = MAX_int32;
TSet<FVoxelTileKey> BandRemeshQueue;
// DIG RESPONSIVENESS — loaded level-0 tiles touched by a player carve/fill that still need an
// async re-mesh (the neighbours of the synchronously-remeshed centre tile, plus any tile that was
// mid-gen at carve time). Drained FIRST in the submit loop (ahead of streaming + band) and launched
// at BackgroundHigh so a dig never waits behind streaming. Unlike the old inline RemeshDirtyChunks,
// a tile that's in flight is KEPT queued (not dropped) so the stale pre-carve result is corrected
// once it lands — the source of "the hole shows up a beat late, or not until I move".
// DIG RESPONSIVENESS — tiles at every LOD touched by a player carve/fill that still need an
// async re-mesh (the synchronously-remeshed centre tile is excluded). Drained FIRST in the submit
// loop (ahead of streaming + band) and launched at BackgroundHigh so a dig never waits behind
// streaming. A tile that's in flight is marked obsolete and KEPT queued so a stale pre-carve
// result cannot publish and the post-carve result is admitted immediately.
TSet<FVoxelTileKey> DirtyRemeshQueue;
// F18 — TROU XY de l'anneau feuille (voxels ; Max EXCLUSIF ; sentinelles MAX/MIN = pas de
@@ -1016,6 +1018,21 @@ public:
uint64 AppliedBandTileCountByLevel[TrackedClassifierLODCount]{};
TMap<FVoxelTileKey, FLoadedTileGeometryStats> LoadedTileGeometry;
// Diff revision captured by tile requests. A result from before an edit is never allowed to
// replace a tile whose footprint intersects that edit; the old visual/collision state remains
// until a post-edit result is applied.
struct FModificationRevision
{
uint32 Epoch = 0;
FVector MinVoxel = FVector::ZeroVector;
FVector MaxVoxel = FVector::ZeroVector;
};
uint32 ModificationEpoch = 0;
FVector LastModificationMinVoxel = FVector::ZeroVector;
FVector LastModificationMaxVoxel = FVector::ZeroVector;
TArray<FModificationRevision> ModificationRevisions;
TMap<FVoxelTileKey, uint32> AppliedModificationEpoch;
struct FStreamingLatencySample
{
double RequestToReadySeconds = 0.0;
@@ -1154,6 +1171,24 @@ public:
FString HeadlessStrateCrossingTestFailureReason;
TArray<FVector> HeadlessStrateCrossingTestRoute;
// Explicit visual-staleness evidence. Mode 1 audits an edit; mode 2 audits an LOD transition.
// These fields are command-line-only diagnostics and are inert in ordinary gameplay.
bool bVisualStalenessAudit = false;
int32 VisualStalenessAuditMode = 0;
bool bVisualStalenessAuditExitLogged = false;
uint32 VisualAuditEditEpoch = 0;
FVector VisualAuditEditMinVoxel = FVector::ZeroVector;
FVector VisualAuditEditMaxVoxel = FVector::ZeroVector;
double VisualAuditEditBeginSeconds = 0.0;
double VisualAuditEditStaleDurationSeconds = -1.0;
int32 VisualAuditEditMaxStaleTiles = 0;
bool bVisualAuditEditSawStale = false;
bool bVisualAuditEditResolved = false;
double VisualAuditLODOverlapStartSeconds = 0.0;
double VisualAuditLODOverlapMaxSeconds = 0.0;
int32 VisualAuditLODOverlapEvents = 0;
int32 VisualAuditLODOverlapMaxPairs = 0;
// PLAYER COLLISION GATE — only the downward component is held while the level-0 collision body
// beneath the pawn's feet is unresolved. Horizontal movement and player control remain live.
TWeakObjectPtr<APawn> PawnTickPrerequisite;
@@ -1208,6 +1243,9 @@ public:
void ProcessPendingChunks();
void ProcessUnloadQueue(); // budgeted teardown drain (see PendingUnload)
void QueuePendingUnload(const FVoxelTileKey& Tile);
void HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile);
void ReconcileReadyTransitionVisibility();
/**
* Re-queue loaded chunks for async re-generation + re-meshing.