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:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 06:46:54 +02:00
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(&GTraceLock);
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(&GTraceLock);
+218 -16
View File
@@ -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);
+12
View File
@@ -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