wip(assembler): round S6 -- in-game cold view counted; workers mostly idle, apply cap not the floor
Direct per-tile trace on the BuildHost actor (owner plan, 2,578 keys): full set 17.4 s legacy; gated coarse-first (after the 9-tile walkable ring) 11.7 s with the ring still at 0.63 s, but full LOD0 slips 2.3 -> 10.2 s. Startup apply boost changed nothing (0.4%). 37.6 worker-s over 17.4 s on 10 slots = ~3.3 busy: the floor is admission/work supply. S5's 251 ms staircase was the trace sampler. Fixed CVar parsing in headless BeginPlay and the 60 m/s crossing instrument. All three knobs stay opt-in. 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
c0596c5f24
commit
b9c13c37c0
11 files changed
+903
-31
No files matched your search
@@ -20,6 +20,7 @@ namespace
|
||||
int32 TileRecordsSeen = 0;
|
||||
int32 TileRecordsWritten = 0;
|
||||
int32 TileRecordsSuppressed = 0;
|
||||
int32 ApplyFrameRecords = 0;
|
||||
TArray<FTileSample> Samples;
|
||||
TMap<FString, double> StageStarts;
|
||||
TMap<FString, double> StageTotals;
|
||||
@@ -94,7 +95,7 @@ namespace
|
||||
|
||||
FString BuildTileGroupJson(const TArray<FTileSample>& Samples, int32 Level)
|
||||
{
|
||||
TArray<double> Request, Queue, Generation, Classify, Mesh, Apply;
|
||||
TArray<double> Request, Queue, WorkerTask, Generation, Classify, Mesh, Apply;
|
||||
int32 Count = 0;
|
||||
int32 Empty = 0;
|
||||
int32 AllAir = 0;
|
||||
@@ -117,6 +118,7 @@ namespace
|
||||
GeometryBytes += Sample.GeometryBytes;
|
||||
Request.Add(Sample.RequestToApplySeconds);
|
||||
Queue.Add(Sample.QueueWaitSeconds);
|
||||
WorkerTask.Add(Sample.WorkerTaskSeconds);
|
||||
Generation.Add(Sample.GenerationSeconds);
|
||||
Classify.Add(Sample.ClassifySeconds);
|
||||
Mesh.Add(Sample.MeshSeconds);
|
||||
@@ -127,6 +129,7 @@ namespace
|
||||
TEXT("{\"count\":%d,\"empty\":%d,\"all_air\":%d,\"all_solid\":%d,\"mixed\":%d,\"vertices\":%lld,\"triangles\":%lld,\"geometry_bytes\":%llu,"
|
||||
"\"request_apply_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"queue_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"worker_task_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"generation_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"classify_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"mesh_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
@@ -136,6 +139,7 @@ namespace
|
||||
static_cast<unsigned long long>(GeometryBytes),
|
||||
*Number(Percentile(Request, 0.50)), *Number(Percentile(Request, 0.95)), *Number(Maximum(Request)),
|
||||
*Number(Percentile(Queue, 0.50)), *Number(Percentile(Queue, 0.95)), *Number(Maximum(Queue)),
|
||||
*Number(Percentile(WorkerTask, 0.50)), *Number(Percentile(WorkerTask, 0.95)), *Number(Maximum(WorkerTask)),
|
||||
*Number(Percentile(Generation, 0.50)), *Number(Percentile(Generation, 0.95)), *Number(Maximum(Generation)),
|
||||
*Number(Percentile(Classify, 0.50)), *Number(Percentile(Classify, 0.95)), *Number(Maximum(Classify)),
|
||||
*Number(Percentile(Mesh, 0.50)), *Number(Percentile(Mesh, 0.95)), *Number(Maximum(Mesh)),
|
||||
@@ -173,11 +177,12 @@ namespace
|
||||
return FString::Printf(
|
||||
TEXT("\"reason\":\"%s\",\"wall_s\":%s,\"tile_records_seen\":%d,"
|
||||
"\"tile_records_written\":%d,\"tile_records_suppressed\":%d,"
|
||||
"\"tile_record_cap\":%d,\"stage_seconds\":%s,\"tile_percentiles\":{%s}"),
|
||||
"\"tile_record_cap\":%d,\"apply_frame_records\":%d,"
|
||||
"\"stage_seconds\":%s,\"tile_percentiles\":{%s}"),
|
||||
*JsonEscape(Reason ? FString(Reason) : FString(TEXT("unknown"))),
|
||||
*Number(FPlatformTime::Seconds() - State.StartSeconds),
|
||||
State.TileRecordsSeen, State.TileRecordsWritten, State.TileRecordsSuppressed,
|
||||
State.TileRecordCap, *Stages, *Groups);
|
||||
State.TileRecordCap, State.ApplyFrameRecords, *Stages, *Groups);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +288,7 @@ void RecordTile(const FTileSample& Sample)
|
||||
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
|
||||
"\"request_ordinal\":%llu,\"generation_ordinal\":%llu,\"resident_at_request\":%d,"
|
||||
"\"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,"
|
||||
"\"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"),
|
||||
Sample.TileX, Sample.TileY, Sample.TileZ,
|
||||
Sample.Level, Sample.WorkPriority,
|
||||
@@ -295,10 +300,59 @@ void RecordTile(const FTileSample& Sample)
|
||||
static_cast<unsigned long long>(Sample.GeometryBytes),
|
||||
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
||||
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
||||
*Number(Sample.WorkerTaskSeconds),
|
||||
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
||||
*Number(Sample.MeshSeconds), *Number(Sample.StreamSeconds), *Number(Sample.ApplySeconds)));
|
||||
}
|
||||
|
||||
void RecordApplyFrame(const FApplyFrameSample& Sample)
|
||||
{
|
||||
FScopeLock Lock(>raceLock);
|
||||
if (!GState.IsValid() || !GState->bActive) return;
|
||||
|
||||
FString PendingByLevel = TEXT("[");
|
||||
FString ApplyResultsByLevel = TEXT("[");
|
||||
FString MeshesByLevel = TEXT("[");
|
||||
FString ApplyMillisecondsByLevel = TEXT("[");
|
||||
for (int32 Level = 0; Level < 9; ++Level)
|
||||
{
|
||||
if (Level > 0)
|
||||
{
|
||||
PendingByLevel += TEXT(",");
|
||||
ApplyResultsByLevel += TEXT(",");
|
||||
MeshesByLevel += TEXT(",");
|
||||
ApplyMillisecondsByLevel += TEXT(",");
|
||||
}
|
||||
PendingByLevel += FString::FromInt(Sample.PendingByLevel[Level]);
|
||||
ApplyResultsByLevel += FString::FromInt(Sample.ApplyResultsByLevel[Level]);
|
||||
MeshesByLevel += FString::FromInt(Sample.MeshesAppliedByLevel[Level]);
|
||||
ApplyMillisecondsByLevel += Number(Sample.ApplySecondsByLevel[Level] * 1000.0);
|
||||
}
|
||||
PendingByLevel += TEXT("]");
|
||||
ApplyResultsByLevel += TEXT("]");
|
||||
MeshesByLevel += TEXT("]");
|
||||
ApplyMillisecondsByLevel += TEXT("]");
|
||||
|
||||
++GState->ApplyFrameRecords;
|
||||
WriteEventLocked(*GState, TEXT("apply_frame"), FString::Printf(
|
||||
TEXT("\"frame\":%llu,\"frame_ms\":%s,\"speed_cm_s\":%s,\"apply_ms\":%s,\"drain_ms\":%s,"
|
||||
"\"active_tasks\":%d,\"running_workers\":%d,\"task_cap\":%d,"
|
||||
"\"pending_tiles\":%d,\"pending_results_estimate\":%d,"
|
||||
"\"results_drained\":%d,\"apply_results\":%d,\"meshes_applied\":%d,\"max_applies\":%d,"
|
||||
"\"apply_budget_ms\":%s,\"count_limit_reached\":%d,\"time_limit_reached\":%d,"
|
||||
"\"pending_by_lod\":%s,\"apply_results_by_lod\":%s,"
|
||||
"\"meshes_by_lod\":%s,\"apply_ms_by_lod\":%s"),
|
||||
static_cast<unsigned long long>(Sample.Frame),
|
||||
*Number(Sample.FrameSeconds * 1000.0), *Number(Sample.SpeedCmPerSecond),
|
||||
*Number(Sample.ApplySeconds * 1000.0), *Number(Sample.DrainSeconds * 1000.0), Sample.ActiveTasks,
|
||||
Sample.RunningWorkerTasks, Sample.TaskCap, Sample.PendingTiles,
|
||||
Sample.PendingResultsEstimate, Sample.ResultsDrained, Sample.ApplyResultsByLevelTotal,
|
||||
Sample.MeshesApplied,
|
||||
Sample.MaxApplies, *Number(Sample.ApplyBudgetMilliseconds),
|
||||
Sample.bCountLimitReached ? 1 : 0, Sample.bTimeLimitReached ? 1 : 0,
|
||||
*PendingByLevel, *ApplyResultsByLevel, *MeshesByLevel, *ApplyMillisecondsByLevel));
|
||||
}
|
||||
|
||||
void Finish(const TCHAR* Reason)
|
||||
{
|
||||
FScopeLock Lock(>raceLock);
|
||||
|
||||
@@ -237,6 +237,15 @@ namespace
|
||||
GVoxelForgeStep1MaxMeshApplies,
|
||||
TEXT("Step 1 measurement override: 0 uses the asset MaxMeshAppliesPerFrame, otherwise caps visible applies per frame."));
|
||||
|
||||
int32 GVoxelForgeStreamingStartupApplyBoost = 0;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingStartupApplyBoost(
|
||||
TEXT("voxel.StreamingStartupApplyBoost"),
|
||||
GVoxelForgeStreamingStartupApplyBoost,
|
||||
TEXT("Opt-in cold-view upload experiment (default off): 1 allows a 14 ms / 12-mesh budget until the initial desired set and walkable ring are ready; 0 uses the authored per-frame budget."));
|
||||
constexpr int32 StreamingStartupMaxMeshApplies = 12;
|
||||
constexpr float StreamingStartupApplyBudgetMilliseconds = 14.0f;
|
||||
constexpr double StreamingStartupApplyMaxSeconds = 30.0;
|
||||
|
||||
int32 GVoxelForgeStep1PrequeueVisibleLOD0 = 0;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStep1PrequeueVisibleLOD0(
|
||||
TEXT("voxel.Step1PrequeueVisibleLOD0"),
|
||||
@@ -2041,6 +2050,16 @@ void AVoxelWorld::BeginPlay()
|
||||
{
|
||||
GVoxelForgeStep1MaxMeshApplies = CommandLineStep1MaxMeshApplies;
|
||||
}
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingStartupApplyBoost="),
|
||||
GVoxelForgeStreamingStartupApplyBoost);
|
||||
// Headless BuildHost does not reliably apply arbitrary console-variable command-line tokens
|
||||
// before BeginPlay; parse the test switches explicitly so A/B runs exercise the requested path.
|
||||
// BuildHost sans interface n'applique pas toujours les CVar arbitraires avant BeginPlay ;
|
||||
// lire explicitement ces options afin que les essais A/B utilisent bien le chemin demande.
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingCoarseFirst="),
|
||||
GVoxelForgeStreamingCoarseFirst);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingDynamicNearReserve="),
|
||||
GVoxelForgeStreamingDynamicNearReserve);
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="),
|
||||
CommandLineStep1PrequeueVisibleLOD0))
|
||||
{
|
||||
@@ -3026,9 +3045,26 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||
|
||||
if (bMoveRequested && !bHeadlessStrateCrossingTest)
|
||||
{
|
||||
// FParse::Value treats commas as separators in some commandlet argument builds; read the
|
||||
// whole vector token so the requested X,Y,Z direction survives intact.
|
||||
// FParse::Value separe parfois les virgules dans les commandes ; lire le jeton X,Y,Z complet.
|
||||
FString DirectionText;
|
||||
if (FParse::Value(CommandLine, TEXT("voxel.TestMoveDirection="), DirectionText))
|
||||
const FString CommandLineText(CommandLine);
|
||||
const FString DirectionKey(TEXT("voxel.TestMoveDirection="));
|
||||
const int32 DirectionKeyIndex = CommandLineText.Find(
|
||||
DirectionKey, ESearchCase::IgnoreCase, ESearchDir::FromStart);
|
||||
if (DirectionKeyIndex != INDEX_NONE)
|
||||
{
|
||||
DirectionText = CommandLineText.Mid(DirectionKeyIndex + DirectionKey.Len());
|
||||
const int32 DirectionEnd = DirectionText.Find(
|
||||
TEXT(" "), ESearchCase::CaseSensitive, ESearchDir::FromStart);
|
||||
if (DirectionEnd != INDEX_NONE)
|
||||
{
|
||||
DirectionText.LeftInline(DirectionEnd, EAllowShrinking::No);
|
||||
}
|
||||
DirectionText.TrimStartAndEndInline();
|
||||
DirectionText.RemoveFromStart(TEXT("\""));
|
||||
DirectionText.RemoveFromEnd(TEXT("\""));
|
||||
TArray<FString> Components;
|
||||
DirectionText.ParseIntoArray(Components, TEXT(","), true);
|
||||
if (Components.Num() == 3)
|
||||
@@ -6717,6 +6753,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
AdvanceHeadlessDigMotionTest(PlayerLastPos, PlayerPawn);
|
||||
PeakObservedPawnSpeedCmPerSecond = FMath::Max(
|
||||
PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size2D());
|
||||
StartupTraceSpeedCmPerSecond = PlayerHeading.Size2D();
|
||||
UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime);
|
||||
RecordHeadlessDigMotionFrame(PlayerPawn, PlayerLastPos, DeltaTime);
|
||||
ObserveHeadlessStrateCrossingTest(PlayerLastPos, PlayerPawn);
|
||||
@@ -9064,19 +9101,121 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
// Budget limits how many VISIBLE mesh applies we do per frame (GPU upload cost).
|
||||
// Empty meshes and stale results are free to drain — don't count them.
|
||||
const bool bProfileApply = GVoxelForgeProfileTileGeneration != 0;
|
||||
uint64 DrainStartCycles = FPlatformTime::Cycles64();
|
||||
const uint64 ApplyFrameStartCycles = FPlatformTime::Cycles64();
|
||||
const double FrameSeconds = LastApplyFrameCycles != 0
|
||||
&& ApplyFrameStartCycles >= LastApplyFrameCycles
|
||||
? FPlatformTime::ToSeconds64(ApplyFrameStartCycles - LastApplyFrameCycles)
|
||||
: 0.0;
|
||||
LastApplyFrameCycles = ApplyFrameStartCycles;
|
||||
|
||||
const double StartupElapsedSeconds = DesiredEpochStartCycles != 0
|
||||
&& ApplyFrameStartCycles >= DesiredEpochStartCycles
|
||||
? FPlatformTime::ToSeconds64(ApplyFrameStartCycles - DesiredEpochStartCycles)
|
||||
: -1.0;
|
||||
const bool bInitialDesiredSetReady = DesiredEpoch == 1
|
||||
&& bAllChunksLoaded
|
||||
&& PendingTiles.Num() == 0
|
||||
&& PendingCollisionCooks.Num() == 0
|
||||
&& LastLOD0RingReadyEpoch == DesiredEpoch;
|
||||
if (bStartupApplyBoostStarted && !bStartupApplyBoostEnded
|
||||
&& (bInitialDesiredSetReady || DesiredEpoch != 1
|
||||
|| GVoxelForgeStreamingStartupApplyBoost <= 0
|
||||
|| GVoxelForgeStep1MaxMeshApplies > 0
|
||||
|| StartupElapsedSeconds >= StreamingStartupApplyMaxSeconds))
|
||||
{
|
||||
bStartupApplyBoostEnded = true;
|
||||
if (VoxelForgeStartupTrace::IsActive())
|
||||
{
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("startup_apply_budget"), FString::Printf(
|
||||
TEXT("\"state\":\"normal\",\"reason\":\"%s\",\"elapsed_s\":%s"),
|
||||
bInitialDesiredSetReady ? TEXT("initial_set_and_walkable_ring_ready")
|
||||
: (StartupElapsedSeconds >= StreamingStartupApplyMaxSeconds
|
||||
? TEXT("time_limit")
|
||||
: (DesiredEpoch != 1 ? TEXT("desired_epoch_changed")
|
||||
: TEXT("override_disabled"))),
|
||||
*FString::SanitizeFloat(StartupElapsedSeconds)));
|
||||
}
|
||||
}
|
||||
const bool bUseStartupApplyBoost = GVoxelForgeStreamingStartupApplyBoost > 0
|
||||
&& GVoxelForgeStep1MaxMeshApplies <= 0
|
||||
&& !bStartupApplyBoostEnded
|
||||
&& DesiredEpoch == 1
|
||||
&& StartupElapsedSeconds >= 0.0
|
||||
&& !bInitialDesiredSetReady
|
||||
&& StartupElapsedSeconds < StreamingStartupApplyMaxSeconds;
|
||||
if (bUseStartupApplyBoost && !bStartupApplyBoostStarted)
|
||||
{
|
||||
bStartupApplyBoostStarted = true;
|
||||
if (VoxelForgeStartupTrace::IsActive())
|
||||
{
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("startup_apply_budget"), FString::Printf(
|
||||
TEXT("\"state\":\"boosted\",\"max_applies\":%d,\"budget_ms\":%.3f,"
|
||||
"\"max_duration_s\":%.3f,\"desired_epoch\":%u"),
|
||||
StreamingStartupMaxMeshApplies,
|
||||
StreamingStartupApplyBudgetMilliseconds,
|
||||
StreamingStartupApplyMaxSeconds,
|
||||
DesiredEpoch));
|
||||
}
|
||||
}
|
||||
|
||||
uint64 DrainStartCycles = ApplyFrameStartCycles;
|
||||
const int32 ConfiguredMaxApplies = FMath::Max(
|
||||
Settings ? Settings->MaxMeshAppliesPerFrame : 4, 1);
|
||||
const int32 MaxApplies = GVoxelForgeStep1MaxMeshApplies > 0
|
||||
? GVoxelForgeStep1MaxMeshApplies : ConfiguredMaxApplies;
|
||||
const float ApplyBudgetMilliseconds = Settings
|
||||
? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f)
|
||||
: 2.0f;
|
||||
? GVoxelForgeStep1MaxMeshApplies
|
||||
: (bUseStartupApplyBoost ? StreamingStartupMaxMeshApplies : ConfiguredMaxApplies);
|
||||
const float ApplyBudgetMilliseconds = bUseStartupApplyBoost
|
||||
? StreamingStartupApplyBudgetMilliseconds
|
||||
: (Settings ? FMath::Max(Settings->MaxMeshApplyMilliseconds, 0.0f) : 2.0f);
|
||||
const double ApplyBudgetSeconds =
|
||||
static_cast<double>(ApplyBudgetMilliseconds) * 0.001;
|
||||
int32 MeshesApplied = 0;
|
||||
int32 ResultsDrained = 0;
|
||||
int32 ApplyResultsByLevelTotal = 0;
|
||||
double ApplySeconds = 0.0;
|
||||
TStaticArray<int32, 9> ApplyResultsByLevel{};
|
||||
TStaticArray<int32, 9> MeshesAppliedByLevel{};
|
||||
TStaticArray<double, 9> ApplySecondsByLevel{};
|
||||
bool bTimeBudgetHit = false;
|
||||
const auto RecordApplyFrame = [&]()
|
||||
{
|
||||
if (!VoxelForgeStartupTrace::IsActive()) return;
|
||||
VoxelForgeStartupTrace::FApplyFrameSample Sample;
|
||||
Sample.Frame = GFrameCounter;
|
||||
Sample.FrameSeconds = FrameSeconds;
|
||||
Sample.SpeedCmPerSecond = StartupTraceSpeedCmPerSecond;
|
||||
Sample.ApplySeconds = ApplySeconds;
|
||||
Sample.DrainSeconds = FPlatformTime::ToSeconds64(
|
||||
FPlatformTime::Cycles64() - DrainStartCycles);
|
||||
// `GetActiveGenerationTaskCount` includes completed tiles until the game-thread apply removes
|
||||
// their PendingTiles entry. Use the task guard's counter here so the difference estimates
|
||||
// results waiting in the MPSC queues instead of reporting zero by construction.
|
||||
// `GetActiveGenerationTaskCount` inclut les resultats termines jusqu'a leur application.
|
||||
// Utiliser le compteur RAII pour mesurer les resultats en attente dans les files MPSC.
|
||||
Sample.ActiveTasks = ActiveTaskCount.load(std::memory_order_relaxed);
|
||||
Sample.RunningWorkerTasks = ActiveGenerationWorkerCount.load(std::memory_order_relaxed);
|
||||
Sample.TaskCap = GetMaxConcurrentTasks();
|
||||
Sample.PendingTiles = PendingTiles.Num();
|
||||
Sample.PendingResultsEstimate = FMath::Max(0, Sample.PendingTiles - Sample.ActiveTasks);
|
||||
Sample.ResultsDrained = ResultsDrained;
|
||||
Sample.ApplyResultsByLevelTotal = ApplyResultsByLevelTotal;
|
||||
Sample.MeshesApplied = MeshesApplied;
|
||||
Sample.MaxApplies = MaxApplies;
|
||||
Sample.ApplyBudgetMilliseconds = ApplyBudgetMilliseconds;
|
||||
Sample.bCountLimitReached = MeshesApplied >= MaxApplies;
|
||||
Sample.bTimeLimitReached = bTimeBudgetHit;
|
||||
Sample.ApplyResultsByLevel = ApplyResultsByLevel;
|
||||
Sample.MeshesAppliedByLevel = MeshesAppliedByLevel;
|
||||
Sample.ApplySecondsByLevel = ApplySecondsByLevel;
|
||||
for (const FVoxelTileKey& PendingTile : PendingTiles)
|
||||
{
|
||||
if (PendingTile.Level >= 0 && PendingTile.Level < 9)
|
||||
{
|
||||
++Sample.PendingByLevel[PendingTile.Level];
|
||||
}
|
||||
}
|
||||
VoxelForgeStartupTrace::RecordApplyFrame(Sample);
|
||||
};
|
||||
FChunkResult DequeuedChunk;
|
||||
const auto DequeueNextResult = [&]() -> bool
|
||||
{
|
||||
@@ -9177,6 +9316,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
Step1HeldApplyResults.Num(), GetActiveGenerationTaskCount(), PendingTiles.Num());
|
||||
Step1LastLoggedPrequeueCount = Step1HeldVisibleLOD0Count;
|
||||
}
|
||||
RecordApplyFrame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -9222,9 +9362,27 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
// ApplyTileResult does epoch check, capture ingest, empty-release / mesh submission, and
|
||||
// only then records the tile as loaded. Only a real (visible) upload counts against the
|
||||
// per-frame budget — stale/empty drains are free.
|
||||
if (ApplyTileResult(DequeuedChunk))
|
||||
const int32 ResultLevel = DequeuedChunk.Tile.Level;
|
||||
const uint64 TileApplyStartCycles = FPlatformTime::Cycles64();
|
||||
const bool bMeshApplied = ApplyTileResult(DequeuedChunk);
|
||||
const uint64 TileApplyEndCycles = FPlatformTime::Cycles64();
|
||||
const double TileApplySeconds = TileApplyEndCycles >= TileApplyStartCycles
|
||||
? FPlatformTime::ToSeconds64(TileApplyEndCycles - TileApplyStartCycles)
|
||||
: 0.0;
|
||||
ApplySeconds += TileApplySeconds;
|
||||
if (ResultLevel >= 0 && ResultLevel < 9)
|
||||
{
|
||||
++ApplyResultsByLevelTotal;
|
||||
++ApplyResultsByLevel[ResultLevel];
|
||||
ApplySecondsByLevel[ResultLevel] += TileApplySeconds;
|
||||
}
|
||||
if (bMeshApplied)
|
||||
{
|
||||
++MeshesApplied;
|
||||
if (ResultLevel >= 0 && ResultLevel < 9)
|
||||
{
|
||||
++MeshesAppliedByLevel[ResultLevel];
|
||||
}
|
||||
}
|
||||
|
||||
// Time-box the entire game-thread drain, not only visible uploads. Empty releases and
|
||||
@@ -9255,6 +9413,7 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
ApplyBudgetMilliseconds,
|
||||
bTimeBudgetHit ? 1 : 0);
|
||||
}
|
||||
RecordApplyFrame();
|
||||
}
|
||||
|
||||
// Game-thread apply for one gen result. Shared by ProcessPendingChunks (async drain) and
|
||||
@@ -9379,31 +9538,44 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
const uint64 AppliedCycles = FPlatformTime::Cycles64();
|
||||
const bool bHaveWorkerTiming = Result.GenerationStartCycles != 0
|
||||
&& Result.GenerationEndCycles >= Result.GenerationStartCycles;
|
||||
const bool bHaveWorkerTaskTiming = Result.WorkerTaskStartCycles != 0
|
||||
&& Result.WorkerTaskEndCycles >= Result.WorkerTaskStartCycles;
|
||||
const double RequestToApply = FPlatformTime::ToSeconds64(
|
||||
AppliedCycles - Result.RequestStartCycles);
|
||||
const double ApplySeconds = ApplyStartCycles != 0
|
||||
? FPlatformTime::ToSeconds64(AppliedCycles - ApplyStartCycles) : 0.0;
|
||||
const double WorkerTaskSeconds = bHaveWorkerTaskTiming
|
||||
? FPlatformTime::ToSeconds64(
|
||||
Result.WorkerTaskEndCycles - Result.WorkerTaskStartCycles)
|
||||
: (bHaveWorkerTiming ? FPlatformTime::ToSeconds64(
|
||||
Result.GenerationEndCycles - Result.GenerationStartCycles) : 0.0);
|
||||
const double GenerationSeconds = bHaveWorkerTiming
|
||||
? FPlatformTime::ToSeconds64(
|
||||
Result.GenerationEndCycles - Result.GenerationStartCycles) : 0.0;
|
||||
const double WorkerQueueSeconds = bHaveWorkerTiming
|
||||
? FPlatformTime::ToSeconds64(
|
||||
Result.GenerationStartCycles - Result.RequestStartCycles) : 0.0;
|
||||
const double ResultQueueSeconds = bHaveWorkerTiming
|
||||
(bHaveWorkerTaskTiming ? Result.WorkerTaskStartCycles
|
||||
: Result.GenerationStartCycles) - Result.RequestStartCycles) : 0.0;
|
||||
const uint64 WorkerTaskEndCycles = bHaveWorkerTaskTiming
|
||||
? Result.WorkerTaskEndCycles : Result.GenerationEndCycles;
|
||||
const double ResultQueueSeconds = bHaveWorkerTiming && ApplyStartCycles != 0
|
||||
&& ApplyStartCycles >= WorkerTaskEndCycles
|
||||
? FPlatformTime::ToSeconds64(ApplyStartCycles - WorkerTaskEndCycles)
|
||||
: (bHaveWorkerTiming
|
||||
? FMath::Max(0.0, RequestToApply - WorkerQueueSeconds
|
||||
- GenerationSeconds - ApplySeconds)
|
||||
: FMath::Max(0.0, RequestToApply - ApplySeconds);
|
||||
- WorkerTaskSeconds - ApplySeconds)
|
||||
: FMath::Max(0.0, RequestToApply - ApplySeconds));
|
||||
if (GVoxelForgeProfileTileGeneration != 0)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeTileApplied] tile=(%d,%d,%d) level=%d empty=%d triangles=%d "
|
||||
"band=(%d,%d) "
|
||||
"request_to_apply=%.6f worker_queue=%.6f generation=%.6f "
|
||||
"request_to_apply=%.6f worker_queue=%.6f worker_task=%.6f generation=%.6f "
|
||||
"result_queue=%.6f apply=%.6f"),
|
||||
Result.Tile.Coord.X, Result.Tile.Coord.Y, Result.Tile.Coord.Z,
|
||||
Result.Tile.Level, bResultEmpty ? 1 : 0, Result.NumTriangles,
|
||||
Result.BandChunkLo, Result.BandChunkHi,
|
||||
RequestToApply, WorkerQueueSeconds, GenerationSeconds,
|
||||
RequestToApply, WorkerQueueSeconds, WorkerTaskSeconds, GenerationSeconds,
|
||||
ResultQueueSeconds, ApplySeconds);
|
||||
}
|
||||
if (bTraceTile)
|
||||
@@ -9427,6 +9599,7 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
Sample.WorkerQueueSeconds = WorkerQueueSeconds;
|
||||
Sample.ResultQueueSeconds = ResultQueueSeconds;
|
||||
Sample.QueueWaitSeconds = WorkerQueueSeconds + ResultQueueSeconds;
|
||||
Sample.WorkerTaskSeconds = WorkerTaskSeconds;
|
||||
Sample.GenerationSeconds = GenerationSeconds;
|
||||
Sample.ClassifySeconds = Result.ClassifySeconds;
|
||||
Sample.MeshSeconds = Result.MeshSeconds;
|
||||
@@ -11307,7 +11480,12 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
// refinement. The opt-out reproduces the S4 LOD0-to-LOD8 submission order for a control.
|
||||
// Le shell lointain passe apres l'anneau de collision, puis le raffinement fin. L'option
|
||||
// inverse restaure l'ordre LOD0-vers-LOD8 de S4 comme controle negatif.
|
||||
if (GVoxelForgeStreamingCoarseFirst != 0)
|
||||
// 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);
|
||||
@@ -11426,7 +11604,8 @@ 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,\"epoch\":%u,"
|
||||
"\"active_generation\":%d,\"task_cap\":%d,\"near_reserve\":%d,"
|
||||
"\"coarse_first\":%d,\"dynamic_near_reserve\":%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),
|
||||
@@ -11434,7 +11613,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
*FString::SanitizeFloat(PlayerHeading.Size2D()),
|
||||
bWalkableRingReadyForReserve ? 1 : 0,
|
||||
*DesiredCounts, *ResidentCounts, *PendingCounts,
|
||||
GetActiveGenerationTaskCount(), MaxTasks, NearTaskReserve, GenerationEpoch,
|
||||
GetActiveGenerationTaskCount(), MaxTasks, NearTaskReserve,
|
||||
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
||||
GenerationEpoch,
|
||||
*FString::SanitizeFloat(StreamingUpdateMilliseconds),
|
||||
*FString::SanitizeFloat(DesiredRebuildMilliseconds)));
|
||||
LastStreamingTraceSampleSeconds = TraceNow;
|
||||
@@ -11626,8 +11807,16 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
std::atomic<int32>& Counter;
|
||||
~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} Guard{ActiveTaskCount};
|
||||
ActiveGenerationWorkerCount.fetch_add(1, std::memory_order_relaxed);
|
||||
const uint64 WorkerTaskStartCycles = FPlatformTime::Cycles64();
|
||||
struct FWorkerGuard
|
||||
{
|
||||
std::atomic<int32>& Counter;
|
||||
~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} WorkerGuard{ActiveGenerationWorkerCount};
|
||||
|
||||
FChunkResult Result;
|
||||
Result.WorkerTaskStartCycles = WorkerTaskStartCycles;
|
||||
Result.RequestOrdinal = RequestOrdinal;
|
||||
Result.bResidentAtRequest = bResidentAtRequest;
|
||||
Result.WorkPriority = WorkPriority;
|
||||
@@ -11647,6 +11836,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
Canceled.RequestStartCycles = RequestStartCycles;
|
||||
Canceled.RequestOrdinal = RequestOrdinal;
|
||||
Canceled.GenerationOrdinal = Result.GenerationOrdinal;
|
||||
Canceled.WorkerTaskStartCycles = Result.WorkerTaskStartCycles;
|
||||
Canceled.WorkerTaskEndCycles = FPlatformTime::Cycles64();
|
||||
Canceled.GenerationStartCycles = Result.GenerationStartCycles;
|
||||
Canceled.GenerationEndCycles = FPlatformTime::Cycles64();
|
||||
Canceled.bResidentAtRequest = bResidentAtRequest;
|
||||
@@ -11723,6 +11914,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
|
||||
if (!ShouldAbortWork())
|
||||
{
|
||||
Result.WorkerTaskEndCycles = FPlatformTime::Cycles64();
|
||||
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
||||
CriticalProcessQueue.Enqueue(MoveTemp(Result));
|
||||
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
|
||||
@@ -11790,9 +11982,17 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
std::atomic<int32>& Counter;
|
||||
~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} Guard{ActiveTaskCount};
|
||||
ActiveGenerationWorkerCount.fetch_add(1, std::memory_order_relaxed);
|
||||
const uint64 WorkerTaskStartCycles = FPlatformTime::Cycles64();
|
||||
struct FWorkerGuard
|
||||
{
|
||||
std::atomic<int32>& Counter;
|
||||
~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} WorkerGuard{ActiveGenerationWorkerCount};
|
||||
|
||||
auto Enqueue = [this, WorkPriority](FChunkResult&& Result)
|
||||
{
|
||||
Result.WorkerTaskEndCycles = FPlatformTime::Cycles64();
|
||||
if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical)
|
||||
CriticalProcessQueue.Enqueue(MoveTemp(Result));
|
||||
else if (WorkPriority == EVoxelTileWorkPriority::Edited)
|
||||
@@ -11810,6 +12010,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
}
|
||||
|
||||
FChunkResult Result;
|
||||
Result.WorkerTaskStartCycles = WorkerTaskStartCycles;
|
||||
Result.Tile = Tile;
|
||||
Result.Epoch = TaskEpoch;
|
||||
Result.DesiredEpoch = TaskDesiredEpoch;
|
||||
@@ -11831,6 +12032,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
Canceled.RequestStartCycles = RequestStartCycles;
|
||||
Canceled.RequestOrdinal = RequestOrdinal;
|
||||
Canceled.GenerationOrdinal = Result.GenerationOrdinal;
|
||||
Canceled.WorkerTaskStartCycles = Result.WorkerTaskStartCycles;
|
||||
Canceled.bResidentAtRequest = bResidentAtRequest;
|
||||
Canceled.GenerationStartCycles = Result.GenerationStartCycles;
|
||||
Canceled.GenerationEndCycles = FPlatformTime::Cycles64();
|
||||
|
||||
Reference in new issue
Block a user