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:
1 parent
b9c13c37c0
commit
5eb9ee76ff
7 files changed
+726
-53
No files matched your search
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user