wip(assembler): round S7 -- workers kept busy: in-game cold full view 17.5 s -> 7.8 s

Two admission blockers found by per-frame decline codes: the near reserve held
four slots idle after LOD0 was done (coarse capped at 6), and finished-but-
unapplied results counted against the task cap. Admission now counts queued +
running tasks; near reserve releases once the walkable ring is ready while
stationary (voxel.StreamingDynamicNearReserve, default on). Walkable ring
0.65 -> 0.58 s, full LOD0 2.28 -> 1.44 s, busy workers 2.2 -> 5.6 of 10.
Old behaviour behind voxel.StreamingLegacyPendingResultCap. RMC source reviewed
(read-only): batching/fast-cook ranked, not yet worth it on measured apply cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 08:10:10 +02:00
1 parent b9c13c37c0
commit 5eb9ee76ff
7 files changed
+726 -53

No files matched your search

+331 -29
View File
@@ -185,11 +185,17 @@ namespace
GVoxelForgeStreamingCoarseFirst,
TEXT("Prioritize current walkable tiles, then far coarse clipmap levels, then near refinement; 0 restores legacy LOD order."));
int32 GVoxelForgeStreamingDynamicNearReserve = 0;
int32 GVoxelForgeStreamingDynamicNearReserve = 1;
FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve(
TEXT("voxel.StreamingDynamicNearReserve"),
GVoxelForgeStreamingDynamicNearReserve,
TEXT("Keep the authored near-worker reserve while the pawn moves or its walkable ring is incomplete; release it when stationary and ready."));
TEXT("Keep the authored near-worker reserve while moving or before the walkable ring is ready; default 1 releases idle slots after that gate, 0 restores the legacy fixed reserve."));
int32 GVoxelForgeStreamingLegacyPendingResultCap = 0;
FAutoConsoleVariableRef CVarVoxelForgeStreamingLegacyPendingResultCap(
TEXT("voxel.StreamingLegacyPendingResultCap"),
GVoxelForgeStreamingLegacyPendingResultCap,
TEXT("Compatibility control: 1 counts completed results awaiting apply against the generation task cap; default 0 caps only queued or running generation tasks."));
int32 GVoxelForgeProfileTileGeneration = 0;
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
@@ -1944,6 +1950,9 @@ void AVoxelWorld::BeginPlay()
PeakObservedPawnSpeedCmPerSecond = 0.0f;
TotalWorkerGenerationCycles.store(0, std::memory_order_relaxed);
TotalWorkerGenerationTasks.store(0, std::memory_order_relaxed);
TotalGenerationWorkerTaskCycles.store(0, std::memory_order_relaxed);
LastStreamingSchedulerSampleSeconds = 0.0;
LastStreamingSchedulerWorkerCycles = 0;
TotalObsoleteWorkerCycles.store(0, std::memory_order_relaxed);
TotalObsoleteWorkerTasks.store(0, std::memory_order_relaxed);
TotalValidationDensityCalls.store(0, std::memory_order_relaxed);
@@ -2060,6 +2069,8 @@ void AVoxelWorld::BeginPlay()
GVoxelForgeStreamingCoarseFirst);
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingDynamicNearReserve="),
GVoxelForgeStreamingDynamicNearReserve);
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingLegacyPendingResultCap="),
GVoxelForgeStreamingLegacyPendingResultCap);
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="),
CommandLineStep1PrequeueVisibleLOD0))
{
@@ -10954,6 +10965,15 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
int32 AVoxelWorld::GetActiveGenerationTaskCount() const
{
return FMath::Max(0, ActiveTaskCount.load(std::memory_order_relaxed));
}
int32 AVoxelWorld::GetGenerationAdmissionCount() const
{
if (GVoxelForgeStreamingLegacyPendingResultCap == 0)
{
return GetActiveGenerationTaskCount();
}
const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0
? Step1HeldApplyTiles.Num() : 0;
return FMath::Max(0, PendingTiles.Num() - HeldCompletedResults);
@@ -10976,13 +10996,56 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
const bool bTraceStreamingScheduler = VoxelForgeStartupTrace::IsActive();
const double StreamingUpdateStartSeconds = bTraceStreamingScheduler
? FPlatformTime::Seconds() : 0.0;
const int32 PendingTilesAtUpdateEntry = PendingTiles.Num();
double DesiredRebuildMilliseconds = 0.0;
const auto RecordSchedulerSkip = [&](const TCHAR* Reason)
{
if (!bTraceStreamingScheduler) return;
const double Now = FPlatformTime::Seconds();
int32 DesiredMissing = 0;
for (const FVoxelTileKey& Tile : DesiredSorted)
{
if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile))
{
++DesiredMissing;
}
}
const int32 ActiveTasks = ActiveTaskCount.load(std::memory_order_relaxed);
const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0
? Step1HeldApplyTiles.Num() : 0;
const int32 PendingResultsEstimate = FMath::Max(
0, PendingTiles.Num() - ActiveTasks - HeldCompletedResults);
const int32 MaxTasks = GetMaxConcurrentTasks();
const int32 AdmissionOccupancy = GetGenerationAdmissionCount();
const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed);
const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles
? TotalWorkerCycles - LastStreamingSchedulerWorkerCycles : 0;
const double IntervalStart = LastStreamingSchedulerSampleSeconds > 0.0
? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds;
const double IntervalSeconds = FMath::Max(0.0, Now - IntervalStart);
const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta);
VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_skip"), FString::Printf(
TEXT("\"frame\":%llu,\"reason\":\"%s\",\"desired_count\":%d,"
"\"desired_missing\":%d,\"pending_tiles\":%d,\"admission_occupancy\":%d,"
"\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d,"
"\"task_cap\":%d,\"legacy_pending_result_cap\":%d,"
"\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%s"),
static_cast<unsigned long long>(GFrameCounter), Reason, DesiredSorted.Num(),
DesiredMissing, PendingTiles.Num(), AdmissionOccupancy, ActiveTasks,
ActiveGenerationWorkerCount.load(std::memory_order_relaxed),
PendingResultsEstimate, MaxTasks, GVoxelForgeStreamingLegacyPendingResultCap,
*FString::SanitizeFloat(BusySeconds),
*FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0)));
LastStreamingSchedulerSampleSeconds = Now;
LastStreamingSchedulerWorkerCycles = TotalWorkerCycles;
};
if (bUseVoxelForgeCore)
{
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
if (!State.IsValid() || State->bInitialFailed.load(std::memory_order_acquire)
|| !State->bReady.load(std::memory_order_acquire))
{
RecordSchedulerSkip(TEXT("core_not_ready"));
return; // Core mode never falls through to the legacy generator.
}
if (State->bRebuildDesired.exchange(false, std::memory_order_acq_rel))
@@ -11004,6 +11067,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
else
{
StartPendingCoreRegionCompile();
RecordSchedulerSkip(TEXT("core_region_compile_pending"));
return;
}
}
@@ -11058,6 +11122,19 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
: ((bPawnMoving || !bWalkableRingReadyForReserve) ? AuthoredNearTaskReserve : 0);
const int32 NearTaskReserve = EffectiveNearTaskReserve;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve);
const int32 FirstCoarseLevel = FMath::Max(
2, Settings ? Settings->FullResClipLevels : 2);
const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0
&& bWalkableRingReadyForReserve;
int32 SchedulerSubmittedByLevel[9] = {};
int32 SchedulerLoadAttemptsByLevel[9] = {};
int32 SchedulerLevelCapReturns[9] = {};
int32 SchedulerLevelCapBreaks[9] = {};
int32 SchedulerCriticalFullCapBreaks = 0;
int32 SchedulerDirtyFullCapBreaks = 0;
int32 SchedulerBandCoarseCapBreaks = 0;
int32 SchedulerOrderStopLevel = -1;
int32 SchedulerOrderStopCap = 0;
if (GVoxelForgeProfileTileGeneration != 0
&& !GVoxelForgeStreamingBudgetReported)
{
@@ -11070,7 +11147,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
"step1_generation_cap=%d step1_max_mesh_applies=%d "
"near_task_reserve=%d dynamic_reserve=%d moving=%d walkable_ring_ready=%d "
"near_task_reserve=%d dynamic_reserve=%d legacy_result_cap=%d "
"moving=%d walkable_ring_ready=%d "
"coarse_task_cap=%d coarse_first=%d "
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
LogicalCores,
@@ -11082,6 +11160,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
GVoxelForgeStep1MaxMeshApplies,
NearTaskReserve,
GVoxelForgeStreamingDynamicNearReserve,
GVoxelForgeStreamingLegacyPendingResultCap,
bPawnMoving ? 1 : 0,
bWalkableRingReadyForReserve ? 1 : 0,
CoarseTaskCap,
@@ -11126,7 +11205,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// Rebuild the desired tile set when the player crosses a level-0 tile boundary — or when a
// streaming anchor moved / (un)registered (bAnchorsMoved / bForceDesiredRebuild).
//=========================================================================
if (CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild)
const bool bDesiredRebuiltThisUpdate =
CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild;
if (bDesiredRebuiltThisUpdate)
{
LastUpdateCenter = CenterChunk;
bAllChunksLoaded = false;
@@ -11375,10 +11456,26 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// Coarse work is capped below the full task budget so later near requests keep admission.
// Le travail grossier reste sous le budget total pour reserver des admissions aux tuiles proches.
//=========================================================================
if (!bAllChunksLoaded)
const bool bSubmissionLoopRan = !bAllChunksLoaded;
if (bSubmissionLoopRan)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles);
int32 Submitted = 0;
const auto SubmitTileWithTraceCount = [&](const FVoxelTileKey& Tile,
EVoxelTileWorkPriority Priority)
{
if (Tile.Level >= 0 && Tile.Level < 9)
{
++SchedulerLoadAttemptsByLevel[Tile.Level];
}
const uint64 OrdinalBefore = NextStreamingRequestOrdinal;
LoadTile(Tile, Priority);
if (NextStreamingRequestOrdinal != OrdinalBefore
&& Tile.Level >= 0 && Tile.Level < 9)
{
++SchedulerSubmittedByLevel[Tile.Level];
}
};
// FLOOR RESPONSIVENESS — the support tile and collision-required 3x3 XY ring lead ordinary work.
// PRIORITE AU SOL — la tuile de support et l'anneau XY 3x3 requis pour la collision passent avant le reste.
@@ -11386,9 +11483,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// Ils passent en BackgroundHigh afin que le sol du pion obtienne les premiers workers disponibles.
for (const FVoxelTileKey& T : CriticalDesiredTiles)
{
if (GetActiveGenerationTaskCount() >= MaxTasks) break;
if (GetGenerationAdmissionCount() >= MaxTasks)
{
++SchedulerCriticalFullCapBreaks;
break;
}
if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue;
LoadTile(T, EVoxelTileWorkPriority::CollisionCritical);
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::CollisionCritical);
++Submitted;
}
@@ -11396,12 +11497,16 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// still in flight is KEPT queued (retried next frame) so its stale pre-carve result is corrected.
for (auto It = DirtyRemeshQueue.CreateIterator(); It; ++It)
{
if (GetActiveGenerationTaskCount() >= MaxTasks) break;
if (GetGenerationAdmissionCount() >= MaxTasks)
{
++SchedulerDirtyFullCapBreaks;
break;
}
const FVoxelTileKey T = *It;
if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
It.RemoveCurrent();
LoadTile(T, EVoxelTileWorkPriority::Edited);
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Edited);
++Submitted;
}
@@ -11416,8 +11521,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
auto SubmitLevel = [&](int32 Level, EVoxelTileWorkPriority Priority, int32 AdmissionCap,
bool bPreferTravelDirection)
{
if (GetActiveGenerationTaskCount() >= AdmissionCap)
if (GetGenerationAdmissionCount() >= AdmissionCap)
{
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapReturns[Level]; }
return;
}
TArray<FVoxelTileKey> LevelTiles;
@@ -11467,10 +11573,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
}
for (const FVoxelTileKey& T : LevelTiles)
{
if (GetActiveGenerationTaskCount() >= AdmissionCap) break;
if (GetGenerationAdmissionCount() >= AdmissionCap)
{
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; }
break;
}
if (PendingTiles.Contains(T)) continue;
if (LoadedTiles.Contains(T)) continue;
LoadTile(T, Priority);
SubmitTileWithTraceCount(T, Priority);
++Submitted;
}
};
@@ -11483,12 +11593,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// Keep the walkable ring on the legacy near-first path; only spend its reserved task slots
// on coarse work after all nine collision gates are ready.
// Garder l'anneau praticable en priorite ; les taches grossieres commencent apres ses neuf collisions.
const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0
&& bWalkableRingReadyForReserve;
if (bUseCoarseFirstOrder)
{
const int32 FirstCoarseLevel = FMath::Max(
2, Settings ? Settings->FullResClipLevels : 2);
for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level)
{
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
@@ -11508,7 +11614,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
{
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
CoarseTaskCap, false);
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break;
if (GetGenerationAdmissionCount() >= CoarseTaskCap)
{
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; }
SchedulerOrderStopLevel = Level;
SchedulerOrderStopCap = CoarseTaskCap;
break;
}
}
}
if (bMeasureReadyTransitionVisibility)
@@ -11523,11 +11635,15 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks).
for (auto It = BandRemeshQueue.CreateIterator(); It; ++It)
{
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break;
if (GetGenerationAdmissionCount() >= CoarseTaskCap)
{
++SchedulerBandCoarseCapBreaks;
break;
}
const FVoxelTileKey T = *It;
It.RemoveCurrent();
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
LoadTile(T);
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Horizon);
++Submitted;
}
@@ -11540,7 +11656,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
}
if (GVoxelForgeProfileTileGeneration != 0
&& GetActiveGenerationTaskCount() >= CoarseTaskCap
&& GetGenerationAdmissionCount() >= CoarseTaskCap
&& !GVoxelForgeGenerationCapHitReported)
{
UE_LOG(LogTemp, Display,
@@ -11604,8 +11720,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d,"
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"pending_by_lod\":%s,"
"\"active_generation\":%d,\"task_cap\":%d,\"near_reserve\":%d,"
"\"coarse_first\":%d,\"dynamic_near_reserve\":%d,\"epoch\":%u,"
"\"active_generation\":%d,\"admission_occupancy\":%d,"
"\"task_cap\":%d,\"near_reserve\":%d,"
"\"coarse_first\":%d,\"dynamic_near_reserve\":%d,"
"\"legacy_pending_result_cap\":%d,\"epoch\":%u,"
"\"update_chunks_ms\":%s,\"desired_rebuild_ms\":%s"),
CenterChunk.X, CenterChunk.Y, CenterChunk.Z,
*FString::SanitizeFloat(LocalPawn.X), *FString::SanitizeFloat(LocalPawn.Y),
@@ -11613,13 +11731,177 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
*FString::SanitizeFloat(PlayerHeading.Size2D()),
bWalkableRingReadyForReserve ? 1 : 0,
*DesiredCounts, *ResidentCounts, *PendingCounts,
GetActiveGenerationTaskCount(), MaxTasks, NearTaskReserve,
GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve,
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
GVoxelForgeStreamingLegacyPendingResultCap,
GenerationEpoch,
*FString::SanitizeFloat(StreamingUpdateMilliseconds),
*FString::SanitizeFloat(DesiredRebuildMilliseconds)));
LastStreamingTraceSampleSeconds = TraceNow;
}
// Mesurer chaque decision d'admission avec l'etat du meme tick, y compris les resultats en attente.
// Capture each admission decision from this tick, including completed results still awaiting apply.
int32 DesiredMissingByLevel[9] = {};
int32 LoadedByLevel[9] = {};
int32 PendingByLevel[9] = {};
int32 AdmissionCapByLevel[9] = {};
int32 DeclinedRuleByLevel[9] = {};
for (const FVoxelTileKey& Tile : DesiredSorted)
{
if (Tile.Level < 0 || Tile.Level > 8) continue;
if (PendingTiles.Contains(Tile))
{
++PendingByLevel[Tile.Level];
}
else if (LoadedTiles.Contains(Tile))
{
++LoadedByLevel[Tile.Level];
}
else
{
++DesiredMissingByLevel[Tile.Level];
}
}
const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0
? Step1HeldApplyTiles.Num() : 0;
const int32 AdmissionOccupancy = GetGenerationAdmissionCount();
const int32 ActiveTaskGuard = ActiveTaskCount.load(std::memory_order_relaxed);
const int32 PendingResultsEstimate = FMath::Max(
0, PendingTiles.Num() - ActiveTaskGuard - HeldCompletedResults);
int32 FullCapDeclinedTiles = 0;
int32 ReserveDeclinedTiles = 0;
int32 InnerLoadDeclinedTiles = 0;
for (int32 Level = 0; Level < 9; ++Level)
{
AdmissionCapByLevel[Level] = bUseCoarseFirstOrder
? (Level >= FirstCoarseLevel ? CoarseTaskCap : MaxTasks)
: (Level == 0 ? MaxTasks : CoarseTaskCap);
if (DesiredMissingByLevel[Level] <= 0) continue;
if (bAllChunksLoaded)
{
DeclinedRuleByLevel[Level] = 3; // Loaded latch while unadmitted desired keys remain.
// Verrou charge alors que des cles desirees restent non admises.
}
else if (SchedulerLevelCapReturns[Level] > 0
|| SchedulerLevelCapBreaks[Level] > 0
|| (SchedulerOrderStopLevel >= 0 && Level >= SchedulerOrderStopLevel))
{
const int32 DeclinedAtCap = AdmissionCapByLevel[Level] > 0
? AdmissionCapByLevel[Level] : SchedulerOrderStopCap;
if (DeclinedAtCap < MaxTasks)
{
DeclinedRuleByLevel[Level] = 2; // Near reserve reached during this submit pass.
// Reserve proche atteinte pendant cette passe d'admission.
ReserveDeclinedTiles += DesiredMissingByLevel[Level];
}
else
{
DeclinedRuleByLevel[Level] = 1; // Full task cap reached during this submit pass.
// Capacite totale des taches atteinte pendant cette passe.
FullCapDeclinedTiles += DesiredMissingByLevel[Level];
}
}
else if (AdmissionOccupancy >= MaxTasks)
{
DeclinedRuleByLevel[Level] = 1; // Full task admission cap.
// Capacite totale d'admission des taches atteinte.
FullCapDeclinedTiles += DesiredMissingByLevel[Level];
}
else if (AdmissionOccupancy >= AdmissionCapByLevel[Level]
&& AdmissionCapByLevel[Level] < MaxTasks)
{
DeclinedRuleByLevel[Level] = 2; // Authored near reserve held back this LOD.
// La reserve proche configuree retient ce niveau de detail.
ReserveDeclinedTiles += DesiredMissingByLevel[Level];
}
else if (SchedulerLoadAttemptsByLevel[Level] > SchedulerSubmittedByLevel[Level])
{
DeclinedRuleByLevel[Level] = 4; // LoadTile/LoadCoreTile declined despite a scheduler attempt.
// LoadTile/LoadCoreTile a refuse la tuile apres la tentative du scheduler.
InnerLoadDeclinedTiles += DesiredMissingByLevel[Level];
}
else
{
DeclinedRuleByLevel[Level] = 5; // Candidate remains with apparent admission capacity.
// La tuile desiree reste manquante avec une capacite d'admission apparente.
}
}
const auto IntArrayJson = [](const int32 Values[9])
{
FString Json = TEXT("[");
for (int32 Index = 0; Index < 9; ++Index)
{
if (Index > 0) Json += TEXT(",");
Json += FString::FromInt(Values[Index]);
}
Json += TEXT("]");
return Json;
};
const double SchedulerTraceNow = FPlatformTime::Seconds();
const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed);
const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles
? TotalWorkerCycles - LastStreamingSchedulerWorkerCycles : 0;
const double IntervalStart = LastStreamingSchedulerSampleSeconds > 0.0
? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds;
const double IntervalSeconds = FMath::Max(0.0, SchedulerTraceNow - IntervalStart);
const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta);
VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_admission"), FString::Printf(
TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"desired_rebuilt\":%d,"
"\"submission_loop_ran\":%d,\"all_chunks_loaded\":%d,"
"\"walkable_ready\":%d,\"moving\":%d,"
"\"core_region_compile_pending\":%d,\"core_ready\":%d,\"coarse_first\":%d,"
"\"dynamic_near_reserve\":%d,\"legacy_pending_result_cap\":%d,"
"\"max_tasks\":%d,\"near_reserve\":%d,"
"\"coarse_cap\":%d,\"first_coarse_level\":%d,"
"\"submission_stop_level\":%d,\"submission_stop_cap\":%d,"
"\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d,"
"\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d,"
"\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%s,"
"\"desired_missing_by_lod\":%s,\"loaded_by_lod\":%s,\"pending_by_lod\":%s,"
"\"submitted_by_lod\":%s,\"load_attempts_by_lod\":%s,"
"\"admission_cap_by_lod\":%s,\"declined_rule_by_lod\":%s,"
"\"level_cap_returns_by_lod\":%s,\"level_cap_breaks_by_lod\":%s,"
"\"full_cap_declined_tiles\":%d,\"reserve_declined_tiles\":%d,"
"\"inner_load_declined_tiles\":%d,\"critical_full_cap_breaks\":%d,"
"\"dirty_full_cap_breaks\":%d,\"band_coarse_cap_breaks\":%d"),
static_cast<unsigned long long>(GFrameCounter), CenterChunk.X, CenterChunk.Y, CenterChunk.Z,
bDesiredRebuiltThisUpdate ? 1 : 0, bSubmissionLoopRan ? 1 : 0, bAllChunksLoaded ? 1 : 0,
bWalkableRingReadyForReserve ? 1 : 0, bPawnMoving ? 1 : 0,
bCoreRegionCompilePending ? 1 : 0, IsVoxelForgeCoreReady() ? 1 : 0,
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
GVoxelForgeStreamingLegacyPendingResultCap, MaxTasks,
NearTaskReserve, CoarseTaskCap, FirstCoarseLevel,
SchedulerOrderStopLevel, SchedulerOrderStopCap,
PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard,
ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate,
*FString::SanitizeFloat(BusySeconds),
*FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0),
*IntArrayJson(DesiredMissingByLevel), *IntArrayJson(LoadedByLevel),
*IntArrayJson(PendingByLevel), *IntArrayJson(SchedulerSubmittedByLevel),
*IntArrayJson(SchedulerLoadAttemptsByLevel), *IntArrayJson(AdmissionCapByLevel),
*IntArrayJson(DeclinedRuleByLevel), *IntArrayJson(SchedulerLevelCapReturns),
*IntArrayJson(SchedulerLevelCapBreaks), FullCapDeclinedTiles, ReserveDeclinedTiles,
InnerLoadDeclinedTiles, SchedulerCriticalFullCapBreaks,
SchedulerDirtyFullCapBreaks, SchedulerBandCoarseCapBreaks));
LastStreamingSchedulerSampleSeconds = SchedulerTraceNow;
LastStreamingSchedulerWorkerCycles = TotalWorkerCycles;
}
else if (bTraceStreamingScheduler)
{
// Report a loaded latch only if desired work is still unadmitted; the normal no-missing path stays quiet.
// Signaler le verrou charge seulement s'il reste du travail desire non admis.
for (const FVoxelTileKey& Tile : DesiredSorted)
{
if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile))
{
RecordSchedulerSkip(TEXT("all_chunks_loaded_latch"));
break;
}
}
}
}
@@ -11641,7 +11923,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
? CoarseTaskCap : MaxTasks;
if (GetActiveGenerationTaskCount() >= AdmissionCap)
if (GetGenerationAdmissionCount() >= AdmissionCap)
{
if (GVoxelForgeProfileTileGeneration != 0
&& !GVoxelForgeGenerationCapHitReported)
@@ -11812,8 +12094,18 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
struct FWorkerGuard
{
std::atomic<int32>& Counter;
~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
} WorkerGuard{ActiveGenerationWorkerCount};
std::atomic<uint64>& BusyCycles;
uint64 StartedCycles;
~FWorkerGuard()
{
const uint64 EndedCycles = FPlatformTime::Cycles64();
if (EndedCycles >= StartedCycles)
{
BusyCycles.fetch_add(EndedCycles - StartedCycles, std::memory_order_relaxed);
}
Counter.fetch_sub(1, std::memory_order_relaxed);
}
} WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles};
FChunkResult Result;
Result.WorkerTaskStartCycles = WorkerTaskStartCycles;
@@ -11942,7 +12234,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
? CoarseTaskCap : MaxTasks;
if (GetActiveGenerationTaskCount() >= AdmissionCap) return;
if (GetGenerationAdmissionCount() >= AdmissionCap) return;
PendingTiles.Add(Tile);
const TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe> Cancellation =
@@ -11987,8 +12279,18 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
struct FWorkerGuard
{
std::atomic<int32>& Counter;
~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
} WorkerGuard{ActiveGenerationWorkerCount};
std::atomic<uint64>& BusyCycles;
uint64 StartedCycles;
~FWorkerGuard()
{
const uint64 EndedCycles = FPlatformTime::Cycles64();
if (EndedCycles >= StartedCycles)
{
BusyCycles.fetch_add(EndedCycles - StartedCycles, std::memory_order_relaxed);
}
Counter.fetch_sub(1, std::memory_order_relaxed);
}
} WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles};
auto Enqueue = [this, WorkPriority](FChunkResult&& Result)
{
+15 -2
View File
@@ -443,7 +443,12 @@ 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;
/** Submitted generation tasks still queued or running; completed MPSC results do not occupy admission.
* Taches de generation soumises, en file ou en cours ; les resultats MPSC termines ne bloquent pas l'admission. */
int32 GetActiveGenerationTaskCount() const;
/** Count queued/running work for admission; the legacy CVar can include finished results awaiting apply.
* Compter le travail en file/en cours ; le CVar de compatibilite peut inclure les resultats finis en attente. */
int32 GetGenerationAdmissionCount() const;
/** Reserve task slots for collision-critical/edit/LOD0 work; far work uses the remainder. */
int32 GetNearTaskReserve(int32 MaxTasks) const;
@@ -1093,7 +1098,9 @@ public:
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
// Keys stay pending from task submission until the game thread applies the result; completed results can wait here.
// Les cles restent en attente de la soumission jusqu'a l'application sur le thread de jeu ; les resultats termines y attendent.
TSet<FVoxelTileKey> PendingTiles;
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
TMap<FVoxelTileKey, TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>>
PendingTileCancellation;
@@ -1121,7 +1128,8 @@ public:
// Active pendant les mutations de génération lancées par l'éditeur, sans signifier la destruction.
std::atomic<bool> bGenerationPaused{false};
// Number of async tasks currently running — EndPlay waits for this to reach 0
// Submitted async tasks (including UE-queued work) — EndPlay waits for this to reach 0.
// Taches asynchrones soumises (y compris la file UE) — EndPlay attend que le compteur revienne a 0.
std::atomic<int32> ActiveTaskCount{0};
// Active workers are counted inside the launched tile task, distinct from tasks queued by UE.
// Les travailleurs actifs sont comptes dans la tache de tuile, distincts des taches en attente UE.
@@ -1199,6 +1207,11 @@ public:
int32 EffectiveNearTaskReserve = -1;
uint64 NextStreamingRequestOrdinal = 0;
std::atomic<uint64> NextStreamingGenerationOrdinal{0};
// Cumulative worker-task wall cycles avoid aliasing the instantaneous per-frame counter.
// Les cycles cumules des taches evitent l'aliasing du compteur instantane echantillonne par frame.
std::atomic<uint64> TotalGenerationWorkerTaskCycles{0};
double LastStreamingSchedulerSampleSeconds = 0.0;
uint64 LastStreamingSchedulerWorkerCycles = 0;
struct FReadyTransitionVisibilitySample
{