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)
|
||||
{
|
||||
FChunkResult StaleResult;
|
||||
while (CriticalProcessQueue.Dequeue(StaleResult)) {}
|
||||
while (EditedProcessQueue.Dequeue(StaleResult)) {}
|
||||
while (NearProcessQueue.Dequeue(StaleResult)) {}
|
||||
while (ProcessQueue.Dequeue(StaleResult)) {}
|
||||
PendingTiles.Empty();
|
||||
PendingTileCancellation.Empty();
|
||||
@@ -1138,6 +1141,18 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
}
|
||||
// Drain any queued results
|
||||
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))
|
||||
{
|
||||
if (Discard.bObsolete) { ++ObsoleteTileAbortCount; }
|
||||
@@ -3606,6 +3621,9 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
}
|
||||
}
|
||||
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:
|
||||
// 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
|
||||
// immediate game-thread request when no mesh/cook is already in flight. LoadTile's true flag
|
||||
// is the established BackgroundHigh streaming priority.
|
||||
// immediate game-thread request when no mesh/cook is already in flight. The critical work
|
||||
// class uses the established BackgroundHigh streaming priority.
|
||||
CriticalDesiredTiles.Remove(SupportTile);
|
||||
CriticalDesiredTiles.Insert(SupportTile, 0);
|
||||
if (PendingTiles.Contains(SupportTile) || LoadedTiles.Contains(SupportTile))
|
||||
@@ -4208,7 +4226,7 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
|
||||
}
|
||||
|
||||
const int32 PendingBefore = PendingTiles.Num();
|
||||
LoadTile(SupportTile, /*bHighPriority*/ true);
|
||||
LoadTile(SupportTile, EVoxelTileWorkPriority::CollisionCritical);
|
||||
if (PendingTiles.Num() > PendingBefore)
|
||||
{
|
||||
bAllChunksLoaded = false;
|
||||
@@ -4570,6 +4588,56 @@ void AVoxelWorld::RecordLOD0ReadySample(
|
||||
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
|
||||
{
|
||||
TArray<double> RequestToReady;
|
||||
@@ -4854,7 +4922,16 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
int32 ResultsDrained = 0;
|
||||
bool bTimeBudgetHit = false;
|
||||
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;
|
||||
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
|
||||
++DesiredStamp; // les upserts ci-dessous marquent le crossing courant
|
||||
++DesiredEpoch;
|
||||
DesiredEpochStartCycles = FPlatformTime::Cycles64();
|
||||
DesiredEpochCenterChunk = Center;
|
||||
|
||||
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -5550,11 +5641,21 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||
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,
|
||||
const FVector& PlayerHeading)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
|
||||
const int32 MaxTasks = GetMaxConcurrentTasks();
|
||||
const int32 NearTaskReserve = GetNearTaskReserve(MaxTasks);
|
||||
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve);
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& !GVoxelForgeStreamingBudgetReported)
|
||||
{
|
||||
@@ -5566,12 +5667,15 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_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"),
|
||||
LogicalCores,
|
||||
AutomaticCap,
|
||||
AuthoredCoreCap,
|
||||
AssetCap,
|
||||
MaxTasks,
|
||||
NearTaskReserve,
|
||||
CoarseTaskCap,
|
||||
Settings ? Settings->MaxMeshAppliesPerFrame : 4,
|
||||
Settings ? Settings->MaxMeshApplyMilliseconds : 2.0f,
|
||||
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,
|
||||
// do the "settled" cull of the deferred LOD-transition tiles, then go idle.
|
||||
// Submit pending work (budgeted, collision-critical -> edits -> LOD0 -> ascending coarse
|
||||
// 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)
|
||||
{
|
||||
@@ -5900,7 +6005,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
{
|
||||
if (PendingTiles.Num() >= MaxTasks) break;
|
||||
if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue;
|
||||
LoadTile(T, /*bHighPriority*/ true);
|
||||
LoadTile(T, EVoxelTileWorkPriority::CollisionCritical);
|
||||
++Submitted;
|
||||
}
|
||||
|
||||
@@ -5914,24 +6019,50 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop
|
||||
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
|
||||
It.RemoveCurrent();
|
||||
LoadTile(T, /*bHighPriority*/ true);
|
||||
LoadTile(T, EVoxelTileWorkPriority::Edited);
|
||||
++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)
|
||||
{
|
||||
if (T.Level != 0) continue;
|
||||
if (PendingTiles.Num() >= MaxTasks) break;
|
||||
if (PendingTiles.Contains(T)) continue; // in flight
|
||||
if (LoadedTiles.Contains(T)) continue; // already loaded (level is in the key — no LOD remesh)
|
||||
LoadTile(T);
|
||||
LoadTile(T, EVoxelTileWorkPriority::LOD0);
|
||||
++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
|
||||
// mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks).
|
||||
for (auto It = BandRemeshQueue.CreateIterator(); It; ++It)
|
||||
{
|
||||
if (PendingTiles.Num() >= MaxTasks) break;
|
||||
if (PendingTiles.Num() >= CoarseTaskCap) break;
|
||||
const FVoxelTileKey T = *It;
|
||||
It.RemoveCurrent();
|
||||
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
||||
@@ -5948,12 +6079,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
|
||||
}
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& PendingTiles.Num() >= MaxTasks
|
||||
&& PendingTiles.Num() >= CoarseTaskCap
|
||||
&& !GVoxelForgeGenerationCapHitReported)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d effective=%d"),
|
||||
PendingTiles.Num(), MaxTasks);
|
||||
TEXT("[VoxelForgeStreamingBudget] coarse_generation_cap_hit=1 pending=%d "
|
||||
"coarse_cap=%d effective=%d near_reserve=%d"),
|
||||
PendingTiles.Num(), CoarseTaskCap, MaxTasks, NearTaskReserve);
|
||||
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;
|
||||
|
||||
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
|
||||
&& !GVoxelForgeGenerationCapHitReported)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d effective=%d"),
|
||||
PendingTiles.Num(), MaxTasks);
|
||||
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d admission=%d "
|
||||
"effective=%d near_reserve=%d tile_level=%d"),
|
||||
PendingTiles.Num(), AdmissionCap, MaxTasks, GetNearTaskReserve(MaxTasks), Tile.Level);
|
||||
GVoxelForgeGenerationCapHitReported = true;
|
||||
}
|
||||
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.DesiredEpoch = TaskDesiredEpoch;
|
||||
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;
|
||||
}
|
||||
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.BandChunkLo = BandChunkLo;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -6091,20 +6244,22 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
PendingTileCancellation.Add(Tile, Cancellation);
|
||||
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
|
||||
// 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.
|
||||
// FLOOR/DIG RESPONSIVENESS: critical support requests and player carves launch at BackgroundHigh
|
||||
// (bHighPriority) — still a background worker (yields to the frame, keeps the invariant) but
|
||||
// jumps AHEAD of ordinary pending streaming gen.
|
||||
const UE::Tasks::ETaskPriority TaskPriority = bHighPriority
|
||||
// NEAR RESPONSIVENESS: collision support, edits, and every level-0 tile launch at
|
||||
// BackgroundHigh — still a background worker (yields to the frame, keeps the invariant) but
|
||||
// jumps AHEAD of ordinary pending streaming gen. Admission reserves slots for this class and
|
||||
// the result queues apply it in collision -> edit -> LOD0 order.
|
||||
const UE::Tasks::ETaskPriority TaskPriority = bNearPriority
|
||||
? UE::Tasks::ETaskPriority::BackgroundHigh
|
||||
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
||||
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
||||
RequestStartCycles, TaskDesiredEpoch, Cancellation]()
|
||||
RequestStartCycles, TaskDesiredEpoch, Cancellation,
|
||||
WorkPriority]()
|
||||
{
|
||||
// RAII: decrement the counter on every exit path.
|
||||
struct FTaskGuard
|
||||
@@ -6125,9 +6280,17 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
Canceled.Epoch = TaskEpoch;
|
||||
Canceled.DesiredEpoch = TaskDesiredEpoch;
|
||||
Canceled.RequestStartCycles = RequestStartCycles;
|
||||
Canceled.WorkPriority = WorkPriority;
|
||||
Canceled.bAborted = 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;
|
||||
@@ -6140,6 +6303,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
FScopedVoxelStackRegistration StackRegistration(Tile.Level);
|
||||
FChunkResult Result;
|
||||
Result.DesiredEpoch = TaskDesiredEpoch;
|
||||
Result.WorkPriority = WorkPriority;
|
||||
const uint64 GenerationStartCycles = FPlatformTime::Cycles64();
|
||||
Result.GenerationStartCycles = GenerationStartCycles;
|
||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||
@@ -6173,7 +6337,14 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,13 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Voxel|Streaming", meta = (ClampMin = "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
|
||||
// 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,
|
||||
|
||||
@@ -88,6 +88,14 @@ struct FVoxelStreamingAnchor
|
||||
* - BeginPlay: Initialize generator, mesher
|
||||
* - Tick: Update chunks around player position
|
||||
*/
|
||||
enum class EVoxelTileWorkPriority : uint8
|
||||
{
|
||||
Horizon = 0,
|
||||
LOD0 = 1,
|
||||
Edited = 2,
|
||||
CollisionCritical = 3
|
||||
};
|
||||
|
||||
struct FChunkResult
|
||||
{
|
||||
FVoxelTileKey Tile; // which clipmap tile this mesh is for (carries coord + level)
|
||||
@@ -108,6 +116,7 @@ struct FChunkResult
|
||||
uint64 GenerationStartCycles = 0;
|
||||
uint64 GenerationEndCycles = 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 NumVertices = 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
|
||||
* priority stops frame starvation, not the context-switch overhead). */
|
||||
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)
|
||||
@@ -738,7 +749,8 @@ public:
|
||||
*
|
||||
* @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.
|
||||
@@ -871,6 +883,7 @@ public:
|
||||
void RecordLOD0ReadySample(uint64 RequestStartCycles, uint64 GenerationStartCycles,
|
||||
uint64 GenerationEndCycles, uint64 ApplyStartCycles,
|
||||
uint64 ReadyCycles, uint64 CollisionSubmittedCycles);
|
||||
void RecordLOD0RingReadyIfComplete();
|
||||
void LogStreamingLatencySummary() const;
|
||||
|
||||
// (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
|
||||
// link and silently drop results, which leaks PendingChunkCoord slots until the
|
||||
// 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;
|
||||
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.
|
||||
@@ -1005,6 +1024,13 @@ public:
|
||||
double CollisionCookSeconds = 0.0;
|
||||
};
|
||||
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
|
||||
// by explicit command-line switches; they do not alter ordinary player movement or streaming.
|
||||
|
||||
Reference in new issue
Block a user