wip(assembler): round S12 -- short ready queue keeps workers fed between ticks: full set 6.0 s -> 4.9 s

voxel.StreamingReadyQueueDepth=8 admits one extra worker-set of prioritized
tasks; voxel.StreamingBackgroundWorkerCap=1 sizes slots to the 8 TaskGraph
background workers actually running tile tasks. Full set 5.96 -> 4.91 s,
LOD0 1.40 -> 1.09 s, ring 0.49 -> 0.51 s, 40 m/s ring 75/75. Tick p95
+0.6 ms spawn / +0.26 ms walk, max flat. No geometry change.
Report: Standalone/Research/WorldAssemblerM3/Round16.

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 14:29:38 +02:00
1 parent dd9bfd3217
commit 212814a75c
8 files changed
+554 -36

No files matched your search

@@ -284,9 +284,17 @@ void RecordTile(const FTileSample& Sample)
GState->Samples.Add(Sample);
++GState->TileRecordsWritten;
const double TraceNowSeconds = FPlatformTime::Seconds() - GState->StartSeconds;
const double SubmittedAtSeconds = Sample.RequestStartCycles != 0
? TraceNowSeconds - Sample.RequestToApplySeconds : -1.0;
const double WorkerStartedAtSeconds = Sample.WorkerTaskStartCycles != 0
? SubmittedAtSeconds + Sample.WorkerQueueSeconds : -1.0;
const double WorkerEndedAtSeconds = Sample.WorkerTaskEndCycles != 0
? WorkerStartedAtSeconds + Sample.WorkerTaskSeconds : -1.0;
WriteEventLocked(*GState, TEXT("tile"), FString::Printf(
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
"\"request_ordinal\":%llu,\"generation_ordinal\":%llu,\"resident_at_request\":%d,"
"\"submitted_t_s\":%s,\"worker_start_t_s\":%s,\"worker_end_t_s\":%s,\"worker_thread_id\":%u,"
"\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,\"worker_task_s\":%s,"
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
@@ -295,6 +303,10 @@ void RecordTile(const FTileSample& Sample)
static_cast<unsigned long long>(Sample.RequestOrdinal),
static_cast<unsigned long long>(Sample.GenerationOrdinal),
Sample.bResidentAtRequest ? 1 : 0,
*Number(SubmittedAtSeconds),
*Number(WorkerStartedAtSeconds),
*Number(WorkerEndedAtSeconds),
Sample.WorkerThreadId,
Sample.Verdict, Sample.bSealedSolidProof ? 1 : 0,
Sample.bEmpty ? 1 : 0, Sample.Vertices, Sample.Triangles,
static_cast<unsigned long long>(Sample.GeometryBytes),
+155 -36
View File
@@ -53,6 +53,8 @@
#include "Engine/GameViewportClient.h"
#include "Slate/SceneViewport.h"
#include "Camera/CameraActor.h"
#include "Async/Fundamental/Scheduler.h"
#include "Async/TaskGraphInterfaces.h"
#include "Components/PointLightComponent.h"
#include "VoxelAtmosphereManager.h"
#include "DrawDebugHelpers.h"
@@ -221,6 +223,18 @@ namespace
GVoxelForgeStreamingResultBacklogCap,
TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap."));
int32 GVoxelForgeStreamingReadyQueueDepth = 8;
FAutoConsoleVariableRef CVarVoxelForgeStreamingReadyQueueDepth(
TEXT("voxel.StreamingReadyQueueDepth"),
GVoxelForgeStreamingReadyQueueDepth,
TEXT("Extra admitted tile tasks kept ready beyond the worker slots, up to one worker-set; 0 disables the refill queue."));
int32 GVoxelForgeStreamingBackgroundWorkerCap = 1;
FAutoConsoleVariableRef CVarVoxelForgeStreamingBackgroundWorkerCap(
TEXT("voxel.StreamingBackgroundWorkerCap"),
GVoxelForgeStreamingBackgroundWorkerCap,
TEXT("Size tile admission slots to the TaskGraph background workers used by tile tasks; 0 restores the logical-core cap."));
int32 GVoxelForgeProfileTileGeneration = 0;
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
TEXT("voxel.ProfileTileGeneration"),
@@ -2064,6 +2078,8 @@ void AVoxelWorld::BeginPlay()
int32 CommandLineCoreRegionConcurrentAdmission = GVoxelForgeCoreRegionConcurrentAdmission;
int32 CommandLineStreamingCollisionLookahead = GVoxelForgeStreamingCollisionLookahead;
int32 CommandLineStreamingResultBacklogCap = GVoxelForgeStreamingResultBacklogCap;
int32 CommandLineStreamingReadyQueueDepth = GVoxelForgeStreamingReadyQueueDepth;
int32 CommandLineStreamingBackgroundWorkerCap = GVoxelForgeStreamingBackgroundWorkerCap;
int32 CommandLineStep1GenerationCap = GVoxelForgeStep1GenerationCap;
int32 CommandLineStep1ExitAfterFrames = GVoxelForgeStep1ExitAfterFrames;
int32 CommandLineStep1CsvCaptureOnBeginPlay = GVoxelForgeStep1CsvCaptureOnBeginPlay;
@@ -2158,6 +2174,18 @@ void AVoxelWorld::BeginPlay()
{
GVoxelForgeStreamingResultBacklogCap = FMath::Max(0, CommandLineStreamingResultBacklogCap);
}
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingReadyQueueDepth="),
CommandLineStreamingReadyQueueDepth))
{
GVoxelForgeStreamingReadyQueueDepth = FMath::Clamp(
CommandLineStreamingReadyQueueDepth, 0, 32);
}
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingBackgroundWorkerCap="),
CommandLineStreamingBackgroundWorkerCap))
{
GVoxelForgeStreamingBackgroundWorkerCap =
CommandLineStreamingBackgroundWorkerCap != 0 ? 1 : 0;
}
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="),
CommandLineStep1PrequeueVisibleLOD0))
{
@@ -2560,6 +2588,48 @@ void AVoxelWorld::BeginPlay()
Settings->bGenerateSkirts ? 1 : 0,
Settings->FullResClipLevels, Settings->CoarseTileCells));
if (VoxelForgeStartupTrace::IsActive())
{
FTaskGraphInterface& TaskGraph = FTaskGraphInterface::Get();
LowLevelTasks::FScheduler& TaskScheduler = LowLevelTasks::FScheduler::Get();
const int32 QueueDepth = FMath::Clamp(
GVoxelForgeStreamingReadyQueueDepth, 0, GetMaxConcurrentStreamingTasks());
VoxelForgeStartupTrace::RecordEvent(TEXT("worker_backend_config"), FString::Printf(
TEXT("\"logical_cores\":%d,\"effective_tile_slots\":%d,"
"\"task_api\":\"UE::Tasks::Launch\",\"task_scheduler_workers\":%u,"
"\"task_scheduler_max_workers\":%u,\"task_scheduler_worker_priority\":%d,"
"\"task_scheduler_background_priority\":%d,"
"\"taskgraph_worker_threads_per_priority\":%d,\"taskgraph_foreground_threads\":%d,"
"\"taskgraph_background_threads\":%d,\"global_queued_thread_pool_threads\":%d,"
"\"step1_cook_pool_threads\":%d,\"ready_queue_depth_cvar\":%d,"
"\"ready_queue_depth\":%d,"
"\"near_priority\":\"BackgroundHigh\","
"\"horizon_priority\":\"BackgroundNormal\","
"\"streaming_coarse_first\":%d,\"streaming_dynamic_near_reserve\":%d,"
"\"streaming_legacy_pending_result_cap\":%d,"
"\"streaming_collision_lookahead\":%d,\"core_region_concurrent_admission\":%d,"
"\"collision_gate\":%d,"
"\"streaming_result_backlog_cap\":%d,\"streaming_startup_apply_boost\":%d"),
FPlatformMisc::NumberOfCoresIncludingHyperthreads(), GetMaxConcurrentStreamingTasks(),
TaskScheduler.GetNumWorkers(), TaskScheduler.GetMaxNumWorkers(),
static_cast<int32>(TaskScheduler.GetWorkerPriority()),
static_cast<int32>(TaskScheduler.GetBackgroundPriority()),
TaskGraph.GetNumWorkerThreads(), TaskGraph.GetNumForegroundThreads(),
TaskGraph.GetNumBackgroundThreads(), GThreadPool ? GThreadPool->GetNumThreads() : 0,
GVoxelForgeStep1CoreCookPool.Pool ? GVoxelForgeStep1CoreCookPool.Pool->GetNumThreads() : 0,
GVoxelForgeStreamingReadyQueueDepth, QueueDepth,
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
GVoxelForgeStreamingLegacyPendingResultCap,
GVoxelForgeStreamingCollisionLookahead, GVoxelForgeCoreRegionConcurrentAdmission,
GVoxelForgeCollisionGate,
GVoxelForgeStreamingResultBacklogCap, GVoxelForgeStreamingStartupApplyBoost));
VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_worker_cap_readback"), FString::Printf(
TEXT("\"streaming_background_worker_cap\":%d,\"background_worker_threads\":%d,"
"\"effective_tile_slots\":%d"),
GVoxelForgeStreamingBackgroundWorkerCap,
TaskGraph.GetNumBackgroundThreads(), GetMaxConcurrentStreamingTasks()));
}
// Système de strates — a cooked season takes precedence; otherwise the authored pool path is
// exactly the legacy one.
if (!Settings->Season.IsNull() || Settings->StratePool.Num() > 0
@@ -9779,6 +9849,10 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
Sample.WorkPriority = static_cast<int32>(Result.WorkPriority);
Sample.RequestOrdinal = Result.RequestOrdinal;
Sample.GenerationOrdinal = Result.GenerationOrdinal;
Sample.RequestStartCycles = Result.RequestStartCycles;
Sample.WorkerTaskStartCycles = Result.WorkerTaskStartCycles;
Sample.WorkerTaskEndCycles = Result.WorkerTaskEndCycles;
Sample.WorkerThreadId = Result.WorkerThreadId;
Sample.bResidentAtRequest = Result.bResidentAtRequest;
Sample.Verdict = Result.ClassifyVerdict;
Sample.bSealedSolidProof = Result.bSealedSolidProof;
@@ -11349,8 +11423,8 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
int32 AVoxelWorld::GetMaxConcurrentTasks() const
{
// T2.d — both sides of the cap are authored in Voxel|Streaming. BackgroundNormal priority
// (see LoadTile) already stops gen from starving the frame; this cap stops a flat task count
// from thrashing context switches on small CPUs where it exceeds spare parallelism.
// (see LoadTile) yields to frame work; this cap avoids oversubscribing available workers.
// La priorite BackgroundNormal cede la place au frame ; ce plafond evite de depasser les workers disponibles.
const int32 Asset = Settings ? Settings->MaxConcurrentTasks : 16;
const int32 LogicalCores = FMath::Max(
1, FPlatformMisc::NumberOfCoresIncludingHyperthreads());
@@ -11369,6 +11443,17 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
: NormalCap;
}
int32 AVoxelWorld::GetMaxConcurrentStreamingTasks() const
{
const int32 TaskCap = GetMaxConcurrentTasks();
if (TaskCap <= 0 || GVoxelForgeStreamingBackgroundWorkerCap == 0)
{
return TaskCap;
}
const int32 BackgroundWorkers = FTaskGraphInterface::Get().GetNumBackgroundThreads();
return BackgroundWorkers > 0 ? FMath::Min(TaskCap, BackgroundWorkers) : TaskCap;
}
int32 AVoxelWorld::GetActiveGenerationTaskCount() const
{
return FMath::Max(0, ActiveTaskCount.load(std::memory_order_relaxed));
@@ -11509,7 +11594,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
? Step1HeldApplyTiles.Num() : 0;
const int32 PendingResultsEstimate = FMath::Max(
0, PendingTiles.Num() - ActiveTasks - HeldCompletedResults);
const int32 MaxTasks = GetMaxConcurrentTasks();
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
const int32 AdmissionOccupancy = GetGenerationAdmissionCount();
const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed);
const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles
@@ -11684,7 +11769,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
&& GVoxelForgeCoreRegionConcurrentAdmission == 0;
}
}
const int32 MaxTasks = GetMaxConcurrentTasks();
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
// Keep at most one worker-set of ready work so an admission pass can feed idle slots
// without letting far work consume the near reserve. / Garder une file d'un ensemble
// de workers pour nourrir les slots sans consommer la reserve proche.
const int32 ReadyQueueDepth = FMath::Clamp(
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks);
const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition));
FVoxelTileKey WalkableSupportTile;
@@ -11741,11 +11832,18 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
return;
}
const bool bPawnMoving = PlayerHeading.Size() >= 50.0f;
bStreamingPlayerMoving = bPawnMoving;
// Match the near reserve to the added far queue so queued horizon tiles cannot spend that headroom.
// Aligner la reserve proche sur la file lointaine pour que l'horizon ne consomme pas cette marge.
const int32 QueueAwareNearTaskReserve = FMath::Clamp(
FMath::Max(AuthoredNearTaskReserve, ReadyQueueDepth), 0,
FMath::Max(0, MaxTasks - 1));
EffectiveNearTaskReserve = GVoxelForgeStreamingDynamicNearReserve == 0
? AuthoredNearTaskReserve
: ((bPawnMoving || !bWalkableRingReadyForReserve) ? AuthoredNearTaskReserve : 0);
: ((bPawnMoving || !bWalkableRingReadyForReserve) ? QueueAwareNearTaskReserve : 0);
const int32 NearTaskReserve = EffectiveNearTaskReserve;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve);
const int32 NearAdmissionCap = bPawnMoving ? MaxTasks : TotalAdmissionCap;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve) + ReadyQueueDepth;
const int32 FirstCoarseLevel = FMath::Max(
2, Settings ? Settings->FullResClipLevels : 2);
const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0
@@ -11765,11 +11863,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
const int32 LogicalCores = FMath::Max(
1, FPlatformMisc::NumberOfCoresIncludingHyperthreads());
const int32 AutomaticCap = FMath::Max(2, LogicalCores - 2);
const int32 BackgroundWorkerThreads = FTaskGraphInterface::Get().GetNumBackgroundThreads();
const int32 AssetCap = Settings ? Settings->MaxConcurrentTasks : 16;
const int32 AuthoredCoreCap = Settings ? Settings->MaxGenerationWorkerCores : 0;
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 "
"taskgraph_background_workers=%d background_worker_cap=%d "
"ready_queue_depth=%d total_admission_cap=%d "
"step1_generation_cap=%d step1_max_mesh_applies=%d "
"near_task_reserve=%d dynamic_reserve=%d legacy_result_cap=%d "
"moving=%d walkable_ring_ready=%d "
@@ -11780,6 +11881,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
AuthoredCoreCap,
AssetCap,
MaxTasks,
BackgroundWorkerThreads,
GVoxelForgeStreamingBackgroundWorkerCap,
ReadyQueueDepth,
TotalAdmissionCap,
GVoxelForgeStep1GenerationCap,
GVoxelForgeStep1MaxMeshApplies,
NearTaskReserve,
@@ -12110,11 +12215,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// 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.
// They run at BackgroundHigh so the pawn's floor gets the first available worker.
// Ils passent en BackgroundHigh afin que le sol du pion obtienne les premiers workers disponibles.
// They use the foreground task lane when enabled, above ordinary background streaming.
// Ils utilisent la voie foreground lorsqu'elle est active, avant le streaming distant.
for (const FVoxelTileKey& T : CriticalDesiredTiles)
{
if (GetGenerationAdmissionCount() >= MaxTasks)
if (GetGenerationAdmissionCount() >= TotalAdmissionCap)
{
++SchedulerCriticalFullCapBreaks;
break;
@@ -12128,7 +12233,7 @@ 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 (GetGenerationAdmissionCount() >= MaxTasks)
if (GetGenerationAdmissionCount() >= TotalAdmissionCap)
{
++SchedulerDirtyFullCapBreaks;
break;
@@ -12235,12 +12340,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
{
const EVoxelTileWorkPriority Priority = Level == 0
? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon;
SubmitLevel(Level, Priority, MaxTasks, true);
const int32 LevelAdmissionCap = Priority == EVoxelTileWorkPriority::Horizon
? CoarseTaskCap : NearAdmissionCap;
SubmitLevel(Level, Priority, LevelAdmissionCap, true);
}
}
else
{
SubmitLevel(0, EVoxelTileWorkPriority::LOD0, MaxTasks, false);
SubmitLevel(0, EVoxelTileWorkPriority::LOD0, NearAdmissionCap, false);
for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level)
{
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
@@ -12283,8 +12390,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
{
GVoxelForgeMaxPendingTilesObserved = PendingTiles.Num();
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStreamingBudget] pending_max=%d effective=%d submitted=%d"),
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
TEXT("[VoxelForgeStreamingBudget] pending_max=%d effective=%d ready_queue=%d submitted=%d"),
GVoxelForgeMaxPendingTilesObserved, MaxTasks, ReadyQueueDepth, Submitted);
}
if (GVoxelForgeProfileTileGeneration != 0
&& GetGenerationAdmissionCount() >= CoarseTaskCap
@@ -12411,8 +12518,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
for (int32 Level = 0; Level < 9; ++Level)
{
AdmissionCapByLevel[Level] = bUseCoarseFirstOrder
? (Level >= FirstCoarseLevel ? CoarseTaskCap : MaxTasks)
: (Level == 0 ? MaxTasks : CoarseTaskCap);
? (Level >= FirstCoarseLevel || Level > 0 ? CoarseTaskCap : NearAdmissionCap)
: (Level == 0 ? NearAdmissionCap : CoarseTaskCap);
if (DesiredMissingByLevel[Level] <= 0) continue;
if (bAllChunksLoaded)
@@ -12426,7 +12533,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
{
const int32 DeclinedAtCap = AdmissionCapByLevel[Level] > 0
? AdmissionCapByLevel[Level] : SchedulerOrderStopCap;
if (DeclinedAtCap < MaxTasks)
if (DeclinedAtCap < TotalAdmissionCap)
{
DeclinedRuleByLevel[Level] = 2; // Near reserve reached during this submit pass.
// Reserve proche atteinte pendant cette passe d'admission.
@@ -12439,14 +12546,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
FullCapDeclinedTiles += DesiredMissingByLevel[Level];
}
}
else if (AdmissionOccupancy >= MaxTasks)
else if (AdmissionOccupancy >= TotalAdmissionCap)
{
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)
&& AdmissionCapByLevel[Level] < TotalAdmissionCap)
{
DeclinedRuleByLevel[Level] = 2; // Authored near reserve held back this LOD.
// La reserve proche configuree retient ce niveau de detail.
@@ -12497,7 +12604,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
"\"core_region_compile_pending\":%d,\"core_ready\":%d,\"coarse_first\":%d,"
"\"dynamic_near_reserve\":%d,\"legacy_pending_result_cap\":%d,"
"\"core_region_concurrent_admission\":%d,"
"\"max_tasks\":%d,\"near_reserve\":%d,"
"\"max_tasks\":%d,\"ready_queue_depth\":%d,"
"\"total_admission_cap\":%d,\"near_admission_cap\":%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,"
@@ -12520,8 +12629,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
bCoreRegionCompilePending ? 1 : 0, IsVoxelForgeCoreReady() ? 1 : 0,
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
GVoxelForgeStreamingLegacyPendingResultCap,
GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks,
NearTaskReserve, CoarseTaskCap, FirstCoarseLevel,
GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks, ReadyQueueDepth,
TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, CoarseTaskCap, FirstCoarseLevel,
SchedulerOrderStopLevel, SchedulerOrderStopCap,
PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard,
ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate,
@@ -12572,14 +12681,19 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
return;
}
const int32 MaxTasks = GetMaxConcurrentTasks(); // T2.d — core-clamped
const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Core and worker lane clamped. / Core et voie worker plafonnes.
const int32 ReadyQueueDepth = FMath::Clamp(
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
const int32 NearReserve = EffectiveNearTaskReserve >= 0
? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1))
: GetNearTaskReserve(MaxTasks);
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth;
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
? CoarseTaskCap : MaxTasks;
? CoarseTaskCap
: (WorkPriority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : TotalAdmissionCap);
if (GetGenerationAdmissionCount() >= AdmissionCap)
{
if (GVoxelForgeProfileTileGeneration != 0
@@ -12716,14 +12830,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
ActiveTileTaskCount.fetch_add(1, std::memory_order_relaxed);
// 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.
// 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.
// BackgroundNormal yields to foreground work; near tasks stay ahead at BackgroundHigh.
// BackgroundNormal cede la priorite au premier plan ; les tuiles proches restent prioritaires en BackgroundHigh.
const UE::Tasks::ETaskPriority TaskPriority = bNearPriority
? UE::Tasks::ETaskPriority::BackgroundHigh
: UE::Tasks::ETaskPriority::BackgroundNormal;
@@ -12747,6 +12855,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
} Guard{ActiveTaskCount, ActiveTileTaskCount};
ActiveGenerationWorkerCount.fetch_add(1, std::memory_order_relaxed);
const uint64 WorkerTaskStartCycles = FPlatformTime::Cycles64();
const uint32 WorkerThreadId = FPlatformTLS::GetCurrentThreadId();
struct FWorkerGuard
{
std::atomic<int32>& Counter;
@@ -12765,6 +12874,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
FChunkResult Result;
Result.WorkerTaskStartCycles = WorkerTaskStartCycles;
Result.WorkerThreadId = WorkerThreadId;
Result.RequestOrdinal = RequestOrdinal;
Result.bResidentAtRequest = bResidentAtRequest;
Result.WorkPriority = WorkPriority;
@@ -12786,6 +12896,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
Canceled.GenerationOrdinal = Result.GenerationOrdinal;
Canceled.WorkerTaskStartCycles = Result.WorkerTaskStartCycles;
Canceled.WorkerTaskEndCycles = FPlatformTime::Cycles64();
Canceled.WorkerThreadId = Result.WorkerThreadId;
Canceled.GenerationStartCycles = Result.GenerationStartCycles;
Canceled.GenerationEndCycles = FPlatformTime::Cycles64();
Canceled.bResidentAtRequest = bResidentAtRequest;
@@ -12887,14 +12998,19 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
return;
}
const int32 MaxTasks = GetMaxConcurrentTasks();
const int32 MaxTasks = GetMaxConcurrentStreamingTasks();
const int32 ReadyQueueDepth = FMath::Clamp(
GVoxelForgeStreamingReadyQueueDepth, 0, MaxTasks);
const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth;
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
const int32 NearReserve = EffectiveNearTaskReserve >= 0
? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1))
: GetNearTaskReserve(MaxTasks);
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth;
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
? CoarseTaskCap : MaxTasks;
? CoarseTaskCap
: (WorkPriority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : TotalAdmissionCap);
if (GetGenerationAdmissionCount() >= AdmissionCap) return;
PendingTiles.Add(Tile);
@@ -12947,6 +13063,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
} Guard{ActiveTaskCount, ActiveTileTaskCount};
ActiveGenerationWorkerCount.fetch_add(1, std::memory_order_relaxed);
const uint64 WorkerTaskStartCycles = FPlatformTime::Cycles64();
const uint32 WorkerThreadId = FPlatformTLS::GetCurrentThreadId();
struct FWorkerGuard
{
std::atomic<int32>& Counter;
@@ -12978,6 +13095,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
FChunkResult Result;
Result.WorkerTaskStartCycles = WorkerTaskStartCycles;
Result.WorkerThreadId = WorkerThreadId;
Result.Tile = Tile;
Result.Epoch = TaskEpoch;
Result.DesiredEpoch = TaskDesiredEpoch;
@@ -13000,6 +13118,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
Canceled.RequestOrdinal = RequestOrdinal;
Canceled.GenerationOrdinal = Result.GenerationOrdinal;
Canceled.WorkerTaskStartCycles = Result.WorkerTaskStartCycles;
Canceled.WorkerThreadId = Result.WorkerThreadId;
Canceled.bResidentAtRequest = bResidentAtRequest;
Canceled.GenerationStartCycles = Result.GenerationStartCycles;
Canceled.GenerationEndCycles = FPlatformTime::Cycles64();
@@ -17,6 +17,10 @@ namespace VoxelForgeStartupTrace
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (early path)
uint64 RequestOrdinal = 0;
uint64 GenerationOrdinal = 0;
uint64 RequestStartCycles = 0;
uint64 WorkerTaskStartCycles = 0;
uint64 WorkerTaskEndCycles = 0;
uint32 WorkerThreadId = 0;
bool bResidentAtRequest = false;
bool bSealedSolidProof = false;
bool bEmpty = true;
+5
View File
@@ -137,6 +137,9 @@ struct FChunkResult
// Les bornes de tâche incluent la conversion et la préparation du résultat autour du générateur.
uint64 WorkerTaskStartCycles = 0;
uint64 WorkerTaskEndCycles = 0;
// Operating-system thread id identifies the physical worker slot used by this tile.
// L'identifiant du thread systeme repere le slot worker utilise par cette tuile.
uint32 WorkerThreadId = 0;
// Worker timing boundaries for separating scheduler wait from generation and result/apply wait.
// These are captured with low overhead for the clean streaming-latency summary; expensive
// per-tile profiling remains opt-in.
@@ -443,6 +446,7 @@ 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;
int32 GetMaxConcurrentStreamingTasks() 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;
@@ -1239,6 +1243,7 @@ public:
int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count
float PeakObservedPawnSpeedCmPerSecond = 0.0f;
int32 EffectiveNearTaskReserve = -1;
bool bStreamingPlayerMoving = false; // GT snapshot consumed by admission helpers / instantane GT pour les admissions
uint64 NextStreamingRequestOrdinal = 0;
std::atomic<uint64> NextStreamingGenerationOrdinal{0};
// Cumulative worker-task wall cycles avoid aliasing the instantaneous per-frame counter.