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:
1 parent
f36bc1f9b2
commit
7486c299d4
3 files changed
+615
-38
No files matched your search
@@ -59,6 +59,9 @@
|
|||||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||||
|
|
||||||
static int32 VF_MaxMarchingCubesLevel(const UVoxelSettings* Settings);
|
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
|
#if WITH_EDITOR
|
||||||
#include "VoxelStrateComposer.h"
|
#include "VoxelStrateComposer.h"
|
||||||
@@ -132,6 +135,7 @@ namespace
|
|||||||
float GVoxelForgePendingModificationRadius = 3.0f;
|
float GVoxelForgePendingModificationRadius = 3.0f;
|
||||||
float GVoxelForgePendingModificationStrength = 10.0f;
|
float GVoxelForgePendingModificationStrength = 10.0f;
|
||||||
bool GVoxelForgeTestModificationLoadedProofTarget = false;
|
bool GVoxelForgeTestModificationLoadedProofTarget = false;
|
||||||
|
bool GVoxelForgeTestModificationLoadedGeometryTarget = false;
|
||||||
bool GVoxelForgeTestModificationCommandLineConsumed = false;
|
bool GVoxelForgeTestModificationCommandLineConsumed = false;
|
||||||
bool GVoxelForgeStreamingBudgetReported = false;
|
bool GVoxelForgeStreamingBudgetReported = false;
|
||||||
bool GVoxelForgeGenerationCapHitReported = false;
|
bool GVoxelForgeGenerationCapHitReported = false;
|
||||||
@@ -538,6 +542,11 @@ void AVoxelWorld::RegenerateAllChunks()
|
|||||||
TileComponents.Empty();
|
TileComponents.Empty();
|
||||||
LoadedTiles.Empty();
|
LoadedTiles.Empty();
|
||||||
LoadedTileGeometry.Reset();
|
LoadedTileGeometry.Reset();
|
||||||
|
ModificationEpoch = 0;
|
||||||
|
LastModificationMinVoxel = FVector::ZeroVector;
|
||||||
|
LastModificationMaxVoxel = FVector::ZeroVector;
|
||||||
|
ModificationRevisions.Reset();
|
||||||
|
AppliedModificationEpoch.Reset();
|
||||||
CollisionReadyTiles.Empty();
|
CollisionReadyTiles.Empty();
|
||||||
CollisionNotRequiredTiles.Empty();
|
CollisionNotRequiredTiles.Empty();
|
||||||
CollisionSolidTiles.Empty();
|
CollisionSolidTiles.Empty();
|
||||||
@@ -1935,7 +1944,32 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
|||||||
HeadlessStrateCrossingTestFailureReason.Reset();
|
HeadlessStrateCrossingTestFailureReason.Reset();
|
||||||
HeadlessStrateCrossingTestRoute.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();
|
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;
|
FString RequestedTileHashDumpPath;
|
||||||
if (FParse::Value(CommandLine, TEXT("voxel.TileHashDump="), RequestedTileHashDumpPath))
|
if (FParse::Value(CommandLine, TEXT("voxel.TileHashDump="), RequestedTileHashDumpPath))
|
||||||
{
|
{
|
||||||
@@ -3513,6 +3547,7 @@ void AVoxelWorld::MaybeFinishHeadlessStreamingTest()
|
|||||||
}
|
}
|
||||||
|
|
||||||
bHeadlessStreamingTestExitRequested = true;
|
bHeadlessStreamingTestExitRequested = true;
|
||||||
|
LogVisualStalenessAudit();
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeStreamingTest] complete; moves=%d distance_m=%.6f last_position=(%.1f,%.1f,%.1f); requesting graceful exit after clean session"),
|
TEXT("[VoxelForgeStreamingTest] complete; moves=%d distance_m=%.6f last_position=(%.1f,%.1f,%.1f); requesting graceful exit after clean session"),
|
||||||
HeadlessStreamingTestMoveAttempts,
|
HeadlessStreamingTestMoveAttempts,
|
||||||
@@ -3523,6 +3558,172 @@ void AVoxelWorld::MaybeFinishHeadlessStreamingTest()
|
|||||||
FPlatformMisc::RequestExit(false, TEXT("VoxelForge headless streaming test complete"));
|
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<float>(Origin.X), static_cast<float>(Origin.Y), static_cast<float>(Origin.Z));
|
||||||
|
const FVector TileMax = TileMin + FVector(static_cast<float>(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<float>(AOrigin.X), static_cast<float>(AOrigin.Y), static_cast<float>(AOrigin.Z));
|
||||||
|
const FVector BMin(
|
||||||
|
static_cast<float>(BOrigin.X), static_cast<float>(BOrigin.Y), static_cast<float>(BOrigin.Z));
|
||||||
|
const float AExtent = static_cast<float>(A.ExtentVoxels());
|
||||||
|
const float BExtent = static_cast<float>(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<FVoxelTileKey, URealtimeMeshComponent*>& 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<double>(HeadlessStreamingTestStartSeconds))
|
||||||
|
{
|
||||||
|
TArray<FVoxelTileKey> VisibleTiles;
|
||||||
|
VisibleTiles.Reserve(TileComponents.Num());
|
||||||
|
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& 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)
|
void AVoxelWorld::Tick(float DeltaTime)
|
||||||
{
|
{
|
||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
@@ -3612,15 +3813,22 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
{
|
{
|
||||||
GVoxelForgeTestModificationLoadedProofTarget = true;
|
GVoxelForgeTestModificationLoadedProofTarget = true;
|
||||||
}
|
}
|
||||||
|
else if (ModificationTarget.Equals(TEXT("loaded-geometry"), ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
GVoxelForgeTestModificationLoadedGeometryTarget = true;
|
||||||
|
}
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeTestModification] queued radius=%.2f strength=%.2f target=%s"),
|
TEXT("[VoxelForgeTestModification] queued radius=%.2f strength=%.2f target=%s"),
|
||||||
GVoxelForgePendingModificationRadius,
|
GVoxelForgePendingModificationRadius,
|
||||||
GVoxelForgePendingModificationStrength,
|
GVoxelForgePendingModificationStrength,
|
||||||
GVoxelForgeTestModificationLoadedProofTarget ? TEXT("loaded-proof") : TEXT("player"));
|
GVoxelForgeTestModificationLoadedProofTarget ? TEXT("loaded-proof")
|
||||||
|
: (GVoxelForgeTestModificationLoadedGeometryTarget
|
||||||
|
? TEXT("loaded-geometry") : TEXT("player")));
|
||||||
GVoxelForgePendingTestModification = 1;
|
GVoxelForgePendingTestModification = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProcessPendingChunks();
|
ProcessPendingChunks();
|
||||||
|
ReconcileReadyTransitionVisibility();
|
||||||
// Near-field evidence is independent of the full horizon settle point. Record one completion
|
// 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.
|
// for each desired-set epoch after this frame's result drain, including moving crossings.
|
||||||
RecordLOD0RingReadyIfComplete();
|
RecordLOD0RingReadyIfComplete();
|
||||||
@@ -3733,7 +3941,8 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
WorldToChunkCoord(WorldToLocalCm(ModificationPosition)), 0);
|
WorldToChunkCoord(WorldToLocalCm(ModificationPosition)), 0);
|
||||||
bool bModificationTargetSelected = false;
|
bool bModificationTargetSelected = false;
|
||||||
if (GVoxelForgePendingTestModification != 0
|
if (GVoxelForgePendingTestModification != 0
|
||||||
&& GVoxelForgeTestModificationLoadedProofTarget
|
&& (GVoxelForgeTestModificationLoadedProofTarget
|
||||||
|
|| GVoxelForgeTestModificationLoadedGeometryTarget)
|
||||||
&& Generator)
|
&& Generator)
|
||||||
{
|
{
|
||||||
// The diagnostic target is deliberately selected from the applied set. That makes the
|
// The diagnostic target is deliberately selected from the applied set. That makes the
|
||||||
@@ -3746,11 +3955,34 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (GVoxelForgeTestModificationLoadedGeometryTarget
|
||||||
|
&& !LoadedTileGeometry.Contains(CandidateTile))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (GVoxelForgeTestModificationLoadedGeometryTarget)
|
||||||
|
{
|
||||||
|
bool bHasVisibleCoarseOverlay = false;
|
||||||
|
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& 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(
|
const FIntVector CandidateOrigin(
|
||||||
CandidateTile.Coord.X * CHUNK_SIZE,
|
CandidateTile.Coord.X * CHUNK_SIZE,
|
||||||
CandidateTile.Coord.Y * CHUNK_SIZE,
|
CandidateTile.Coord.Y * CHUNK_SIZE,
|
||||||
CandidateTile.Coord.Z * CHUNK_SIZE);
|
CandidateTile.Coord.Z * CHUNK_SIZE);
|
||||||
if (!Generator->TryProveSealedSolidTile(CandidateOrigin, 1, CHUNK_SIZE))
|
if (GVoxelForgeTestModificationLoadedProofTarget
|
||||||
|
&& !Generator->TryProveSealedSolidTile(CandidateOrigin, 1, CHUNK_SIZE))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -3764,15 +3996,18 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
bModificationTargetSelected = true;
|
bModificationTargetSelected = true;
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeTestModification] player_mine_target tile=(%d,%d,%d) "
|
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,
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (GVoxelForgePendingTestModification != 0
|
if (GVoxelForgePendingTestModification != 0
|
||||||
&& DiffLayer
|
&& DiffLayer
|
||||||
&& (!GVoxelForgeTestModificationLoadedProofTarget || bModificationTargetSelected)
|
&& ((!GVoxelForgeTestModificationLoadedProofTarget
|
||||||
|
&& !GVoxelForgeTestModificationLoadedGeometryTarget)
|
||||||
|
|| bModificationTargetSelected)
|
||||||
&& LoadedTiles.Contains(ModificationCenterTile)
|
&& LoadedTiles.Contains(ModificationCenterTile)
|
||||||
&& !PendingTiles.Contains(ModificationCenterTile))
|
&& !PendingTiles.Contains(ModificationCenterTile))
|
||||||
{
|
{
|
||||||
@@ -3800,6 +4035,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
ModificationCenterTile.Coord.Z, bProofSurvivedCarve ? 1 : 0);
|
ModificationCenterTile.Coord.Z, bProofSurvivedCarve ? 1 : 0);
|
||||||
}
|
}
|
||||||
ProcessUnloadQueue();
|
ProcessUnloadQueue();
|
||||||
|
UpdateVisualStalenessAudit();
|
||||||
MaybeFinishHeadlessStreamingTest();
|
MaybeFinishHeadlessStreamingTest();
|
||||||
|
|
||||||
if (bTestCeilingViewEnabled)
|
if (bTestCeilingViewEnabled)
|
||||||
@@ -5007,6 +5243,31 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
return false;
|
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.
|
// 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
|
// It deliberately does NOT mean collision-ready: RMC's cook future records that later boundary
|
||||||
// in HandleTileCollisionCookComplete.
|
// in HandleTileCollisionCookComplete.
|
||||||
@@ -5143,6 +5404,7 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
FPlatformTime::Cycles64() - EmptyReleaseStartCycles));
|
FPlatformTime::Cycles64() - EmptyReleaseStartCycles));
|
||||||
}
|
}
|
||||||
LoadedTiles.Add(Result.Tile);
|
LoadedTiles.Add(Result.Tile);
|
||||||
|
AppliedModificationEpoch.Add(Result.Tile, Result.ModificationEpoch);
|
||||||
LoadedTileGeometry.Remove(Result.Tile);
|
LoadedTileGeometry.Remove(Result.Tile);
|
||||||
LogTileApplied();
|
LogTileApplied();
|
||||||
if (Result.Tile.Level == 0 && Result.bRecordStreamingLatency)
|
if (Result.Tile.Level == 0 && Result.bRecordStreamingLatency)
|
||||||
@@ -5172,6 +5434,7 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
LoadedTiles.Add(Result.Tile);
|
LoadedTiles.Add(Result.Tile);
|
||||||
|
AppliedModificationEpoch.Add(Result.Tile, Result.ModificationEpoch);
|
||||||
LoadedTileGeometry.Add(Result.Tile, FLoadedTileGeometryStats{
|
LoadedTileGeometry.Add(Result.Tile, FLoadedTileGeometryStats{
|
||||||
static_cast<uint64>(FMath::Max(0, Result.NumVertices)),
|
static_cast<uint64>(FMath::Max(0, Result.NumVertices)),
|
||||||
static_cast<uint64>(FMath::Max(0, Result.NumTriangles)),
|
static_cast<uint64>(FMath::Max(0, Result.NumTriangles)),
|
||||||
@@ -5209,6 +5472,7 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
|||||||
|
|
||||||
FChunkResult Result;
|
FChunkResult Result;
|
||||||
Result.DesiredEpoch = DesiredEpoch;
|
Result.DesiredEpoch = DesiredEpoch;
|
||||||
|
Result.ModificationEpoch = ModificationEpoch;
|
||||||
Result.bRecordStreamingLatency = false;
|
Result.bRecordStreamingLatency = false;
|
||||||
Result.RequestStartCycles = bProfileModification
|
Result.RequestStartCycles = bProfileModification
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? 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
|
// 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.
|
// 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);
|
Dequeued.Add(T);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -5271,6 +5539,132 @@ void AVoxelWorld::ProcessUnloadQueue()
|
|||||||
for (const FVoxelTileKey& T : Dequeued) PendingUnload.Remove(T);
|
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<FVoxelTileKey, URealtimeMeshComponent*>& 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<FVoxelTileKey> ReadyToHide;
|
||||||
|
ReadyToHide.Reserve(TileComponents.Num());
|
||||||
|
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& 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<FVoxelTileKey, URealtimeMeshComponent*>& 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<FVoxelTileKey, URealtimeMeshComponent*>& 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.
|
// Integer floor-division (correct for negatives), scalar + vector.
|
||||||
static FORCEINLINE int32 VF_FloorDiv(int32 V, int32 D)
|
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
|
if (!IsTileInClipRange(T, CenterChunk)) // left the view → cull now
|
||||||
{
|
{
|
||||||
PendingUnload.Add(T);
|
QueuePendingUnload(T);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// In-range transition. Quand le backlog est gros (sprint), sauter le test de
|
// 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();
|
EnsurePending();
|
||||||
if (DesiredPending.Num() <= 48 && ReplacementsReady(T))
|
if (DesiredPending.Num() <= 48 && ReplacementsReady(T))
|
||||||
{
|
{
|
||||||
PendingUnload.Add(T);
|
QueuePendingUnload(T);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
HideOverlappingDesiredVisuals(T);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -6015,7 +6410,6 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
{
|
{
|
||||||
if (PendingTiles.Num() >= MaxTasks) break;
|
if (PendingTiles.Num() >= MaxTasks) break;
|
||||||
const FVoxelTileKey T = *It;
|
const FVoxelTileKey T = *It;
|
||||||
if (!LoadedTiles.Contains(T)) { It.RemoveCurrent(); continue; } // unloaded — drop
|
|
||||||
if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop
|
if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop
|
||||||
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
|
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
|
||||||
It.RemoveCurrent();
|
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 —
|
// 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.
|
// rare (once per settle), and the safety net behind the delta cull above.
|
||||||
for (const auto& Pair : TileComponents)
|
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)
|
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.
|
// Teardown is drained by ProcessUnloadQueue (budgeted) — single spike-free path.
|
||||||
|
|
||||||
bAllChunksLoaded = true;
|
bAllChunksLoaded = true;
|
||||||
@@ -6156,6 +6550,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
const int32 Step = FMath::Max(1, Extent / Cells);
|
const int32 Step = FMath::Max(1, Extent / Cells);
|
||||||
const uint32 TaskEpoch = GenerationEpoch;
|
const uint32 TaskEpoch = GenerationEpoch;
|
||||||
const uint32 TaskDesiredEpoch = DesiredEpoch;
|
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
|
// CAPTURE-DURING-MESHING: only level-0 full-res tiles map 1:1 onto a density-clipmap level
|
||||||
// (Step == 1<<Level, Cells == CHUNK_SIZE). When the density volume is active, ask the mesher to
|
// (Step == 1<<Level, Cells == CHUNK_SIZE). When the density volume is active, ask the mesher to
|
||||||
@@ -6184,6 +6579,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
Empty.Tile = Tile;
|
Empty.Tile = Tile;
|
||||||
Empty.Epoch = TaskEpoch;
|
Empty.Epoch = TaskEpoch;
|
||||||
Empty.DesiredEpoch = TaskDesiredEpoch;
|
Empty.DesiredEpoch = TaskDesiredEpoch;
|
||||||
|
Empty.ModificationEpoch = TaskModificationEpoch;
|
||||||
Empty.RequestStartCycles = RequestStartCycles;
|
Empty.RequestStartCycles = RequestStartCycles;
|
||||||
Empty.WorkPriority = WorkPriority;
|
Empty.WorkPriority = WorkPriority;
|
||||||
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
||||||
@@ -6223,6 +6619,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
Empty.Tile = Tile;
|
Empty.Tile = Tile;
|
||||||
Empty.Epoch = TaskEpoch;
|
Empty.Epoch = TaskEpoch;
|
||||||
Empty.DesiredEpoch = TaskDesiredEpoch;
|
Empty.DesiredEpoch = TaskDesiredEpoch;
|
||||||
|
Empty.ModificationEpoch = TaskModificationEpoch;
|
||||||
Empty.RequestStartCycles = RequestStartCycles;
|
Empty.RequestStartCycles = RequestStartCycles;
|
||||||
Empty.BandChunkLo = BandChunkLo;
|
Empty.BandChunkLo = BandChunkLo;
|
||||||
Empty.BandChunkHi = BandChunkHi;
|
Empty.BandChunkHi = BandChunkHi;
|
||||||
@@ -6258,7 +6655,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
||||||
RequestStartCycles, TaskDesiredEpoch, Cancellation,
|
RequestStartCycles, TaskDesiredEpoch, TaskModificationEpoch, Cancellation,
|
||||||
WorkPriority]()
|
WorkPriority]()
|
||||||
{
|
{
|
||||||
// RAII: decrement the counter on every exit path.
|
// RAII: decrement the counter on every exit path.
|
||||||
@@ -6279,6 +6676,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
Canceled.Tile = Tile;
|
Canceled.Tile = Tile;
|
||||||
Canceled.Epoch = TaskEpoch;
|
Canceled.Epoch = TaskEpoch;
|
||||||
Canceled.DesiredEpoch = TaskDesiredEpoch;
|
Canceled.DesiredEpoch = TaskDesiredEpoch;
|
||||||
|
Canceled.ModificationEpoch = TaskModificationEpoch;
|
||||||
Canceled.RequestStartCycles = RequestStartCycles;
|
Canceled.RequestStartCycles = RequestStartCycles;
|
||||||
Canceled.WorkPriority = WorkPriority;
|
Canceled.WorkPriority = WorkPriority;
|
||||||
Canceled.bAborted = true;
|
Canceled.bAborted = true;
|
||||||
@@ -6303,6 +6701,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
FScopedVoxelStackRegistration StackRegistration(Tile.Level);
|
FScopedVoxelStackRegistration StackRegistration(Tile.Level);
|
||||||
FChunkResult Result;
|
FChunkResult Result;
|
||||||
Result.DesiredEpoch = TaskDesiredEpoch;
|
Result.DesiredEpoch = TaskDesiredEpoch;
|
||||||
|
Result.ModificationEpoch = TaskModificationEpoch;
|
||||||
Result.WorkPriority = WorkPriority;
|
Result.WorkPriority = WorkPriority;
|
||||||
const uint64 GenerationStartCycles = FPlatformTime::Cycles64();
|
const uint64 GenerationStartCycles = FPlatformTime::Cycles64();
|
||||||
Result.GenerationStartCycles = GenerationStartCycles;
|
Result.GenerationStartCycles = GenerationStartCycles;
|
||||||
@@ -6908,6 +7307,7 @@ void AVoxelWorld::UnloadTile(const FVoxelTileKey& Tile)
|
|||||||
TileComponents.Remove(Tile);
|
TileComponents.Remove(Tile);
|
||||||
}
|
}
|
||||||
LoadedTiles.Remove(Tile);
|
LoadedTiles.Remove(Tile);
|
||||||
|
AppliedModificationEpoch.Remove(Tile);
|
||||||
LoadedTileGeometry.Remove(Tile);
|
LoadedTileGeometry.Remove(Tile);
|
||||||
TransitionHold.Remove(Tile); // couvre aussi le settled cull (qui ne tient pas la hold à jour)
|
TransitionHold.Remove(Tile); // couvre aussi le settled cull (qui ne tient pas la hold à jour)
|
||||||
}
|
}
|
||||||
@@ -7025,7 +7425,28 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
// §9.4 RENDER-SKIP — a tile only a CollisionOnly anchor wants (not the player clipmap) cooks its
|
// §9.4 RENDER-SKIP — a tile only a CollisionOnly anchor wants (not the player clipmap) cooks its
|
||||||
// collision below but is hidden (no draw / VSM). Set every apply (overrides the pool's default-
|
// collision below but is hidden (no draw / VSM). Set every apply (overrides the pool's default-
|
||||||
// visible state); ReconcileAnchorTileVisibility handles later flips on already-loaded tiles.
|
// visible state); ReconcileAnchorTileVisibility handles later flips on already-loaded tiles.
|
||||||
MeshComp->SetVisibility(!CollisionOnlyTiles.Contains(Tile));
|
// During an LOD handoff, keep a newly loaded replacement hidden until the complete covering set
|
||||||
|
// is ready. ReconcileReadyTransitionVisibility then hides the retired component and reveals the
|
||||||
|
// replacement set in the same game-thread turn, so old and new levels are never drawn together.
|
||||||
|
bool bDeferTransitionVisibility = false;
|
||||||
|
if (!CollisionOnlyTiles.Contains(Tile) && IsDesired(Tile))
|
||||||
|
{
|
||||||
|
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& Pair : TileComponents)
|
||||||
|
{
|
||||||
|
if (Pair.Key == Tile || !Pair.Value || IsDesired(Pair.Key)
|
||||||
|
|| !Pair.Value->IsVisible())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (VF_TileFootprintsOverlap(Tile, Pair.Key))
|
||||||
|
{
|
||||||
|
bDeferTransitionVisibility = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MeshComp->SetVisibility(
|
||||||
|
!CollisionOnlyTiles.Contains(Tile) && !bDeferTransitionVisibility);
|
||||||
MeshComp->SetCollisionEnabled(
|
MeshComp->SetCollisionEnabled(
|
||||||
bLevel0 ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision);
|
bLevel0 ? ECollisionEnabled::QueryAndPhysics : ECollisionEnabled::NoCollision);
|
||||||
|
|
||||||
@@ -7274,6 +7695,34 @@ void AVoxelWorld::ApplyModification(const FVoxelModification& Modification)
|
|||||||
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DiffStartCycles)
|
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - DiffStartCycles)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
|
|
||||||
|
if (AffectedChunks.Num() > 0)
|
||||||
|
{
|
||||||
|
++ModificationEpoch;
|
||||||
|
Modification.GetWorldBounds(LastModificationMinVoxel, LastModificationMaxVoxel);
|
||||||
|
FModificationRevision Revision;
|
||||||
|
Revision.Epoch = ModificationEpoch;
|
||||||
|
Revision.MinVoxel = LastModificationMinVoxel;
|
||||||
|
Revision.MaxVoxel = LastModificationMaxVoxel;
|
||||||
|
ModificationRevisions.Add(Revision);
|
||||||
|
if (bVisualStalenessAudit && VisualStalenessAuditMode == 1)
|
||||||
|
{
|
||||||
|
VisualAuditEditEpoch = ModificationEpoch;
|
||||||
|
VisualAuditEditMinVoxel = LastModificationMinVoxel;
|
||||||
|
VisualAuditEditMaxVoxel = LastModificationMaxVoxel;
|
||||||
|
VisualAuditEditBeginSeconds = FPlatformTime::Seconds();
|
||||||
|
VisualAuditEditStaleDurationSeconds = -1.0;
|
||||||
|
VisualAuditEditMaxStaleTiles = 0;
|
||||||
|
bVisualAuditEditSawStale = false;
|
||||||
|
bVisualAuditEditResolved = false;
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeVisualStaleness] edit_begin=1 epoch=%u bounds_min=(%.2f,%.2f,%.2f) "
|
||||||
|
"bounds_max=(%.2f,%.2f,%.2f)"),
|
||||||
|
VisualAuditEditEpoch,
|
||||||
|
VisualAuditEditMinVoxel.X, VisualAuditEditMinVoxel.Y, VisualAuditEditMinVoxel.Z,
|
||||||
|
VisualAuditEditMaxVoxel.X, VisualAuditEditMaxVoxel.Y, VisualAuditEditMaxVoxel.Z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// INSTANT DIG FEEL — synchronously re-mesh the level-0 tile the brush CENTRE sits in, so the hole
|
// INSTANT DIG FEEL — synchronously re-mesh the level-0 tile the brush CENTRE sits in, so the hole
|
||||||
// appears THIS frame right where the player is looking. Neighbour tiles (brush edge) re-mesh async
|
// appears THIS frame right where the player is looking. Neighbour tiles (brush edge) re-mesh async
|
||||||
// and prioritised (RemeshDirtyChunks → DirtyRemeshQueue @ BackgroundHigh), a frame or two behind —
|
// and prioritised (RemeshDirtyChunks → DirtyRemeshQueue @ BackgroundHigh), a frame or two behind —
|
||||||
@@ -7607,24 +8056,50 @@ FString AVoxelWorld::GetCurrentSeasonContentHash() const
|
|||||||
|
|
||||||
void AVoxelWorld::RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords, const FVoxelTileKey* ExcludeTile)
|
void AVoxelWorld::RemeshDirtyChunks(const TArray<FIntVector>& DirtyCoords, const FVoxelTileKey* ExcludeTile)
|
||||||
{
|
{
|
||||||
// Edits only affect LEVEL-0 tiles (collision + visible detail are full-res near the player;
|
// An edit changes every tile whose footprint intersects its affected level-0 chunks, including
|
||||||
// coarse far tiles sample too sparsely to show small carves, and pick up the diff naturally
|
// coarse render tiles. Re-meshing only level 0 leaves a pre-edit coarse component covering the
|
||||||
// when they next stream). Queue each loaded level-0 dirty tile onto DirtyRemeshQueue — drained
|
// same volume until a later LOD transition. The queue is drained FIRST and launched at
|
||||||
// FIRST in the submit loop and launched at BackgroundHigh (ahead of streaming), so a dig never
|
// BackgroundHigh; the old component remains visible until its post-edit replacement is applied.
|
||||||
// waits behind a shell of streaming tasks. LoadTile re-runs gen (density includes the DiffLayer
|
auto TileCoversDirtyCoord = [&DirtyCoords](const FVoxelTileKey& Tile) -> bool
|
||||||
// via GetDensityAt) and ProcessPendingChunks updates the existing component in place (old mesh
|
{
|
||||||
// stays visible until then, no pop).
|
const int32 Span = 1 << Tile.Level;
|
||||||
//
|
const FIntVector MinChunk = Tile.Coord * Span;
|
||||||
// Tiles that are currently mid-gen are STILL queued (not skipped): their in-flight result was
|
const FIntVector MaxChunk = MinChunk + FIntVector(Span, Span, Span);
|
||||||
// sampled BEFORE this carve, so it lands with no hole — keeping the tile queued re-gens it once
|
for (const FIntVector& Coord : DirtyCoords)
|
||||||
// that stale result drains. (The old inline path dropped both over-budget and in-flight tiles,
|
{
|
||||||
// which is why a dig could show up a beat late or not until the player moved.)
|
if (Coord.X >= MinChunk.X && Coord.X < MaxChunk.X
|
||||||
for (const FIntVector& Coord : DirtyCoords)
|
&& Coord.Y >= MinChunk.Y && Coord.Y < MaxChunk.Y
|
||||||
|
&& Coord.Z >= MinChunk.Z && Coord.Z < MaxChunk.Z)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const FVoxelTileKey& Tile : LoadedTiles)
|
||||||
{
|
{
|
||||||
const FVoxelTileKey Tile(Coord, 0);
|
|
||||||
if (ExcludeTile && Tile == *ExcludeTile) continue; // handled synchronously this frame
|
if (ExcludeTile && Tile == *ExcludeTile) continue; // handled synchronously this frame
|
||||||
if (!LoadedTiles.Contains(Tile)) continue; // only re-mesh loaded full-res tiles
|
if (IsDesired(Tile) && TileCoversDirtyCoord(Tile))
|
||||||
|
{
|
||||||
|
DirtyRemeshQueue.Add(Tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pending worker captured the pre-edit field. Mark its token obsolete so its result drains
|
||||||
|
// without publishing; keep the key in DirtyRemeshQueue so the submit budget requests it again.
|
||||||
|
for (const FVoxelTileKey& Tile : PendingTiles)
|
||||||
|
{
|
||||||
|
if (!IsDesired(Tile) || !TileCoversDirtyCoord(Tile)) continue;
|
||||||
DirtyRemeshQueue.Add(Tile);
|
DirtyRemeshQueue.Add(Tile);
|
||||||
|
if (TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>* Cancellation =
|
||||||
|
PendingTileCancellation.Find(Tile))
|
||||||
|
{
|
||||||
|
if (*Cancellation)
|
||||||
|
{
|
||||||
|
(*Cancellation)->bObsolete.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (DirtyRemeshQueue.Num() > 0)
|
if (DirtyRemeshQueue.Num() > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ struct FChunkResult
|
|||||||
bool bAborted = false; // Worker observed shutdown or cancellation; never mark this tile loaded
|
bool bAborted = false; // Worker observed shutdown or cancellation; never mark this tile loaded
|
||||||
bool bObsolete = false; // Worker observed per-tile desired-set cancellation
|
bool bObsolete = false; // Worker observed per-tile desired-set cancellation
|
||||||
uint32 DesiredEpoch = 0; // Desired-set epoch captured at request time
|
uint32 DesiredEpoch = 0; // Desired-set epoch captured at request time
|
||||||
|
uint32 ModificationEpoch = 0; // Diff-layer revision captured at request time
|
||||||
bool bRecordStreamingLatency = true;
|
bool bRecordStreamingLatency = true;
|
||||||
bool bEmpty = true; // true ⇒ no surface (uniform all-air/all-solid or empty band); still loaded
|
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
|
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
|
||||||
@@ -557,6 +558,8 @@ private:
|
|||||||
void MaybeFinishHeadlessSurfaceFallTest();
|
void MaybeFinishHeadlessSurfaceFallTest();
|
||||||
void MaybeFinishHeadlessCollisionGateStressTest();
|
void MaybeFinishHeadlessCollisionGateStressTest();
|
||||||
void MaybeFinishHeadlessStreamingTest();
|
void MaybeFinishHeadlessStreamingTest();
|
||||||
|
void UpdateVisualStalenessAudit();
|
||||||
|
void LogVisualStalenessAudit();
|
||||||
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
void RecordTileHash(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||||
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
int32 Step, int32 Cells, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
bool bSheetTile, const FVoxelMeshData& MeshData);
|
bool bSheetTile, const FVoxelMeshData& MeshData);
|
||||||
@@ -918,12 +921,11 @@ public:
|
|||||||
int32 MeshBandChunkHi = MAX_int32;
|
int32 MeshBandChunkHi = MAX_int32;
|
||||||
TSet<FVoxelTileKey> BandRemeshQueue;
|
TSet<FVoxelTileKey> BandRemeshQueue;
|
||||||
|
|
||||||
// DIG RESPONSIVENESS — loaded level-0 tiles touched by a player carve/fill that still need an
|
// DIG RESPONSIVENESS — tiles at every LOD 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
|
// async re-mesh (the synchronously-remeshed centre tile is excluded). Drained FIRST in the submit
|
||||||
// mid-gen at carve time). Drained FIRST in the submit loop (ahead of streaming + band) and launched
|
// loop (ahead of streaming + band) and launched at BackgroundHigh so a dig never waits behind
|
||||||
// at BackgroundHigh so a dig never waits behind streaming. Unlike the old inline RemeshDirtyChunks,
|
// streaming. A tile that's in flight is marked obsolete and KEPT queued so a stale pre-carve
|
||||||
// a tile that's in flight is KEPT queued (not dropped) so the stale pre-carve result is corrected
|
// result cannot publish and the post-carve result is admitted immediately.
|
||||||
// once it lands — the source of "the hole shows up a beat late, or not until I move".
|
|
||||||
TSet<FVoxelTileKey> DirtyRemeshQueue;
|
TSet<FVoxelTileKey> DirtyRemeshQueue;
|
||||||
|
|
||||||
// F18 — TROU XY de l'anneau feuille (voxels ; Max EXCLUSIF ; sentinelles MAX/MIN = pas de
|
// F18 — TROU XY de l'anneau feuille (voxels ; Max EXCLUSIF ; sentinelles MAX/MIN = pas de
|
||||||
@@ -1016,6 +1018,21 @@ public:
|
|||||||
uint64 AppliedBandTileCountByLevel[TrackedClassifierLODCount]{};
|
uint64 AppliedBandTileCountByLevel[TrackedClassifierLODCount]{};
|
||||||
TMap<FVoxelTileKey, FLoadedTileGeometryStats> LoadedTileGeometry;
|
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
|
struct FStreamingLatencySample
|
||||||
{
|
{
|
||||||
double RequestToReadySeconds = 0.0;
|
double RequestToReadySeconds = 0.0;
|
||||||
@@ -1154,6 +1171,24 @@ public:
|
|||||||
FString HeadlessStrateCrossingTestFailureReason;
|
FString HeadlessStrateCrossingTestFailureReason;
|
||||||
TArray<FVector> HeadlessStrateCrossingTestRoute;
|
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
|
// 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.
|
// beneath the pawn's feet is unresolved. Horizontal movement and player control remain live.
|
||||||
TWeakObjectPtr<APawn> PawnTickPrerequisite;
|
TWeakObjectPtr<APawn> PawnTickPrerequisite;
|
||||||
@@ -1208,6 +1243,9 @@ public:
|
|||||||
|
|
||||||
void ProcessPendingChunks();
|
void ProcessPendingChunks();
|
||||||
void ProcessUnloadQueue(); // budgeted teardown drain (see PendingUnload)
|
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.
|
* Re-queue loaded chunks for async re-generation + re-meshing.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism')]
|
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism')]
|
||||||
[string]$Scenario = 'canonical',
|
[string]$Scenario = 'canonical',
|
||||||
[switch]$Build,
|
[switch]$Build,
|
||||||
[ValidateSet('owner', 'default')]
|
[ValidateSet('owner', 'default')]
|
||||||
@@ -64,6 +64,10 @@ $SurfaceFallMetrics = [ordered]@{
|
|||||||
status = 'not_run'
|
status = 'not_run'
|
||||||
}
|
}
|
||||||
$StrateCrossingMetrics = [ordered]@{ requested = $Scenario -eq 'crossing'; status = 'not_run' }
|
$StrateCrossingMetrics = [ordered]@{ requested = $Scenario -eq 'crossing'; status = 'not_run' }
|
||||||
|
$VisualStalenessMetrics = [ordered]@{
|
||||||
|
requested = $Scenario -in @('visual-edit', 'visual-lod')
|
||||||
|
status = 'not_run'
|
||||||
|
}
|
||||||
$CollisionGateStressMetrics = [ordered]@{
|
$CollisionGateStressMetrics = [ordered]@{
|
||||||
requested = $Scenario -eq 'gate-stress' -or
|
requested = $Scenario -eq 'gate-stress' -or
|
||||||
($Scenario -eq 'tests' -and [string]::IsNullOrWhiteSpace($TestFilter))
|
($Scenario -eq 'tests' -and [string]::IsNullOrWhiteSpace($TestFilter))
|
||||||
@@ -500,7 +504,7 @@ function Sync-OwnerAssets {
|
|||||||
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
|
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
|
||||||
[void]$rows.Add([ordered]@{
|
[void]$rows.Add([ordered]@{
|
||||||
name = $assetName
|
name = $assetName
|
||||||
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'horizon', 'horizon-render', 'determinism')
|
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'horizon', 'horizon-render', 'determinism')
|
||||||
source_path = $source
|
source_path = $source
|
||||||
source_sha256 = Get-Sha256 $source
|
source_sha256 = Get-Sha256 $source
|
||||||
staged_path = $target
|
staged_path = $target
|
||||||
@@ -548,7 +552,7 @@ function Get-AssetEvidence {
|
|||||||
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
|
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
|
||||||
[void]$rows.Add([ordered]@{
|
[void]$rows.Add([ordered]@{
|
||||||
name = $assetName
|
name = $assetName
|
||||||
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'horizon', 'horizon-render', 'determinism')
|
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'horizon', 'horizon-render', 'determinism')
|
||||||
source_path = $source
|
source_path = $source
|
||||||
source_sha256 = Get-Sha256 $source
|
source_sha256 = Get-Sha256 $source
|
||||||
staged_path = $target
|
staged_path = $target
|
||||||
@@ -595,6 +599,7 @@ function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap, [bool]$E
|
|||||||
[void]$args.Add('-FullStdOutLogOutput')
|
[void]$args.Add('-FullStdOutLogOutput')
|
||||||
[void]$args.Add('-forcelogflush')
|
[void]$args.Add('-forcelogflush')
|
||||||
[void]$args.Add('-NoZenAutoLaunch')
|
[void]$args.Add('-NoZenAutoLaunch')
|
||||||
|
[void]$args.Add('-notraceserver')
|
||||||
[void]$args.Add('-nocrashreports')
|
[void]$args.Add('-nocrashreports')
|
||||||
[void]$args.Add('-WindowsPlatformCrashContext.ForceCrashReportDialogOff=1')
|
[void]$args.Add('-WindowsPlatformCrashContext.ForceCrashReportDialogOff=1')
|
||||||
[void]$args.Add("-enginesaveddirsuffix=VoxelForgeTest_$safeLabel")
|
[void]$args.Add("-enginesaveddirsuffix=VoxelForgeTest_$safeLabel")
|
||||||
@@ -613,7 +618,7 @@ function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap, [bool]$E
|
|||||||
$switch = [string]$key
|
$switch = [string]$key
|
||||||
if (-not $switch.StartsWith('-')) { $switch = "-$switch" }
|
if (-not $switch.StartsWith('-')) { $switch = "-$switch" }
|
||||||
$value = Get-ValueText $CvarMap[$key]
|
$value = Get-ValueText $CvarMap[$key]
|
||||||
if ($value -match '\s') {
|
if ($value -match '\s' -or $value.Contains(',')) {
|
||||||
[void]$args.Add(('{0}="{1}"' -f $switch, $value))
|
[void]$args.Add(('{0}="{1}"' -f $switch, $value))
|
||||||
} else {
|
} else {
|
||||||
[void]$args.Add("$switch=$value")
|
[void]$args.Add("$switch=$value")
|
||||||
@@ -994,6 +999,29 @@ function Get-CollisionGateStressMetrics([string]$LogPath) {
|
|||||||
return $metrics
|
return $metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-VisualStalenessMetrics([string]$LogPath) {
|
||||||
|
$lineMatch = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeVisualStaleness\] result=' -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object -Last 1)
|
||||||
|
if ($lineMatch.Count -eq 0) { return [ordered]@{ status = 'missing'; log = $LogPath } }
|
||||||
|
$line = [string]$lineMatch[0].Line
|
||||||
|
$metrics = [ordered]@{ status = 'failed'; log = $LogPath; raw = $line }
|
||||||
|
$result = [regex]::Match($line, 'result=(?<v>PASS|FAIL)')
|
||||||
|
if ($result.Success -and $result.Groups['v'].Value -eq 'PASS') { $metrics.status = 'passed' }
|
||||||
|
foreach ($key in @('mode', 'edit_applied', 'edit_resolved', 'edit_stale_duration_s',
|
||||||
|
'edit_stale_tiles_max', 'lod_overlap_max_s', 'lod_overlap_events',
|
||||||
|
'lod_overlap_pairs_max')) {
|
||||||
|
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
|
||||||
|
if (-not $match.Success) { continue }
|
||||||
|
if ($key -in @('mode', 'edit_applied', 'edit_resolved', 'edit_stale_tiles_max',
|
||||||
|
'lod_overlap_events', 'lod_overlap_pairs_max')) {
|
||||||
|
$metrics[$key] = [int]$match.Groups['v'].Value
|
||||||
|
} else {
|
||||||
|
$metrics[$key] = [double]$match.Groups['v'].Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $metrics
|
||||||
|
}
|
||||||
|
|
||||||
function Get-TestMetrics($Ledger) {
|
function Get-TestMetrics($Ledger) {
|
||||||
if ($null -eq $Ledger) { return @() }
|
if ($null -eq $Ledger) { return @() }
|
||||||
$logPaths = @($Ledger.log, $Ledger.stdout, $Ledger.stderr) | Where-Object {
|
$logPaths = @($Ledger.log, $Ledger.stdout, $Ledger.stderr) | Where-Object {
|
||||||
@@ -1353,6 +1381,40 @@ try {
|
|||||||
(Join-Path $RunRoot 'Logs\perf.log') (Join-Path $RunRoot 'Logs\perf.stdout.log') (Join-Path $RunRoot 'Logs\perf.stderr.log')
|
(Join-Path $RunRoot 'Logs\perf.log') (Join-Path $RunRoot 'Logs\perf.stdout.log') (Join-Path $RunRoot 'Logs\perf.stderr.log')
|
||||||
$GameMetrics = Get-StreamingMetrics (Join-Path $RunRoot 'Logs\perf.log')
|
$GameMetrics = Get-StreamingMetrics (Join-Path $RunRoot 'Logs\perf.log')
|
||||||
}
|
}
|
||||||
|
'visual-edit' {
|
||||||
|
$baseCvars['voxel.TestVisualStaleness'] = 1
|
||||||
|
$baseCvars['voxel.TestModification'] = 1
|
||||||
|
$baseCvars['voxel.TestModificationTarget'] = 'loaded-geometry'
|
||||||
|
$baseCvars['voxel.TestExitSeconds'] = 45
|
||||||
|
$baseCvars['voxel.TestMaxClipLevel'] = 5
|
||||||
|
$baseCvars['voxel.OuterClassifierMode'] = 0
|
||||||
|
$visualLog = Join-Path $RunRoot 'Logs\visual-edit.log'
|
||||||
|
$args = New-CommonArguments 'game' $visualLog (Merge-Cvars $baseCvars)
|
||||||
|
$gameLedger = Invoke-UnrealLaunch 'visual-edit-game' $args `
|
||||||
|
$visualLog (Join-Path $RunRoot 'Logs\visual-edit.stdout.log') (Join-Path $RunRoot 'Logs\visual-edit.stderr.log')
|
||||||
|
$VisualStalenessMetrics = Get-VisualStalenessMetrics $visualLog
|
||||||
|
if ($VisualStalenessMetrics.status -ne 'passed') {
|
||||||
|
Add-Failure "visual edit repro did not pass: $($VisualStalenessMetrics.status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'visual-lod' {
|
||||||
|
$baseCvars['voxel.TestVisualStaleness'] = 2
|
||||||
|
$baseCvars['voxel.TestMove'] = 1
|
||||||
|
$baseCvars['voxel.TestMoveSpeedCmPerSecond'] = 6000
|
||||||
|
$baseCvars['voxel.TestMoveStartSeconds'] = 25
|
||||||
|
$baseCvars['voxel.TestMoveDirection'] = '1, 0, 0.15'
|
||||||
|
$baseCvars['voxel.TestExitSeconds'] = 45
|
||||||
|
$baseCvars['voxel.TestMaxClipLevel'] = 5
|
||||||
|
$baseCvars['voxel.OuterClassifierMode'] = 0
|
||||||
|
$visualLog = Join-Path $RunRoot 'Logs\visual-lod.log'
|
||||||
|
$args = New-CommonArguments 'game' $visualLog (Merge-Cvars $baseCvars)
|
||||||
|
$gameLedger = Invoke-UnrealLaunch 'visual-lod-game' $args `
|
||||||
|
$visualLog (Join-Path $RunRoot 'Logs\visual-lod.stdout.log') (Join-Path $RunRoot 'Logs\visual-lod.stderr.log')
|
||||||
|
$VisualStalenessMetrics = Get-VisualStalenessMetrics $visualLog
|
||||||
|
if ($VisualStalenessMetrics.status -ne 'passed') {
|
||||||
|
Add-Failure "visual LOD repro did not pass: $($VisualStalenessMetrics.status)"
|
||||||
|
}
|
||||||
|
}
|
||||||
'surface-fall' {
|
'surface-fall' {
|
||||||
$baseCvars['voxel.TestSurfaceFall'] = 1
|
$baseCvars['voxel.TestSurfaceFall'] = 1
|
||||||
$baseCvars['voxel.TestSurfaceFallSpawnHeightVoxels'] = 20
|
$baseCvars['voxel.TestSurfaceFallSpawnHeightVoxels'] = 20
|
||||||
@@ -1551,6 +1613,7 @@ $result = [ordered]@{
|
|||||||
determinism = $DeterminismEvidence
|
determinism = $DeterminismEvidence
|
||||||
surface_fall = $SurfaceFallMetrics
|
surface_fall = $SurfaceFallMetrics
|
||||||
strate_crossing = $StrateCrossingMetrics
|
strate_crossing = $StrateCrossingMetrics
|
||||||
|
visual_staleness = $VisualStalenessMetrics
|
||||||
collision_gate_stress = $CollisionGateStressMetrics
|
collision_gate_stress = $CollisionGateStressMetrics
|
||||||
worker_seconds = [ordered]@{
|
worker_seconds = [ordered]@{
|
||||||
commandlet = if ($CommandletMetrics.Contains('worker_seconds')) { $CommandletMetrics.worker_seconds } else { $null }
|
commandlet = if ($CommandletMetrics.Contains('worker_seconds')) { $CommandletMetrics.worker_seconds } else { $null }
|
||||||
@@ -1584,6 +1647,7 @@ try {
|
|||||||
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
|
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
|
||||||
[void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)")
|
[void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)")
|
||||||
[void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)")
|
[void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)")
|
||||||
|
[void]$summaryLines.Add("visual_staleness=$(ConvertTo-CompactJson $result.visual_staleness)")
|
||||||
[void]$summaryLines.Add("collision_gate_stress=$(ConvertTo-CompactJson $result.collision_gate_stress)")
|
[void]$summaryLines.Add("collision_gate_stress=$(ConvertTo-CompactJson $result.collision_gate_stress)")
|
||||||
[void]$summaryLines.Add("probe_amplitudes=$(ConvertTo-CompactJson $result.probe_amplitudes)")
|
[void]$summaryLines.Add("probe_amplitudes=$(ConvertTo-CompactJson $result.probe_amplitudes)")
|
||||||
[void]$summaryLines.Add("worker_seconds=$(ConvertTo-CompactJson $result.worker_seconds)")
|
[void]$summaryLines.Add("worker_seconds=$(ConvertTo-CompactJson $result.worker_seconds)")
|
||||||
|
|||||||
Reference in new issue
Block a user