wip(streaming): priority by class and level -- near ring 53.6s -> 1.2s, but crossing test fails
Scheduling rule: CollisionCritical -> Edited -> LOD0 -> Horizon, with coarse work capped so 4 task slots stay reserved for near work; result queues follow the same order; epoch and cancellation handling unchanged.
Measured at MaxClipLevel=5 / RenderDistanceChunks=768: spawn ring request-to-ready 53.632 s -> 1.171 s; spawn ring from trace start 68.252 -> 62.549 s; moving first complete ring 66.808 -> 63.917 s, warm ring p50 0.267 s, per-tile moving LOD0 p50 69.4 -> 62.0 ms; full settle of 10,841 tiles 110.7 -> 106.9 s. Gate stress PASS (139.99 m, 0 full stops); static perf PASS.
⚠️ NOT SAFE TO MERGE YET: the strate-crossing test fails in both patched runs with capsule_overlaps_pending_floor -- the exact guarantee the gate exists for. Codex suspects the owner's current DA_Settings (seal 50, clip level 5) rather than this change, since older passing captures used different settings. Control needed: same test, same assets, on 370c39e.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
370c39e473
commit
f36bc1f9b2
3 files changed
+232
-28
No files matched your search
@@ -561,6 +561,9 @@ void AVoxelWorld::RegenerateAllChunks()
|
|||||||
if (ActiveTaskCount.load(std::memory_order_relaxed) == 0)
|
if (ActiveTaskCount.load(std::memory_order_relaxed) == 0)
|
||||||
{
|
{
|
||||||
FChunkResult StaleResult;
|
FChunkResult StaleResult;
|
||||||
|
while (CriticalProcessQueue.Dequeue(StaleResult)) {}
|
||||||
|
while (EditedProcessQueue.Dequeue(StaleResult)) {}
|
||||||
|
while (NearProcessQueue.Dequeue(StaleResult)) {}
|
||||||
while (ProcessQueue.Dequeue(StaleResult)) {}
|
while (ProcessQueue.Dequeue(StaleResult)) {}
|
||||||
PendingTiles.Empty();
|
PendingTiles.Empty();
|
||||||
PendingTileCancellation.Empty();
|
PendingTileCancellation.Empty();
|
||||||
@@ -1138,6 +1141,18 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|||||||
}
|
}
|
||||||
// Drain any queued results
|
// Drain any queued results
|
||||||
FChunkResult Discard;
|
FChunkResult Discard;
|
||||||
|
while (CriticalProcessQueue.Dequeue(Discard))
|
||||||
|
{
|
||||||
|
if (Discard.bObsolete) { ++ObsoleteTileAbortCount; }
|
||||||
|
}
|
||||||
|
while (EditedProcessQueue.Dequeue(Discard))
|
||||||
|
{
|
||||||
|
if (Discard.bObsolete) { ++ObsoleteTileAbortCount; }
|
||||||
|
}
|
||||||
|
while (NearProcessQueue.Dequeue(Discard))
|
||||||
|
{
|
||||||
|
if (Discard.bObsolete) { ++ObsoleteTileAbortCount; }
|
||||||
|
}
|
||||||
while (ProcessQueue.Dequeue(Discard))
|
while (ProcessQueue.Dequeue(Discard))
|
||||||
{
|
{
|
||||||
if (Discard.bObsolete) { ++ObsoleteTileAbortCount; }
|
if (Discard.bObsolete) { ++ObsoleteTileAbortCount; }
|
||||||
@@ -3606,6 +3621,9 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ProcessPendingChunks();
|
ProcessPendingChunks();
|
||||||
|
// 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();
|
||||||
|
|
||||||
// The startup trace stops at the same point the streaming policy declares itself settled:
|
// The startup trace stops at the same point the streaming policy declares itself settled:
|
||||||
// every desired key is loaded, no generation task remains, and the game-thread result drain
|
// every desired key is loaded, no generation task remains, and the game-thread result drain
|
||||||
@@ -4188,8 +4206,8 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preserve the existing critical floor ordering for the next submit pass, and make an
|
// Preserve the existing critical floor ordering for the next submit pass, and make an
|
||||||
// immediate game-thread request when no mesh/cook is already in flight. LoadTile's true flag
|
// immediate game-thread request when no mesh/cook is already in flight. The critical work
|
||||||
// is the established BackgroundHigh streaming priority.
|
// class uses the established BackgroundHigh streaming priority.
|
||||||
CriticalDesiredTiles.Remove(SupportTile);
|
CriticalDesiredTiles.Remove(SupportTile);
|
||||||
CriticalDesiredTiles.Insert(SupportTile, 0);
|
CriticalDesiredTiles.Insert(SupportTile, 0);
|
||||||
if (PendingTiles.Contains(SupportTile) || LoadedTiles.Contains(SupportTile))
|
if (PendingTiles.Contains(SupportTile) || LoadedTiles.Contains(SupportTile))
|
||||||
@@ -4208,7 +4226,7 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
|
|||||||
}
|
}
|
||||||
|
|
||||||
const int32 PendingBefore = PendingTiles.Num();
|
const int32 PendingBefore = PendingTiles.Num();
|
||||||
LoadTile(SupportTile, /*bHighPriority*/ true);
|
LoadTile(SupportTile, EVoxelTileWorkPriority::CollisionCritical);
|
||||||
if (PendingTiles.Num() > PendingBefore)
|
if (PendingTiles.Num() > PendingBefore)
|
||||||
{
|
{
|
||||||
bAllChunksLoaded = false;
|
bAllChunksLoaded = false;
|
||||||
@@ -4570,6 +4588,56 @@ void AVoxelWorld::RecordLOD0ReadySample(
|
|||||||
LOD0ReadySamples.Add(Sample);
|
LOD0ReadySamples.Add(Sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::RecordLOD0RingReadyIfComplete()
|
||||||
|
{
|
||||||
|
if (!VoxelForgeStartupTrace::IsActive()
|
||||||
|
|| DesiredEpoch == 0
|
||||||
|
|| LastLOD0RingReadyEpoch == DesiredEpoch)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 DesiredLOD0Count = 0;
|
||||||
|
for (const FVoxelTileKey& Key : DesiredSorted)
|
||||||
|
{
|
||||||
|
if (Key.Level != 0) continue;
|
||||||
|
++DesiredLOD0Count;
|
||||||
|
if (!LoadedTiles.Contains(Key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (DesiredLOD0Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadedTiles remains populated during an async remesh, so also require that no current
|
||||||
|
// level-0 request is still in flight. Obsolete requests from an older desired epoch do not
|
||||||
|
// hold the current ring back; their cancellation/epoch fence is handled by the normal drain.
|
||||||
|
for (const FVoxelTileKey& Key : PendingTiles)
|
||||||
|
{
|
||||||
|
if (Key.Level == 0 && IsDesired(Key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LastLOD0RingReadyEpoch = DesiredEpoch;
|
||||||
|
const uint64 ReadyCycles = FPlatformTime::Cycles64();
|
||||||
|
const double DurationSeconds = DesiredEpochStartCycles != 0
|
||||||
|
&& ReadyCycles >= DesiredEpochStartCycles
|
||||||
|
? FPlatformTime::ToSeconds64(ReadyCycles - DesiredEpochStartCycles)
|
||||||
|
: 0.0;
|
||||||
|
VoxelForgeStartupTrace::RecordEvent(TEXT("lod0_ring_ready"), FString::Printf(
|
||||||
|
TEXT("\"desired_epoch\":%u,\"center_chunk\":[%d,%d,%d],\"lod0_tiles\":%d,"
|
||||||
|
"\"duration_s\":%s"),
|
||||||
|
DesiredEpoch,
|
||||||
|
DesiredEpochCenterChunk.X, DesiredEpochCenterChunk.Y, DesiredEpochCenterChunk.Z,
|
||||||
|
DesiredLOD0Count,
|
||||||
|
*FString::SanitizeFloat(DurationSeconds)));
|
||||||
|
}
|
||||||
|
|
||||||
void AVoxelWorld::LogStreamingLatencySummary() const
|
void AVoxelWorld::LogStreamingLatencySummary() const
|
||||||
{
|
{
|
||||||
TArray<double> RequestToReady;
|
TArray<double> RequestToReady;
|
||||||
@@ -4854,7 +4922,16 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
int32 ResultsDrained = 0;
|
int32 ResultsDrained = 0;
|
||||||
bool bTimeBudgetHit = false;
|
bool bTimeBudgetHit = false;
|
||||||
FChunkResult DequeuedChunk;
|
FChunkResult DequeuedChunk;
|
||||||
while (ProcessQueue.Dequeue(DequeuedChunk))
|
const auto DequeueNextResult = [&]() -> bool
|
||||||
|
{
|
||||||
|
// Mirror the admission order. A horizon result that completed earlier must not consume
|
||||||
|
// the game-thread apply budget ahead of collision support, an edit, or ordinary LOD0.
|
||||||
|
return CriticalProcessQueue.Dequeue(DequeuedChunk)
|
||||||
|
|| EditedProcessQueue.Dequeue(DequeuedChunk)
|
||||||
|
|| NearProcessQueue.Dequeue(DequeuedChunk)
|
||||||
|
|| ProcessQueue.Dequeue(DequeuedChunk);
|
||||||
|
};
|
||||||
|
while (DequeueNextResult())
|
||||||
{
|
{
|
||||||
++ResultsDrained;
|
++ResultsDrained;
|
||||||
const TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe> Cancellation =
|
const TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe> Cancellation =
|
||||||
@@ -5239,6 +5316,8 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
|
|||||||
CollisionOnlyTiles.Reset(); // §9.4 — rebuilt by AddAnchorDesiredTiles below
|
CollisionOnlyTiles.Reset(); // §9.4 — rebuilt by AddAnchorDesiredTiles below
|
||||||
++DesiredStamp; // les upserts ci-dessous marquent le crossing courant
|
++DesiredStamp; // les upserts ci-dessous marquent le crossing courant
|
||||||
++DesiredEpoch;
|
++DesiredEpoch;
|
||||||
|
DesiredEpochStartCycles = FPlatformTime::Cycles64();
|
||||||
|
DesiredEpochCenterChunk = Center;
|
||||||
|
|
||||||
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
||||||
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
|
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
|
||||||
@@ -5381,6 +5460,18 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
|
|||||||
DesiredSorted.Insert(Key, 0);
|
DesiredSorted.Insert(Key, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (VoxelForgeStartupTrace::IsActive())
|
||||||
|
{
|
||||||
|
int32 LOD0Count = 0;
|
||||||
|
for (const FVoxelTileKey& Key : DesiredSorted)
|
||||||
|
{
|
||||||
|
if (Key.Level == 0) ++LOD0Count;
|
||||||
|
}
|
||||||
|
VoxelForgeStartupTrace::RecordEvent(TEXT("lod0_ring_begin"), FString::Printf(
|
||||||
|
TEXT("\"desired_epoch\":%u,\"center_chunk\":[%d,%d,%d],\"lod0_tiles\":%d"),
|
||||||
|
DesiredEpoch, Center.X, Center.Y, Center.Z, LOD0Count));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AVoxelWorld::CancelObsoleteTileWork()
|
void AVoxelWorld::CancelObsoleteTileWork()
|
||||||
@@ -5550,11 +5641,21 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
|||||||
return FMath::Clamp(Asset, 1, CoreCap);
|
return FMath::Clamp(Asset, 1, CoreCap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 AVoxelWorld::GetNearTaskReserve(int32 MaxTasks) const
|
||||||
|
{
|
||||||
|
const int32 AuthoredReserve = Settings ? Settings->NearTaskReserve : 4;
|
||||||
|
// Keep at least one slot usable by coarse work even when an authored reserve is larger than
|
||||||
|
// the effective task budget. A one-slot budget has no meaningful reservation to make.
|
||||||
|
return FMath::Clamp(AuthoredReserve, 0, FMath::Max(0, MaxTasks - 1));
|
||||||
|
}
|
||||||
|
|
||||||
void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APawn* PlayerPawn,
|
void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APawn* PlayerPawn,
|
||||||
const FVector& PlayerHeading)
|
const FVector& PlayerHeading)
|
||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
|
||||||
const int32 MaxTasks = GetMaxConcurrentTasks();
|
const int32 MaxTasks = GetMaxConcurrentTasks();
|
||||||
|
const int32 NearTaskReserve = GetNearTaskReserve(MaxTasks);
|
||||||
|
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve);
|
||||||
if (GVoxelForgeProfileTileGeneration != 0
|
if (GVoxelForgeProfileTileGeneration != 0
|
||||||
&& !GVoxelForgeStreamingBudgetReported)
|
&& !GVoxelForgeStreamingBudgetReported)
|
||||||
{
|
{
|
||||||
@@ -5566,12 +5667,15 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
|
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
|
||||||
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
|
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
|
||||||
|
"near_task_reserve=%d coarse_task_cap=%d "
|
||||||
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
|
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
|
||||||
LogicalCores,
|
LogicalCores,
|
||||||
AutomaticCap,
|
AutomaticCap,
|
||||||
AuthoredCoreCap,
|
AuthoredCoreCap,
|
||||||
AssetCap,
|
AssetCap,
|
||||||
MaxTasks,
|
MaxTasks,
|
||||||
|
NearTaskReserve,
|
||||||
|
CoarseTaskCap,
|
||||||
Settings ? Settings->MaxMeshAppliesPerFrame : 4,
|
Settings ? Settings->MaxMeshAppliesPerFrame : 4,
|
||||||
Settings ? Settings->MaxMeshApplyMilliseconds : 2.0f,
|
Settings ? Settings->MaxMeshApplyMilliseconds : 2.0f,
|
||||||
Settings ? Settings->MaxUnloadsPerFrame : 6);
|
Settings ? Settings->MaxUnloadsPerFrame : 6);
|
||||||
@@ -5885,8 +5989,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// Submit pending work (budgeted, critical floor prefix then nearest-first). Once everything desired is loaded,
|
// Submit pending work (budgeted, collision-critical -> edits -> LOD0 -> ascending coarse
|
||||||
// do the "settled" cull of the deferred LOD-transition tiles, then go idle.
|
// levels). Coarse work is capped below the full task budget so a later near request always
|
||||||
|
// has reserved admission; far work still runs continuously in the remaining slots.
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
if (!bAllChunksLoaded)
|
if (!bAllChunksLoaded)
|
||||||
{
|
{
|
||||||
@@ -5900,7 +6005,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
{
|
{
|
||||||
if (PendingTiles.Num() >= MaxTasks) break;
|
if (PendingTiles.Num() >= MaxTasks) break;
|
||||||
if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue;
|
if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue;
|
||||||
LoadTile(T, /*bHighPriority*/ true);
|
LoadTile(T, EVoxelTileWorkPriority::CollisionCritical);
|
||||||
++Submitted;
|
++Submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5914,24 +6019,50 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
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();
|
||||||
LoadTile(T, /*bHighPriority*/ true);
|
LoadTile(T, EVoxelTileWorkPriority::Edited);
|
||||||
++Submitted;
|
++Submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEAR TERRAIN — all ordinary level-0 work is promoted after the absolute support/edit
|
||||||
|
// prefixes. It uses the near queue and BackgroundHigh so both worker execution and result
|
||||||
|
// application stay ahead of a completed horizon backlog.
|
||||||
for (const FVoxelTileKey& T : DesiredSorted)
|
for (const FVoxelTileKey& T : DesiredSorted)
|
||||||
{
|
{
|
||||||
|
if (T.Level != 0) continue;
|
||||||
if (PendingTiles.Num() >= MaxTasks) break;
|
if (PendingTiles.Num() >= MaxTasks) break;
|
||||||
if (PendingTiles.Contains(T)) continue; // in flight
|
if (PendingTiles.Contains(T)) continue; // in flight
|
||||||
if (LoadedTiles.Contains(T)) continue; // already loaded (level is in the key — no LOD remesh)
|
if (LoadedTiles.Contains(T)) continue; // already loaded (level is in the key — no LOD remesh)
|
||||||
LoadTile(T);
|
LoadTile(T, EVoxelTileWorkPriority::LOD0);
|
||||||
++Submitted;
|
++Submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FAR FIELD — preserve the clipmap's level ordering explicitly. DesiredSorted is
|
||||||
|
// distance-sorted for the critical/near work, but a level pass prevents a level-5 horizon
|
||||||
|
// tile from being admitted ahead of an unfinished level-1/2 bridge tile.
|
||||||
|
int32 MaxDesiredLevel = 0;
|
||||||
|
for (const FVoxelTileKey& T : DesiredSorted)
|
||||||
|
{
|
||||||
|
MaxDesiredLevel = FMath::Max(MaxDesiredLevel, T.Level);
|
||||||
|
}
|
||||||
|
for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level)
|
||||||
|
{
|
||||||
|
for (const FVoxelTileKey& T : DesiredSorted)
|
||||||
|
{
|
||||||
|
if (T.Level != Level) continue;
|
||||||
|
if (PendingTiles.Num() >= CoarseTaskCap) break;
|
||||||
|
if (PendingTiles.Contains(T)) continue; // in flight
|
||||||
|
if (LoadedTiles.Contains(T)) continue; // already loaded
|
||||||
|
LoadTile(T);
|
||||||
|
++Submitted;
|
||||||
|
}
|
||||||
|
if (PendingTiles.Num() >= CoarseTaskCap) break;
|
||||||
|
}
|
||||||
|
|
||||||
// Bande de strate changée : re-gen budgétée des tuiles grossières concernées (le vieux
|
// Bande de strate changée : re-gen budgétée des tuiles grossières concernées (le vieux
|
||||||
// mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks).
|
// mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks).
|
||||||
for (auto It = BandRemeshQueue.CreateIterator(); It; ++It)
|
for (auto It = BandRemeshQueue.CreateIterator(); It; ++It)
|
||||||
{
|
{
|
||||||
if (PendingTiles.Num() >= MaxTasks) break;
|
if (PendingTiles.Num() >= CoarseTaskCap) break;
|
||||||
const FVoxelTileKey T = *It;
|
const FVoxelTileKey T = *It;
|
||||||
It.RemoveCurrent();
|
It.RemoveCurrent();
|
||||||
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
||||||
@@ -5948,12 +6079,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
|
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
|
||||||
}
|
}
|
||||||
if (GVoxelForgeProfileTileGeneration != 0
|
if (GVoxelForgeProfileTileGeneration != 0
|
||||||
&& PendingTiles.Num() >= MaxTasks
|
&& PendingTiles.Num() >= CoarseTaskCap
|
||||||
&& !GVoxelForgeGenerationCapHitReported)
|
&& !GVoxelForgeGenerationCapHitReported)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d effective=%d"),
|
TEXT("[VoxelForgeStreamingBudget] coarse_generation_cap_hit=1 pending=%d "
|
||||||
PendingTiles.Num(), MaxTasks);
|
"coarse_cap=%d effective=%d near_reserve=%d"),
|
||||||
|
PendingTiles.Num(), CoarseTaskCap, MaxTasks, NearTaskReserve);
|
||||||
GVoxelForgeGenerationCapHitReported = true;
|
GVoxelForgeGenerationCapHitReported = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5973,19 +6105,24 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority)
|
||||||
{
|
{
|
||||||
if (PendingTiles.Contains(Tile)) return;
|
if (PendingTiles.Contains(Tile)) return;
|
||||||
|
|
||||||
const int32 MaxTasks = GetMaxConcurrentTasks(); // T2.d — core-clamped
|
const int32 MaxTasks = GetMaxConcurrentTasks(); // T2.d — core-clamped
|
||||||
if (PendingTiles.Num() >= MaxTasks)
|
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
|
||||||
|
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - GetNearTaskReserve(MaxTasks));
|
||||||
|
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
|
||||||
|
? CoarseTaskCap : MaxTasks;
|
||||||
|
if (PendingTiles.Num() >= AdmissionCap)
|
||||||
{
|
{
|
||||||
if (GVoxelForgeProfileTileGeneration != 0
|
if (GVoxelForgeProfileTileGeneration != 0
|
||||||
&& !GVoxelForgeGenerationCapHitReported)
|
&& !GVoxelForgeGenerationCapHitReported)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Display,
|
UE_LOG(LogTemp, Display,
|
||||||
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d effective=%d"),
|
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d admission=%d "
|
||||||
PendingTiles.Num(), MaxTasks);
|
"effective=%d near_reserve=%d tile_level=%d"),
|
||||||
|
PendingTiles.Num(), AdmissionCap, MaxTasks, GetNearTaskReserve(MaxTasks), Tile.Level);
|
||||||
GVoxelForgeGenerationCapHitReported = true;
|
GVoxelForgeGenerationCapHitReported = true;
|
||||||
}
|
}
|
||||||
return; // Budget full — wait for a task to finish.
|
return; // Budget full — wait for a task to finish.
|
||||||
@@ -6048,7 +6185,15 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
Empty.Epoch = TaskEpoch;
|
Empty.Epoch = TaskEpoch;
|
||||||
Empty.DesiredEpoch = TaskDesiredEpoch;
|
Empty.DesiredEpoch = TaskDesiredEpoch;
|
||||||
Empty.RequestStartCycles = RequestStartCycles;
|
Empty.RequestStartCycles = RequestStartCycles;
|
||||||
ProcessQueue.Enqueue(MoveTemp(Empty));
|
Empty.WorkPriority = WorkPriority;
|
||||||
|
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
||||||
|
CriticalProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
|
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
|
||||||
|
EditedProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
|
else if (WorkPriority == EVoxelTileWorkPriority::LOD0)
|
||||||
|
NearProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
|
else
|
||||||
|
ProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BandChunkLo = MeshBandChunkLo; // pour la résolution matériaux sol/cap dans ApplyMeshToTile
|
BandChunkLo = MeshBandChunkLo; // pour la résolution matériaux sol/cap dans ApplyMeshToTile
|
||||||
@@ -6081,7 +6226,15 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
Empty.RequestStartCycles = RequestStartCycles;
|
Empty.RequestStartCycles = RequestStartCycles;
|
||||||
Empty.BandChunkLo = BandChunkLo;
|
Empty.BandChunkLo = BandChunkLo;
|
||||||
Empty.BandChunkHi = BandChunkHi;
|
Empty.BandChunkHi = BandChunkHi;
|
||||||
ProcessQueue.Enqueue(MoveTemp(Empty));
|
Empty.WorkPriority = WorkPriority;
|
||||||
|
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
||||||
|
CriticalProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
|
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
|
||||||
|
EditedProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
|
else if (WorkPriority == EVoxelTileWorkPriority::LOD0)
|
||||||
|
NearProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
|
else
|
||||||
|
ProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6091,20 +6244,22 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
PendingTileCancellation.Add(Tile, Cancellation);
|
PendingTileCancellation.Add(Tile, Cancellation);
|
||||||
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
||||||
// BackgroundNormal priority: gen runs on background workers that YIELD to foreground
|
// BackgroundNormal priority: coarse gen runs on background workers that YIELD to foreground
|
||||||
// (game/render-thread) tasks. Without this, raising MaxConcurrentTasks past the spare
|
// (game/render-thread) tasks. Without this, raising MaxConcurrentTasks past the spare
|
||||||
// core count saturates the scheduler and starves the frame (the "over 12 = lag" symptom).
|
// core count saturates the scheduler and starves the frame (the "over 12 = lag" symptom).
|
||||||
// At background priority the frame keeps its cores; gen just fills in around it.
|
// At background priority the frame keeps its cores; gen just fills in around it.
|
||||||
// FLOOR/DIG RESPONSIVENESS: critical support requests and player carves launch at BackgroundHigh
|
// NEAR RESPONSIVENESS: collision support, edits, and every level-0 tile launch at
|
||||||
// (bHighPriority) — still a background worker (yields to the frame, keeps the invariant) but
|
// BackgroundHigh — still a background worker (yields to the frame, keeps the invariant) but
|
||||||
// jumps AHEAD of ordinary pending streaming gen.
|
// jumps AHEAD of ordinary pending streaming gen. Admission reserves slots for this class and
|
||||||
const UE::Tasks::ETaskPriority TaskPriority = bHighPriority
|
// the result queues apply it in collision -> edit -> LOD0 order.
|
||||||
|
const UE::Tasks::ETaskPriority TaskPriority = bNearPriority
|
||||||
? UE::Tasks::ETaskPriority::BackgroundHigh
|
? UE::Tasks::ETaskPriority::BackgroundHigh
|
||||||
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
||||||
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, Cancellation,
|
||||||
|
WorkPriority]()
|
||||||
{
|
{
|
||||||
// RAII: decrement the counter on every exit path.
|
// RAII: decrement the counter on every exit path.
|
||||||
struct FTaskGuard
|
struct FTaskGuard
|
||||||
@@ -6125,9 +6280,17 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
Canceled.Epoch = TaskEpoch;
|
Canceled.Epoch = TaskEpoch;
|
||||||
Canceled.DesiredEpoch = TaskDesiredEpoch;
|
Canceled.DesiredEpoch = TaskDesiredEpoch;
|
||||||
Canceled.RequestStartCycles = RequestStartCycles;
|
Canceled.RequestStartCycles = RequestStartCycles;
|
||||||
|
Canceled.WorkPriority = WorkPriority;
|
||||||
Canceled.bAborted = true;
|
Canceled.bAborted = true;
|
||||||
Canceled.bObsolete = true;
|
Canceled.bObsolete = true;
|
||||||
ProcessQueue.Enqueue(MoveTemp(Canceled));
|
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
||||||
|
CriticalProcessQueue.Enqueue(MoveTemp(Canceled));
|
||||||
|
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
|
||||||
|
EditedProcessQueue.Enqueue(MoveTemp(Canceled));
|
||||||
|
else if (WorkPriority == EVoxelTileWorkPriority::LOD0)
|
||||||
|
NearProcessQueue.Enqueue(MoveTemp(Canceled));
|
||||||
|
else
|
||||||
|
ProcessQueue.Enqueue(MoveTemp(Canceled));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ShouldAbortWork()) return;
|
if (ShouldAbortWork()) return;
|
||||||
@@ -6140,6 +6303,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
FScopedVoxelStackRegistration StackRegistration(Tile.Level);
|
FScopedVoxelStackRegistration StackRegistration(Tile.Level);
|
||||||
FChunkResult Result;
|
FChunkResult Result;
|
||||||
Result.DesiredEpoch = TaskDesiredEpoch;
|
Result.DesiredEpoch = TaskDesiredEpoch;
|
||||||
|
Result.WorkPriority = WorkPriority;
|
||||||
const uint64 GenerationStartCycles = FPlatformTime::Cycles64();
|
const uint64 GenerationStartCycles = FPlatformTime::Cycles64();
|
||||||
Result.GenerationStartCycles = GenerationStartCycles;
|
Result.GenerationStartCycles = GenerationStartCycles;
|
||||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||||
@@ -6173,7 +6337,14 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
|
|
||||||
if (!ShouldAbortWork())
|
if (!ShouldAbortWork())
|
||||||
{
|
{
|
||||||
ProcessQueue.Enqueue(MoveTemp(Result)); // move: don't copy the geometry payload
|
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
||||||
|
CriticalProcessQueue.Enqueue(MoveTemp(Result));
|
||||||
|
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
|
||||||
|
EditedProcessQueue.Enqueue(MoveTemp(Result));
|
||||||
|
else if (WorkPriority == EVoxelTileWorkPriority::LOD0)
|
||||||
|
NearProcessQueue.Enqueue(MoveTemp(Result));
|
||||||
|
else
|
||||||
|
ProcessQueue.Enqueue(MoveTemp(Result)); // move: don't copy the geometry payload
|
||||||
}
|
}
|
||||||
}, TaskPriority);
|
}, TaskPriority);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0"))
|
||||||
int32 MaxGenerationWorkerCores = 0;
|
int32 MaxGenerationWorkerCores = 0;
|
||||||
|
|
||||||
|
// Keep this many task slots available for collision-critical, edited, and level-0 terrain while
|
||||||
|
// coarse horizon work is in flight. A positive reserve prevents a full far-field queue from
|
||||||
|
// delaying a new near tile because running tasks cannot be preempted. The runtime clamps the
|
||||||
|
// value below the effective task budget; 4 is the conservative default for the ship settings.
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "0", ClampMax = "16"))
|
||||||
|
int32 NearTaskReserve = 4;
|
||||||
|
|
||||||
// Nombre max de meshes appliqués (upload GPU) par frame. Seuls les vrais applies
|
// Nombre max de meshes appliqués (upload GPU) par frame. Seuls les vrais applies
|
||||||
// comptent (chunks vides/périmés se vident gratuitement). À 32³ chaque apply est léger
|
// comptent (chunks vides/périmés se vident gratuitement). À 32³ chaque apply est léger
|
||||||
// (~0.1 ms) — tunable en live sur l'asset : montez (8-16) si le remplissage traîne,
|
// (~0.1 ms) — tunable en live sur l'asset : montez (8-16) si le remplissage traîne,
|
||||||
|
|||||||
@@ -88,6 +88,14 @@ struct FVoxelStreamingAnchor
|
|||||||
* - BeginPlay: Initialize generator, mesher
|
* - BeginPlay: Initialize generator, mesher
|
||||||
* - Tick: Update chunks around player position
|
* - Tick: Update chunks around player position
|
||||||
*/
|
*/
|
||||||
|
enum class EVoxelTileWorkPriority : uint8
|
||||||
|
{
|
||||||
|
Horizon = 0,
|
||||||
|
LOD0 = 1,
|
||||||
|
Edited = 2,
|
||||||
|
CollisionCritical = 3
|
||||||
|
};
|
||||||
|
|
||||||
struct FChunkResult
|
struct FChunkResult
|
||||||
{
|
{
|
||||||
FVoxelTileKey Tile; // which clipmap tile this mesh is for (carries coord + level)
|
FVoxelTileKey Tile; // which clipmap tile this mesh is for (carries coord + level)
|
||||||
@@ -108,6 +116,7 @@ struct FChunkResult
|
|||||||
uint64 GenerationStartCycles = 0;
|
uint64 GenerationStartCycles = 0;
|
||||||
uint64 GenerationEndCycles = 0;
|
uint64 GenerationEndCycles = 0;
|
||||||
uint64 ApplyStartCycles = 0;
|
uint64 ApplyStartCycles = 0;
|
||||||
|
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
|
||||||
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0;
|
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0;
|
||||||
int32 NumVertices = 0;
|
int32 NumVertices = 0;
|
||||||
uint64 GeometryBytes = 0;
|
uint64 GeometryBytes = 0;
|
||||||
@@ -313,6 +322,8 @@ public:
|
|||||||
* capped to (logical cores − 2) so small CPUs don't thrash on a flat 16 (background
|
* capped to (logical cores − 2) so small CPUs don't thrash on a flat 16 (background
|
||||||
* priority stops frame starvation, not the context-switch overhead). */
|
* priority stops frame starvation, not the context-switch overhead). */
|
||||||
int32 GetMaxConcurrentTasks() const;
|
int32 GetMaxConcurrentTasks() const;
|
||||||
|
/** Reserve task slots for collision-critical/edit/LOD0 work; far work uses the remainder. */
|
||||||
|
int32 GetNearTaskReserve(int32 MaxTasks) const;
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// TERRAIN MODIFICATION (player carving & filling)
|
// TERRAIN MODIFICATION (player carving & filling)
|
||||||
@@ -738,7 +749,8 @@ public:
|
|||||||
*
|
*
|
||||||
* @param ChunkCoord - Which chunk to load
|
* @param ChunkCoord - Which chunk to load
|
||||||
*/
|
*/
|
||||||
void LoadTile(const FVoxelTileKey& Tile, bool bHighPriority = false);
|
void LoadTile(const FVoxelTileKey& Tile,
|
||||||
|
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Worker-side gen for one tile: classify → GenerateMesh/GenerateSheetMesh → BuildTileStreamSet.
|
* Worker-side gen for one tile: classify → GenerateMesh/GenerateSheetMesh → BuildTileStreamSet.
|
||||||
@@ -871,6 +883,7 @@ public:
|
|||||||
void RecordLOD0ReadySample(uint64 RequestStartCycles, uint64 GenerationStartCycles,
|
void RecordLOD0ReadySample(uint64 RequestStartCycles, uint64 GenerationStartCycles,
|
||||||
uint64 GenerationEndCycles, uint64 ApplyStartCycles,
|
uint64 GenerationEndCycles, uint64 ApplyStartCycles,
|
||||||
uint64 ReadyCycles, uint64 CollisionSubmittedCycles);
|
uint64 ReadyCycles, uint64 CollisionSubmittedCycles);
|
||||||
|
void RecordLOD0RingReadyIfComplete();
|
||||||
void LogStreamingLatencySummary() const;
|
void LogStreamingLatencySummary() const;
|
||||||
|
|
||||||
// (GetLODForChunk / LODToStep / IsChunkInRange removed — dead since the clipmap
|
// (GetLODForChunk / LODToStep / IsChunkInRange removed — dead since the clipmap
|
||||||
@@ -884,6 +897,12 @@ public:
|
|||||||
// mode is NOT safe for multiple producers — concurrent Enqueues race on the tail
|
// mode is NOT safe for multiple producers — concurrent Enqueues race on the tail
|
||||||
// link and silently drop results, which leaks PendingChunkCoord slots until the
|
// link and silently drop results, which leaks PendingChunkCoord slots until the
|
||||||
// budget is exhausted and streaming stalls permanently. Mpsc guards the producer side.
|
// budget is exhausted and streaming stalls permanently. Mpsc guards the producer side.
|
||||||
|
// Result queues mirror the scheduling order: collision support first, then edited tiles,
|
||||||
|
// then ordinary LOD0, then the horizon. This keeps a support result from waiting behind a
|
||||||
|
// completed near-field backlog while preserving one consumer on the game thread.
|
||||||
|
TQueue<FChunkResult, EQueueMode::Mpsc> CriticalProcessQueue;
|
||||||
|
TQueue<FChunkResult, EQueueMode::Mpsc> EditedProcessQueue;
|
||||||
|
TQueue<FChunkResult, EQueueMode::Mpsc> NearProcessQueue;
|
||||||
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
||||||
TSet<FVoxelTileKey> PendingTiles; // tiles with a gen task in flight
|
TSet<FVoxelTileKey> PendingTiles; // tiles with a gen task in flight
|
||||||
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
|
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
|
||||||
@@ -1005,6 +1024,13 @@ public:
|
|||||||
double CollisionCookSeconds = 0.0;
|
double CollisionCookSeconds = 0.0;
|
||||||
};
|
};
|
||||||
TArray<FStreamingLatencySample> LOD0ReadySamples;
|
TArray<FStreamingLatencySample> LOD0ReadySamples;
|
||||||
|
// Trace-only ring timing: each desired-set rebuild starts a new epoch. A ring is complete
|
||||||
|
// when every current desired level-0 key has reached LoadedTiles and no level-0 request for
|
||||||
|
// that epoch remains pending. This measures visible near-field readiness independently of the
|
||||||
|
// much larger horizon settle point.
|
||||||
|
uint64 DesiredEpochStartCycles = 0;
|
||||||
|
FIntVector DesiredEpochCenterChunk = FIntVector::ZeroValue;
|
||||||
|
uint32 LastLOD0RingReadyEpoch = MAX_uint32;
|
||||||
|
|
||||||
// Headless validation hook. These are intentionally non-UPROPERTY state and are enabled only
|
// Headless validation hook. These are intentionally non-UPROPERTY state and are enabled only
|
||||||
// by explicit command-line switches; they do not alter ordinary player movement or streaming.
|
// by explicit command-line switches; they do not alter ordinary player movement or streaming.
|
||||||
|
|||||||
Reference in new issue
Block a user