diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index d4420d3..6cc5806 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -59,6 +59,9 @@ #include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0) static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings); +static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile, + const FVector& BoundsMin, const FVector& BoundsMax); +static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B); #if WITH_EDITOR #include "VoxelStrateComposer.h" @@ -132,6 +135,7 @@ namespace float GVoxelForgePendingModificationRadius = 3.0f; float GVoxelForgePendingModificationStrength = 10.0f; bool GVoxelForgeTestModificationLoadedProofTarget = false; + bool GVoxelForgeTestModificationLoadedGeometryTarget = false; bool GVoxelForgeTestModificationCommandLineConsumed = false; bool GVoxelForgeStreamingBudgetReported = false; bool GVoxelForgeGenerationCapHitReported = false; @@ -538,6 +542,11 @@ void AVoxelWorld::RegenerateAllChunks() TileComponents.Empty(); LoadedTiles.Empty(); LoadedTileGeometry.Reset(); + ModificationEpoch = 0; + LastModificationMinVoxel = FVector::ZeroVector; + LastModificationMaxVoxel = FVector::ZeroVector; + ModificationRevisions.Reset(); + AppliedModificationEpoch.Reset(); CollisionReadyTiles.Empty(); CollisionNotRequiredTiles.Empty(); CollisionSolidTiles.Empty(); @@ -1935,7 +1944,32 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest() HeadlessStrateCrossingTestFailureReason.Reset(); HeadlessStrateCrossingTestRoute.Reset(); + bVisualStalenessAudit = false; + VisualStalenessAuditMode = 0; + bVisualStalenessAuditExitLogged = false; + VisualAuditEditEpoch = 0; + VisualAuditEditMinVoxel = FVector::ZeroVector; + VisualAuditEditMaxVoxel = FVector::ZeroVector; + VisualAuditEditBeginSeconds = 0.0; + VisualAuditEditStaleDurationSeconds = -1.0; + VisualAuditEditMaxStaleTiles = 0; + bVisualAuditEditSawStale = false; + bVisualAuditEditResolved = false; + VisualAuditLODOverlapStartSeconds = 0.0; + VisualAuditLODOverlapMaxSeconds = 0.0; + VisualAuditLODOverlapEvents = 0; + VisualAuditLODOverlapMaxPairs = 0; + const TCHAR* CommandLine = FCommandLine::Get(); + int32 VisualAuditValue = 0; + if (FParse::Value(CommandLine, TEXT("voxel.TestVisualStaleness="), VisualAuditValue)) + { + VisualStalenessAuditMode = FMath::Clamp(VisualAuditValue, 0, 2); + bVisualStalenessAudit = VisualStalenessAuditMode != 0; + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeVisualStaleness] configured=1 mode=%d"), + VisualStalenessAuditMode); + } FString RequestedTileHashDumpPath; if (FParse::Value(CommandLine, TEXT("voxel.TileHashDump="), RequestedTileHashDumpPath)) { @@ -3513,6 +3547,7 @@ void AVoxelWorld::MaybeFinishHeadlessStreamingTest() } bHeadlessStreamingTestExitRequested = true; + LogVisualStalenessAudit(); UE_LOG(LogTemp, Display, TEXT("[VoxelForgeStreamingTest] complete; moves=%d distance_m=%.6f last_position=(%.1f,%.1f,%.1f); requesting graceful exit after clean session"), HeadlessStreamingTestMoveAttempts, @@ -3523,6 +3558,172 @@ void AVoxelWorld::MaybeFinishHeadlessStreamingTest() FPlatformMisc::RequestExit(false, TEXT("VoxelForge headless streaming test complete")); } +static bool VF_TileOverlapsVoxelBounds(const FVoxelTileKey& Tile, + const FVector& BoundsMin, const FVector& BoundsMax) +{ + const FIntVector Origin = Tile.OriginVoxels(); + const FVector TileMin( + static_cast(Origin.X), static_cast(Origin.Y), static_cast(Origin.Z)); + const FVector TileMax = TileMin + FVector(static_cast(Tile.ExtentVoxels())); + return TileMin.X < BoundsMax.X && TileMax.X > BoundsMin.X + && TileMin.Y < BoundsMax.Y && TileMax.Y > BoundsMin.Y + && TileMin.Z < BoundsMax.Z && TileMax.Z > BoundsMin.Z; +} + +static bool VF_TileFootprintsOverlap(const FVoxelTileKey& A, const FVoxelTileKey& B) +{ + const FIntVector AOrigin = A.OriginVoxels(); + const FIntVector BOrigin = B.OriginVoxels(); + const FVector AMin( + static_cast(AOrigin.X), static_cast(AOrigin.Y), static_cast(AOrigin.Z)); + const FVector BMin( + static_cast(BOrigin.X), static_cast(BOrigin.Y), static_cast(BOrigin.Z)); + const float AExtent = static_cast(A.ExtentVoxels()); + const float BExtent = static_cast(B.ExtentVoxels()); + return AMin.X < BMin.X + BExtent && BMin.X < AMin.X + AExtent + && AMin.Y < BMin.Y + BExtent && BMin.Y < AMin.Y + AExtent + && AMin.Z < BMin.Z + BExtent && BMin.Z < AMin.Z + AExtent; +} + +void AVoxelWorld::UpdateVisualStalenessAudit() +{ + if (!bVisualStalenessAudit) + { + return; + } + + const double Now = FPlatformTime::Seconds(); + if (VisualStalenessAuditMode == 1 && VisualAuditEditEpoch != 0 + && VisualAuditEditBeginSeconds > 0.0) + { + int32 StaleVisibleTiles = 0; + for (const TPair& Pair : TileComponents) + { + if (!Pair.Value || !Pair.Value->IsVisible() + || !VF_TileOverlapsVoxelBounds( + Pair.Key, VisualAuditEditMinVoxel, VisualAuditEditMaxVoxel)) + { + continue; + } + if (AppliedModificationEpoch.FindRef(Pair.Key) != VisualAuditEditEpoch) + { + ++StaleVisibleTiles; + } + } + + if (StaleVisibleTiles > 0) + { + bVisualAuditEditSawStale = true; + VisualAuditEditMaxStaleTiles = FMath::Max( + VisualAuditEditMaxStaleTiles, StaleVisibleTiles); + } + else if (!bVisualAuditEditResolved) + { + bVisualAuditEditResolved = true; + VisualAuditEditStaleDurationSeconds = FMath::Max( + 0.0, Now - VisualAuditEditBeginSeconds); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeVisualStaleness] edit_resolved=1 duration_s=%.6f stale_tiles_max=%d"), + VisualAuditEditStaleDurationSeconds, VisualAuditEditMaxStaleTiles); + } + } + + if (VisualStalenessAuditMode == 2 && bHeadlessStreamingTestMovement + && HeadlessStreamingTestBeginSeconds > 0.0 + && Now - HeadlessStreamingTestBeginSeconds + >= static_cast(HeadlessStreamingTestStartSeconds)) + { + TArray VisibleTiles; + VisibleTiles.Reserve(TileComponents.Num()); + for (const TPair& Pair : TileComponents) + { + if (Pair.Value && Pair.Value->IsVisible()) + { + VisibleTiles.Add(Pair.Key); + } + } + + int32 OverlapPairs = 0; + for (int32 OldIndex = 0; OldIndex < VisibleTiles.Num(); ++OldIndex) + { + const FVoxelTileKey& OldTile = VisibleTiles[OldIndex]; + if (IsDesired(OldTile)) + { + continue; + } + for (int32 NewIndex = 0; NewIndex < VisibleTiles.Num(); ++NewIndex) + { + const FVoxelTileKey& NewTile = VisibleTiles[NewIndex]; + if (OldTile.Level == NewTile.Level || !IsDesired(NewTile) + || !VF_TileFootprintsOverlap(OldTile, NewTile)) + { + continue; + } + ++OverlapPairs; + } + } + + if (OverlapPairs > 0) + { + if (VisualAuditLODOverlapStartSeconds <= 0.0) + { + VisualAuditLODOverlapStartSeconds = Now; + ++VisualAuditLODOverlapEvents; + } + VisualAuditLODOverlapMaxPairs = FMath::Max( + VisualAuditLODOverlapMaxPairs, OverlapPairs); + } + else if (VisualAuditLODOverlapStartSeconds > 0.0) + { + VisualAuditLODOverlapMaxSeconds = FMath::Max( + VisualAuditLODOverlapMaxSeconds, + FMath::Max(0.0, Now - VisualAuditLODOverlapStartSeconds)); + VisualAuditLODOverlapStartSeconds = 0.0; + } + } +} + +void AVoxelWorld::LogVisualStalenessAudit() +{ + if (!bVisualStalenessAudit || bVisualStalenessAuditExitLogged) + { + return; + } + bVisualStalenessAuditExitLogged = true; + + const double Now = FPlatformTime::Seconds(); + if (VisualAuditLODOverlapStartSeconds > 0.0) + { + VisualAuditLODOverlapMaxSeconds = FMath::Max( + VisualAuditLODOverlapMaxSeconds, + FMath::Max(0.0, Now - VisualAuditLODOverlapStartSeconds)); + VisualAuditLODOverlapStartSeconds = 0.0; + } + if (VisualAuditEditEpoch != 0 && VisualAuditEditBeginSeconds > 0.0 + && !bVisualAuditEditResolved) + { + VisualAuditEditStaleDurationSeconds = FMath::Max( + 0.0, Now - VisualAuditEditBeginSeconds); + } + + const bool bPassed = VisualStalenessAuditMode == 1 + ? (VisualAuditEditEpoch != 0 && bVisualAuditEditResolved) + : (VisualStalenessAuditMode == 2 && VisualAuditLODOverlapMaxSeconds <= 0.050); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeVisualStaleness] result=%s mode=%d " + "edit_applied=%d edit_resolved=%d edit_stale_duration_s=%.6f " + "edit_stale_tiles_max=%d lod_overlap_max_s=%.6f " + "lod_overlap_events=%d lod_overlap_pairs_max=%d"), + bPassed ? TEXT("PASS") : TEXT("FAIL"), VisualStalenessAuditMode, + VisualAuditEditEpoch != 0 ? 1 : 0, + bVisualAuditEditResolved ? 1 : 0, + VisualAuditEditStaleDurationSeconds, + VisualAuditEditMaxStaleTiles, + VisualAuditLODOverlapMaxSeconds, + VisualAuditLODOverlapEvents, + VisualAuditLODOverlapMaxPairs); +} + void AVoxelWorld::Tick(float DeltaTime) { Super::Tick(DeltaTime); @@ -3612,15 +3813,22 @@ void AVoxelWorld::Tick(float DeltaTime) { GVoxelForgeTestModificationLoadedProofTarget = true; } + else if (ModificationTarget.Equals(TEXT("loaded-geometry"), ESearchCase::IgnoreCase)) + { + GVoxelForgeTestModificationLoadedGeometryTarget = true; + } UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestModification] queued radius=%.2f strength=%.2f target=%s"), GVoxelForgePendingModificationRadius, GVoxelForgePendingModificationStrength, - GVoxelForgeTestModificationLoadedProofTarget ? TEXT("loaded-proof") : TEXT("player")); + GVoxelForgeTestModificationLoadedProofTarget ? TEXT("loaded-proof") + : (GVoxelForgeTestModificationLoadedGeometryTarget + ? TEXT("loaded-geometry") : TEXT("player"))); GVoxelForgePendingTestModification = 1; } } ProcessPendingChunks(); + ReconcileReadyTransitionVisibility(); // Near-field evidence is independent of the full horizon settle point. Record one completion // for each desired-set epoch after this frame's result drain, including moving crossings. RecordLOD0RingReadyIfComplete(); @@ -3733,7 +3941,8 @@ void AVoxelWorld::Tick(float DeltaTime) WorldToChunkCoord(WorldToLocalCm(ModificationPosition)), 0); bool bModificationTargetSelected = false; if (GVoxelForgePendingTestModification != 0 - && GVoxelForgeTestModificationLoadedProofTarget + && (GVoxelForgeTestModificationLoadedProofTarget + || GVoxelForgeTestModificationLoadedGeometryTarget) && Generator) { // The diagnostic target is deliberately selected from the applied set. That makes the @@ -3746,11 +3955,34 @@ void AVoxelWorld::Tick(float DeltaTime) { continue; } + if (GVoxelForgeTestModificationLoadedGeometryTarget + && !LoadedTileGeometry.Contains(CandidateTile)) + { + continue; + } + if (GVoxelForgeTestModificationLoadedGeometryTarget) + { + bool bHasVisibleCoarseOverlay = false; + for (const TPair& Pair : TileComponents) + { + if (Pair.Key.Level > 0 && Pair.Value && Pair.Value->IsVisible() + && VF_TileFootprintsOverlap(CandidateTile, Pair.Key)) + { + bHasVisibleCoarseOverlay = true; + break; + } + } + if (!bHasVisibleCoarseOverlay) + { + continue; + } + } const FIntVector CandidateOrigin( CandidateTile.Coord.X * CHUNK_SIZE, CandidateTile.Coord.Y * CHUNK_SIZE, CandidateTile.Coord.Z * CHUNK_SIZE); - if (!Generator->TryProveSealedSolidTile(CandidateOrigin, 1, CHUNK_SIZE)) + if (GVoxelForgeTestModificationLoadedProofTarget + && !Generator->TryProveSealedSolidTile(CandidateOrigin, 1, CHUNK_SIZE)) { continue; } @@ -3764,15 +3996,18 @@ void AVoxelWorld::Tick(float DeltaTime) bModificationTargetSelected = true; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTestModification] player_mine_target tile=(%d,%d,%d) " - "local_center=(%d,%d,%d) before_proof=1"), + "local_center=(%d,%d,%d) before_proof=%d"), CandidateTile.Coord.X, CandidateTile.Coord.Y, CandidateTile.Coord.Z, - CandidateCenter.X, CandidateCenter.Y, CandidateCenter.Z); + CandidateCenter.X, CandidateCenter.Y, CandidateCenter.Z, + GVoxelForgeTestModificationLoadedProofTarget ? 1 : 0); break; } } if (GVoxelForgePendingTestModification != 0 && DiffLayer - && (!GVoxelForgeTestModificationLoadedProofTarget || bModificationTargetSelected) + && ((!GVoxelForgeTestModificationLoadedProofTarget + && !GVoxelForgeTestModificationLoadedGeometryTarget) + || bModificationTargetSelected) && LoadedTiles.Contains(ModificationCenterTile) && !PendingTiles.Contains(ModificationCenterTile)) { @@ -3800,6 +4035,7 @@ void AVoxelWorld::Tick(float DeltaTime) ModificationCenterTile.Coord.Z, bProofSurvivedCarve ? 1 : 0); } ProcessUnloadQueue(); + UpdateVisualStalenessAudit(); MaybeFinishHeadlessStreamingTest(); if (bTestCeilingViewEnabled) @@ -5007,6 +5243,31 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result) return false; } + // A worker may have captured the density field before one or more edits. Do not let that + // result overwrite the current visual/collision state when any later revision touches this + // tile's footprint. Keep the old state in place and admit one post-edit request through the + // normal budgeted edited queue; this is the edit equivalent of load-before-unload. + if (Result.ModificationEpoch < ModificationEpoch) + { + bool bAffectedByLaterEdit = false; + for (const FModificationRevision& Revision : ModificationRevisions) + { + if (Revision.Epoch > Result.ModificationEpoch + && VF_TileOverlapsVoxelBounds( + Result.Tile, Revision.MinVoxel, Revision.MaxVoxel)) + { + bAffectedByLaterEdit = true; + break; + } + } + if (bAffectedByLaterEdit) + { + DirtyRemeshQueue.Add(Result.Tile); + bAllChunksLoaded = false; + return false; + } + } + // Applied is emitted after the game-thread submission, not when the worker result is dequeued. // It deliberately does NOT mean collision-ready: RMC's cook future records that later boundary // in HandleTileCollisionCookComplete. @@ -5143,6 +5404,7 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result) FPlatformTime::Cycles64() - EmptyReleaseStartCycles)); } LoadedTiles.Add(Result.Tile); + AppliedModificationEpoch.Add(Result.Tile, Result.ModificationEpoch); LoadedTileGeometry.Remove(Result.Tile); LogTileApplied(); if (Result.Tile.Level == 0 && Result.bRecordStreamingLatency) @@ -5172,6 +5434,7 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result) return false; } LoadedTiles.Add(Result.Tile); + AppliedModificationEpoch.Add(Result.Tile, Result.ModificationEpoch); LoadedTileGeometry.Add(Result.Tile, FLoadedTileGeometryStats{ static_cast(FMath::Max(0, Result.NumVertices)), static_cast(FMath::Max(0, Result.NumTriangles)), @@ -5209,6 +5472,7 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile) FChunkResult Result; Result.DesiredEpoch = DesiredEpoch; + Result.ModificationEpoch = ModificationEpoch; Result.bRecordStreamingLatency = false; Result.RequestStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0; @@ -5261,6 +5525,10 @@ void AVoxelWorld::ProcessUnloadQueue() { // Re-desired before its turn (player reversed) — keep it; it's still loaded, just drop // it from the queue. Doesn't count against the destroy budget. + if (URealtimeMeshComponent* Comp = TileComponents.FindRef(T)) + { + Comp->SetVisibility(!CollisionOnlyTiles.Contains(T)); + } Dequeued.Add(T); continue; } @@ -5271,6 +5539,132 @@ void AVoxelWorld::ProcessUnloadQueue() for (const FVoxelTileKey& T : Dequeued) PendingUnload.Remove(T); } +void AVoxelWorld::QueuePendingUnload(const FVoxelTileKey& Tile) +{ + if (IsDesired(Tile)) + { + return; + } + + HideOverlappingDesiredVisuals(Tile); + PendingUnload.Add(Tile); + TransitionHold.Remove(Tile); + // Teardown remains budgeted in ProcessUnloadQueue. Hiding here is the cheap, same-frame visual + // handoff; collision is deliberately left untouched until UnloadTile drains the queue. + if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Tile)) + { + Comp->SetVisibility(false); + } +} + +void AVoxelWorld::HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile) +{ + for (const TPair& Pair : TileComponents) + { + if (Pair.Key == RetiredTile || !Pair.Value || !IsDesired(Pair.Key) + || CollisionOnlyTiles.Contains(Pair.Key)) + { + continue; + } + if (VF_TileFootprintsOverlap(RetiredTile, Pair.Key)) + { + Pair.Value->SetVisibility(false); + } + } +} + +void AVoxelWorld::ReconcileReadyTransitionVisibility() +{ + if (bAllChunksLoaded && TransitionHold.Num() == 0 && PendingUnload.Num() == 0) + { + return; + } + if (DesiredSorted.Num() == 0 || TileComponents.Num() == 0) + { + return; + } + + // This is a visibility-only pass. It runs while streaming is unsettled and after result + // application, so an old component is hidden in the same game-thread turn in which the full + // desired covering set becomes loaded. Destruction and collision removal stay in the budgeted + // queue below. + TArray ReadyToHide; + ReadyToHide.Reserve(TileComponents.Num()); + for (const TPair& Pair : TileComponents) + { + const FVoxelTileKey& OldTile = Pair.Key; + if (!Pair.Value || IsDesired(OldTile) || PendingUnload.Contains(OldTile)) + { + continue; + } + + if (!IsTileInClipRange(OldTile, CurrentCenterChunk)) + { + ReadyToHide.Add(OldTile); + continue; + } + + bool bHasReplacement = false; + bool bAllReplacementsLoaded = true; + for (const FVoxelTileKey& NewTile : DesiredSorted) + { + if (!VF_TileFootprintsOverlap(OldTile, NewTile)) + { + continue; + } + bHasReplacement = true; + if (!LoadedTiles.Contains(NewTile)) + { + bAllReplacementsLoaded = false; + break; + } + } + if (bHasReplacement && bAllReplacementsLoaded) + { + ReadyToHide.Add(OldTile); + } + } + + for (const FVoxelTileKey& Tile : ReadyToHide) + { + QueuePendingUnload(Tile); + } + + // Replacement components may have been uploaded hidden while a retired tile still covered + // only part of the transition. Reveal them only after every overlapping retired component has + // entered PendingUnload (and was therefore hidden above). Collision visibility is independent: + // the component remains queryable until the budgeted UnloadTile call. + for (const TPair& NewPair : TileComponents) + { + const FVoxelTileKey& NewTile = NewPair.Key; + URealtimeMeshComponent* NewComp = NewPair.Value; + if (!NewComp || !IsDesired(NewTile) || CollisionOnlyTiles.Contains(NewTile)) + { + continue; + } + + bool bBlockedByVisibleRetiredTile = false; + for (const TPair& OldPair : TileComponents) + { + const FVoxelTileKey& OldTile = OldPair.Key; + if (IsDesired(OldTile) || PendingUnload.Contains(OldTile) + || !OldPair.Value || !OldPair.Value->IsVisible()) + { + continue; + } + if (VF_TileFootprintsOverlap(NewTile, OldTile)) + { + bBlockedByVisibleRetiredTile = true; + break; + } + } + if (!bBlockedByVisibleRetiredTile) + { + NewComp->SetVisibility(true); + } + } +} + // Integer floor-division (correct for negatives), scalar + vector. static FORCEINLINE int32 VF_FloorDiv(int32 V, int32 D) { @@ -5938,7 +6332,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw } if (!IsTileInClipRange(T, CenterChunk)) // left the view → cull now { - PendingUnload.Add(T); + QueuePendingUnload(T); return false; } // In-range transition. Quand le backlog est gros (sprint), sauter le test de @@ -5946,9 +6340,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw EnsurePending(); if (DesiredPending.Num() <= 48 && ReplacementsReady(T)) { - PendingUnload.Add(T); + QueuePendingUnload(T); return false; } + HideOverlappingDesiredVisuals(T); return true; }; @@ -6015,7 +6410,6 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw { if (PendingTiles.Num() >= MaxTasks) break; const FVoxelTileKey T = *It; - if (!LoadedTiles.Contains(T)) { It.RemoveCurrent(); continue; } // unloaded — drop if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands It.RemoveCurrent(); @@ -6095,9 +6489,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // deferred (in-range, not-desired) transition tiles now. No holes. Full scan — // rare (once per settle), and the safety net behind the delta cull above. for (const auto& Pair : TileComponents) - if (!IsDesired(Pair.Key)) PendingUnload.Add(Pair.Key); + if (!IsDesired(Pair.Key)) QueuePendingUnload(Pair.Key); for (const FVoxelTileKey& T : LoadedTiles) - if (!IsDesired(T) && !TileComponents.Contains(T)) PendingUnload.Add(T); + if (!IsDesired(T) && !TileComponents.Contains(T)) QueuePendingUnload(T); // Teardown is drained by ProcessUnloadQueue (budgeted) — single spike-free path. bAllChunksLoaded = true; @@ -6156,6 +6550,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor const int32 Step = FMath::Max(1, Extent / Cells); const uint32 TaskEpoch = GenerationEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch; + const uint32 TaskModificationEpoch = ModificationEpoch; // CAPTURE-DURING-MESHING: only level-0 full-res tiles map 1:1 onto a density-clipmap level // (Step == 1<