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:
Fr0zkaandClaude Opus 5 committed 2026-09-23 19:04:13 +02:00
1 parent 370c39e473
commit f36bc1f9b2
3 files changed
+232 -28

No files matched your search

+198 -27
View File
@@ -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);
}
+7
View File
@@ -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,
+27 -1
View File
@@ -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.