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();
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace VoxelForgeStartupTrace
|
||||
double QueueWaitSeconds = 0.0;
|
||||
double WorkerQueueSeconds = 0.0;
|
||||
double ResultQueueSeconds = 0.0;
|
||||
double WorkerTaskSeconds = 0.0;
|
||||
double GenerationSeconds = 0.0;
|
||||
double ClassifySeconds = 0.0;
|
||||
double MeshSeconds = 0.0;
|
||||
@@ -36,6 +37,31 @@ namespace VoxelForgeStartupTrace
|
||||
double ApplySeconds = 0.0;
|
||||
};
|
||||
|
||||
struct VOXELFORGE_API FApplyFrameSample
|
||||
{
|
||||
uint64 Frame = 0;
|
||||
double FrameSeconds = 0.0;
|
||||
double SpeedCmPerSecond = 0.0;
|
||||
double ApplySeconds = 0.0;
|
||||
double DrainSeconds = 0.0;
|
||||
int32 ActiveTasks = 0;
|
||||
int32 RunningWorkerTasks = 0;
|
||||
int32 TaskCap = 0;
|
||||
int32 PendingTiles = 0;
|
||||
int32 PendingResultsEstimate = 0;
|
||||
int32 ResultsDrained = 0;
|
||||
int32 ApplyResultsByLevelTotal = 0;
|
||||
int32 MeshesApplied = 0;
|
||||
int32 MaxApplies = 0;
|
||||
double ApplyBudgetMilliseconds = 0.0;
|
||||
bool bCountLimitReached = false;
|
||||
bool bTimeLimitReached = false;
|
||||
TStaticArray<int32, 9> PendingByLevel{};
|
||||
TStaticArray<int32, 9> ApplyResultsByLevel{};
|
||||
TStaticArray<int32, 9> MeshesAppliedByLevel{};
|
||||
TStaticArray<double, 9> ApplySecondsByLevel{};
|
||||
};
|
||||
|
||||
/** Starts when -voxel.StartupTraceFile=<absolute path> is present. */
|
||||
VOXELFORGE_API void BeginFromCommandLine();
|
||||
|
||||
@@ -51,6 +77,10 @@ namespace VoxelForgeStartupTrace
|
||||
|
||||
VOXELFORGE_API void RecordTile(const FTileSample& Sample);
|
||||
|
||||
/** One game-thread result-drain sample, including worker occupancy and per-LOD applies. */
|
||||
/** Echantillon de vidage des resultats, avec occupation des travailleurs et applications par LOD. */
|
||||
VOXELFORGE_API void RecordApplyFrame(const FApplyFrameSample& Sample);
|
||||
|
||||
/** Writes the final bounded summary and closes the file. Safe to call more than once. */
|
||||
VOXELFORGE_API void Finish(const TCHAR* Reason);
|
||||
|
||||
|
||||
@@ -132,6 +132,11 @@ struct FChunkResult
|
||||
// opt-in profile. It is carried through the worker queue so the report can separate scheduler
|
||||
// wait from generation time.
|
||||
uint64 RequestStartCycles = 0;
|
||||
// Full background-task boundaries include stream conversion and result packaging around the
|
||||
// generator call; they let the trace reconcile per-tile worker-seconds with worker occupancy.
|
||||
// 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;
|
||||
// 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.
|
||||
@@ -1118,6 +1123,9 @@ public:
|
||||
|
||||
// Number of async tasks currently running — EndPlay waits for this to reach 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.
|
||||
std::atomic<int32> ActiveGenerationWorkerCount{0};
|
||||
|
||||
FORCEINLINE bool ShouldAbortWork(const std::atomic<bool>* ObsoleteFlag = nullptr) const
|
||||
{
|
||||
@@ -1175,6 +1183,10 @@ public:
|
||||
bool bAllChunksLoaded = false;
|
||||
bool bStartupTraceDesiredRecorded = false;
|
||||
bool bSampleStacksStarted = false;
|
||||
bool bStartupApplyBoostStarted = false;
|
||||
bool bStartupApplyBoostEnded = false;
|
||||
uint64 LastApplyFrameCycles = 0;
|
||||
float StartupTraceSpeedCmPerSecond = 0.0f;
|
||||
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||
// Ancestor -> desired descendants, rebuilt with DesiredSorted. Reconciliation uses this
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
#include "VoxelTerrainOpDefinition.h"
|
||||
#include "VoxelTypes.h"
|
||||
#include "VoxelWorld.h"
|
||||
#include "VoxelStartupTrace.h"
|
||||
#include "VoxelForgeCoreAssetAdapter.h"
|
||||
#include "VoxelForgeCore/VoxelForgeCore.h"
|
||||
|
||||
@@ -878,6 +879,7 @@ struct FExploreArguments
|
||||
int32 PassageFloorWrites = 1;
|
||||
bool bStandaloneWorldExport = false;
|
||||
bool bCoreWorldTest = false;
|
||||
bool bCoreWorldStreamTraceOnly = false;
|
||||
bool bRenderCameraOverride = false;
|
||||
FVector RenderCamera = FVector::ZeroVector;
|
||||
bool bRenderTargetOverride = false;
|
||||
@@ -923,6 +925,9 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
int32 CoreWorldTest = 0;
|
||||
FParse::Value(*Params, TEXT("coreworldtest="), CoreWorldTest);
|
||||
OutArguments.bCoreWorldTest = CoreWorldTest != 0;
|
||||
int32 CoreWorldStreamTraceOnly = 0;
|
||||
FParse::Value(*Params, TEXT("coreworldstreamtrace="), CoreWorldStreamTraceOnly);
|
||||
OutArguments.bCoreWorldStreamTraceOnly = CoreWorldStreamTraceOnly != 0;
|
||||
FParse::Value(*Params, TEXT("corevoxelsizecm="), OutArguments.CoreVoxelSizeCm);
|
||||
if (!FMath::IsFinite(OutArguments.CoreVoxelSizeCm)
|
||||
|| OutArguments.CoreVoxelSizeCm < VOXEL_SIZE
|
||||
@@ -1499,6 +1504,11 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
OutError = TEXT("coreworldtest requires settingsref and must omit strateref.");
|
||||
return false;
|
||||
}
|
||||
if (OutArguments.bCoreWorldStreamTraceOnly && !OutArguments.bCoreWorldTest)
|
||||
{
|
||||
OutError = TEXT("coreworldstreamtrace requires coreworldtest.");
|
||||
return false;
|
||||
}
|
||||
if ((OutArguments.bCoreHybridMesher || OutArguments.bCoreHybridFixturePlacement
|
||||
|| OutArguments.bCoreHybridCliffOnly || OutArguments.bCoreHybridFixturesOnly
|
||||
|| OutArguments.bCoreFloatingIslandTest || OutArguments.bCoreFallbackTest
|
||||
@@ -17329,6 +17339,241 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Arguments.bCoreWorldStreamTraceOnly)
|
||||
{
|
||||
const UWorld::InitializationValues WorldInitialization = UWorld::InitializationValues()
|
||||
.AllowAudioPlayback(false)
|
||||
.RequiresHitProxies(false)
|
||||
.CreatePhysicsScene(true)
|
||||
.CreateNavigation(false)
|
||||
.CreateAISystem(false)
|
||||
.ShouldSimulatePhysics(true)
|
||||
.SetDefaultGameMode(AGameModeBase::StaticClass())
|
||||
.SetTransactional(false);
|
||||
UWorld* TestWorld = UWorld::CreateWorld(
|
||||
EWorldType::Game, false, FName(TEXT("VoxelForgeCoreStreamTraceWorld")),
|
||||
GetTransientPackage(), false, ERHIFeatureLevel::Num, &WorldInitialization);
|
||||
if (TestWorld == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not create the transient BuildHost stream-trace world.");
|
||||
return false;
|
||||
}
|
||||
UGameInstance* TestGameInstance = NewObject<UGameInstance>(GEngine);
|
||||
FWorldContext& TestWorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
||||
TestWorldContext.OwningGameInstance = TestGameInstance;
|
||||
TestWorldContext.SetCurrentWorld(TestWorld);
|
||||
TestWorld->SetGameInstance(TestGameInstance);
|
||||
TestWorld->SetShouldTick(false);
|
||||
TestGameInstance->Init();
|
||||
TestWorld->AddToRoot();
|
||||
struct FStreamTraceWorldCleanup
|
||||
{
|
||||
UWorld* World = nullptr;
|
||||
uint64 PreviousFrameCounter = GFrameCounter;
|
||||
~FStreamTraceWorldCleanup()
|
||||
{
|
||||
if (World != nullptr)
|
||||
{
|
||||
if (World->HasBegunPlay())
|
||||
{
|
||||
World->BeginTearingDown();
|
||||
World->EndPlay(EEndPlayReason::Quit);
|
||||
}
|
||||
if (UGameInstance* GameInstance = World->GetGameInstance())
|
||||
{
|
||||
GameInstance->Shutdown();
|
||||
}
|
||||
if (GEngine != nullptr) GEngine->DestroyWorldContext(World);
|
||||
World->DestroyWorld(false);
|
||||
if (World->IsRooted()) World->RemoveFromRoot();
|
||||
}
|
||||
GFrameCounter = PreviousFrameCounter;
|
||||
}
|
||||
} Cleanup{TestWorld, GFrameCounter};
|
||||
|
||||
if (!TestWorld->SetGameMode(FURL()))
|
||||
{
|
||||
OutError = TEXT("Could not create the transient BuildHost GameMode for stream tracing.");
|
||||
return false;
|
||||
}
|
||||
if (AGameModeBase* GameMode = TestWorld->GetAuthGameMode())
|
||||
{
|
||||
GameMode->DefaultPawnClass = nullptr;
|
||||
}
|
||||
UPlayer* TestPlayer = NewObject<UPlayer>(GEngine, NAME_None, RF_Transient);
|
||||
APlayerController* PlayerController = TestWorld->SpawnActor<APlayerController>(
|
||||
APlayerController::StaticClass(), FTransform::Identity, FActorSpawnParameters());
|
||||
if (TestPlayer == nullptr || PlayerController == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not create the BuildHost stream-trace player controller.");
|
||||
return false;
|
||||
}
|
||||
PlayerController->SetPlayer(TestPlayer);
|
||||
TestWorld->AddController(PlayerController);
|
||||
APawn* PlayerPawn = TestWorld->SpawnActor<APawn>(
|
||||
APawn::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
if (PlayerPawn == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not create the BuildHost stream-trace pawn.");
|
||||
return false;
|
||||
}
|
||||
USceneComponent* PawnRoot = NewObject<USceneComponent>(PlayerPawn, NAME_None, RF_Transient);
|
||||
if (PawnRoot == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not create the BuildHost stream-trace pawn root.");
|
||||
return false;
|
||||
}
|
||||
PlayerPawn->SetRootComponent(PawnRoot);
|
||||
PawnRoot->RegisterComponent();
|
||||
PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics);
|
||||
PlayerController->Possess(PlayerPawn);
|
||||
if (TestWorld->GetFirstPlayerController() != PlayerController
|
||||
|| PlayerController->GetPawn() != PlayerPawn)
|
||||
{
|
||||
OutError = TEXT("BuildHost stream-trace controller did not retain its pawn.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const FTransform ActorTransform = FTransform::Identity;
|
||||
AVoxelWorld* Actor = TestWorld->SpawnActorDeferred<AVoxelWorld>(
|
||||
AVoxelWorld::StaticClass(), ActorTransform, nullptr, nullptr,
|
||||
ESpawnActorCollisionHandlingMethod::AlwaysSpawn);
|
||||
if (Actor == nullptr)
|
||||
{
|
||||
OutError = TEXT("Could not spawn the transient AVoxelWorld for stream tracing.");
|
||||
return false;
|
||||
}
|
||||
Actor->Settings = AuthoredSettings;
|
||||
Actor->bManageAtmosphere = false;
|
||||
Actor->bUseVoxelForgeCore = true;
|
||||
Actor->CoreVoxelSizeCm = Arguments.CoreVoxelSizeCm;
|
||||
Actor->CoreMesherRecipe = Arguments.CoreHybridMesherVersion == 2
|
||||
? EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV2
|
||||
: (Arguments.CoreHybridMesherVersion == 1
|
||||
? EVoxelForgeCoreMesherRecipe::WorldAssemblerHybridV1
|
||||
: EVoxelForgeCoreMesherRecipe::MarchingCubesV1);
|
||||
Actor->CoreClipRadius = 3;
|
||||
Actor->CoreMaxClipLevel = 8;
|
||||
Actor->bCaptureVoxelForgeCoreHashesForValidation = true;
|
||||
Actor->FinishSpawning(ActorTransform);
|
||||
TestWorld->InitializeActorsForPlay(FURL());
|
||||
TestWorld->BeginPlay();
|
||||
if (!VoxelForgeStartupTrace::IsActive())
|
||||
{
|
||||
OutError = TEXT("Core stream trace mode requires -voxel.StartupTraceFile.");
|
||||
return false;
|
||||
}
|
||||
Actor->SetActorTickEnabled(true);
|
||||
|
||||
auto DesiredTilesLoaded = [](const AVoxelWorld* World)
|
||||
{
|
||||
if (World == nullptr || World->DesiredSorted.IsEmpty()) return false;
|
||||
for (const FVoxelTileKey& Key : World->DesiredSorted)
|
||||
{
|
||||
if (!World->LoadedTiles.Contains(Key)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
auto CollisionSetSettled = [](const AVoxelWorld* World)
|
||||
{
|
||||
if (World == nullptr) return false;
|
||||
for (const FVoxelTileKey& Key : World->CollisionDesiredTiles)
|
||||
{
|
||||
if (!World->CollisionReadyTiles.Contains(Key)
|
||||
&& !World->CollisionNotRequiredTiles.Contains(Key)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const double ActorBeginSeconds = FPlatformTime::Seconds();
|
||||
constexpr double WalkStartSeconds = 30.0;
|
||||
constexpr double RunDurationSeconds = 45.0;
|
||||
TArray<double> SpawnTickMilliseconds;
|
||||
TArray<double> WalkTickMilliseconds;
|
||||
TArray<TPair<double, double>> TickSamples;
|
||||
bool bInitialSetReady = false;
|
||||
double InitialSetReadySeconds = -1.0;
|
||||
while (FPlatformTime::Seconds() - ActorBeginSeconds < RunDurationSeconds)
|
||||
{
|
||||
const double TickStartSeconds = FPlatformTime::Seconds();
|
||||
TestWorld->Tick(LEVELTICK_All, 0.016f);
|
||||
const double TickMilliseconds =
|
||||
(FPlatformTime::Seconds() - TickStartSeconds) * 1000.0;
|
||||
++GFrameCounter;
|
||||
const double ElapsedSeconds = FPlatformTime::Seconds() - ActorBeginSeconds;
|
||||
TickSamples.Emplace(ElapsedSeconds, TickMilliseconds);
|
||||
|
||||
if (!bInitialSetReady && Actor->IsVoxelForgeCoreReady()
|
||||
&& Actor->bAllChunksLoaded && Actor->PendingTiles.IsEmpty()
|
||||
&& Actor->PendingCollisionCooks.IsEmpty() && Actor->PendingUnload.IsEmpty()
|
||||
&& Actor->ActiveTaskCount.load(std::memory_order_acquire) == 0
|
||||
&& DesiredTilesLoaded(Actor) && CollisionSetSettled(Actor))
|
||||
{
|
||||
bInitialSetReady = true;
|
||||
InitialSetReadySeconds = ElapsedSeconds;
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("stream_trace_milestone"), FString::Printf(
|
||||
TEXT("\"name\":\"initial_set_ready\",\"elapsed_s\":%s,\"desired\":%d,\"loaded\":%d,\"collision_desired\":%d,\"collision_ready\":%d"),
|
||||
*FString::SanitizeFloat(ElapsedSeconds), Actor->DesiredSorted.Num(),
|
||||
Actor->LoadedTiles.Num(), Actor->CollisionDesiredTiles.Num(),
|
||||
Actor->CollisionReadyTiles.Num()));
|
||||
}
|
||||
FPlatformProcess::Sleep(0.016f);
|
||||
}
|
||||
|
||||
for (const TPair<double, double>& Sample : TickSamples)
|
||||
{
|
||||
if (bInitialSetReady && Sample.Key <= InitialSetReadySeconds)
|
||||
{
|
||||
SpawnTickMilliseconds.Add(Sample.Value);
|
||||
}
|
||||
else if (Sample.Key >= WalkStartSeconds)
|
||||
{
|
||||
WalkTickMilliseconds.Add(Sample.Value);
|
||||
}
|
||||
}
|
||||
|
||||
auto PercentileMilliseconds = [](TArray<double> Values, double Fraction)
|
||||
{
|
||||
if (Values.IsEmpty()) return 0.0;
|
||||
Values.Sort();
|
||||
const int32 Index = FMath::Clamp(
|
||||
FMath::CeilToInt(Fraction * static_cast<double>(Values.Num() - 1)),
|
||||
0, Values.Num() - 1);
|
||||
return Values[Index];
|
||||
};
|
||||
const auto MaxMilliseconds = [](const TArray<double>& Values)
|
||||
{
|
||||
double Maximum = 0.0;
|
||||
for (const double Value : Values) Maximum = FMath::Max(Maximum, Value);
|
||||
return Maximum;
|
||||
};
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldStreamTrace] initial_ready=%d initial_ready_s=%.6f walk_start_s=%.3f run_s=%.3f desired=%d loaded=%d "
|
||||
"spawn_tick_ms_p95=%.3f spawn_tick_ms_max=%.3f walk_tick_ms_p95=%.3f walk_tick_ms_max=%.3f"),
|
||||
bInitialSetReady ? 1 : 0, InitialSetReadySeconds, WalkStartSeconds,
|
||||
RunDurationSeconds, Actor->DesiredSorted.Num(), Actor->LoadedTiles.Num(),
|
||||
PercentileMilliseconds(SpawnTickMilliseconds, 0.95),
|
||||
MaxMilliseconds(SpawnTickMilliseconds),
|
||||
PercentileMilliseconds(WalkTickMilliseconds, 0.95),
|
||||
MaxMilliseconds(WalkTickMilliseconds));
|
||||
if (!bInitialSetReady)
|
||||
{
|
||||
OutError = FString::Printf(
|
||||
TEXT("Actor-only Core stream trace did not reach initial readiness: ready=%d desired=%d loaded=%d pending=%d cooks=%d active=%d."),
|
||||
Actor->IsVoxelForgeCoreReady() ? 1 : 0, Actor->DesiredSorted.Num(),
|
||||
Actor->LoadedTiles.Num(), Actor->PendingTiles.Num(),
|
||||
Actor->PendingCollisionCooks.Num(),
|
||||
Actor->ActiveTaskCount.load(std::memory_order_acquire));
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogTemp, Display,
|
||||
TEXT("[VoxelForgeCoreWorldStreamTrace] pass complete=1 recipe=%s voxel_cm=%.3f walk_speed_cm_s=600 walk_direction=+Y"),
|
||||
Arguments.CoreHybridMesherVersion == 2 ? TEXT("WorldAssemblerHybridV2")
|
||||
: (Arguments.CoreHybridMesherVersion == 1 ? TEXT("WorldAssemblerHybridV1")
|
||||
: TEXT("MarchingCubesV1")),
|
||||
Arguments.CoreVoxelSizeCm);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build one Standalone reference before creating the actor world so the two full Core worlds
|
||||
// never overlap in memory. The reference also precomputes the exact edit result and hashes.
|
||||
// Construire la référence Standalone avant le monde acteur pour ne jamais garder deux mondes
|
||||
|
||||
Reference in new issue
Block a user