unreal: remove 121 ms per-frame overlap scoring and 162 ms world-lock wait; keep support tiles (L96)

- Per-frame passage-overlap scoring over outstanding tiles (added in L85) measured 121 ms for
  407 LOD2 candidates; removed (deterministic desired/travel order kept).
- Visibility reconciliation read-locked the Core world per candidate while the far-world build
  held the writer lock (162 ms scan, 123 ms lock wait); the immutable render distance is cached.
  Visibility now <= 0.17 ms.
- Ready player-support collision tiles stay in the collision-desired set: fixes the L95 SW outlier
  (LOD0 (3,0,-47/-48/-50) re-admitted); 6 SW runs without re-admission.
- AVoxelWorld::Tick during the arrival burst: FI p99 3.7-4.3 ms (max 6.0), SW p99 8.2-8.9 ms
  (max 11.2). FI still shows rare 40-60 ms world ticks outside our actor scope (engine tick group).
- Near-set times kept: FI 2.06-2.12 s, SW median 1.30 s after first admission. World unchanged.
  Fast tier PASS; Linux UE host builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-28 10:28:28 +01:00
1 parent 5389abed2e
commit 94f9aa6790
7 files changed
+923 -67

No files matched your search

@@ -1,9 +1,12 @@
#include "VoxelStartupTrace.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformTLS.h"
#include "HAL/PlatformTime.h"
#include "CoreGlobals.h"
#include "Misc/CommandLine.h"
#include "Misc/Parse.h"
#include "UObject/UObjectGlobals.h"
#include <atomic>
@@ -15,7 +18,13 @@ namespace
{
FArchive* Archive = nullptr;
FString Path;
uint32 GameThreadId = 0;
double StartSeconds = 0.0;
double GameThreadJsonWriteSeconds = 0.0;
double GameThreadArchiveSerializeSeconds = 0.0;
double GarbageCollectionStartSeconds = 0.0;
FDelegateHandle PreGarbageCollectHandle;
FDelegateHandle PostGarbageCollectHandle;
int32 TileRecordCap = 4096;
int32 TileRecordsSeen = 0;
int32 TileRecordsWritten = 0;
@@ -50,15 +59,22 @@ namespace
void WriteLineLocked(FState& State, const FString& Line)
{
if (State.Archive == nullptr) return;
const uint64 StartCycles = FPlatformTime::Cycles64();
const FString WithNewline = Line + TEXT("\n");
FTCHARToUTF8 Utf8(*WithNewline);
State.Archive->Serialize(const_cast<ANSICHAR*>(Utf8.Get()), Utf8.Length());
if (FPlatformTLS::GetCurrentThreadId() == State.GameThreadId)
{
State.GameThreadArchiveSerializeSeconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - StartCycles);
}
// The file archive buffers records; flushing every tile/request can stall the game thread.
// L'archive tamponne les lignes ; vider apres chaque tuile/requete peut bloquer le thread de jeu.
}
void WriteEventLocked(FState& State, const TCHAR* Type, const FString& FieldsJson)
{
const uint64 StartCycles = FPlatformTime::Cycles64();
FString Line = FString::Printf(
TEXT("{\"type\":\"%s\",\"t_s\":%s"),
Type,
@@ -70,6 +86,11 @@ namespace
}
Line += TEXT("}");
WriteLineLocked(State, Line);
if (FPlatformTLS::GetCurrentThreadId() == State.GameThreadId)
{
State.GameThreadJsonWriteSeconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - StartCycles);
}
}
double Percentile(TArray<double> Values, double Fraction)
@@ -198,6 +219,7 @@ void BeginFromCommandLine()
TUniquePtr<FState> NewState = MakeUnique<FState>();
NewState->Path = Path;
NewState->GameThreadId = FPlatformTLS::GetCurrentThreadId();
FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceMaxTiles="), NewState->TileRecordCap);
NewState->TileRecordCap = FMath::Clamp(NewState->TileRecordCap, 64, 16384);
NewState->Samples.Reserve(NewState->TileRecordCap);
@@ -211,6 +233,32 @@ void BeginFromCommandLine()
NewState->bActive = true;
GState = MoveTemp(NewState);
GTraceActive.store(true, std::memory_order_release);
GState->PreGarbageCollectHandle = FCoreUObjectDelegates::GetPreGarbageCollectDelegate()
.AddLambda([]()
{
FScopeLock CallbackLock(&GTraceLock);
if (GState.IsValid() && GState->bActive)
{
GState->GarbageCollectionStartSeconds = FPlatformTime::Seconds();
}
});
GState->PostGarbageCollectHandle = FCoreUObjectDelegates::GetPostGarbageCollect()
.AddLambda([]()
{
FScopeLock CallbackLock(&GTraceLock);
if (!GState.IsValid() || !GState->bActive
|| GState->GarbageCollectionStartSeconds <= 0.0)
{
return;
}
const double DurationMilliseconds = FMath::Max(
0.0, FPlatformTime::Seconds() - GState->GarbageCollectionStartSeconds) * 1000.0;
GState->GarbageCollectionStartSeconds = 0.0;
WriteEventLocked(*GState, TEXT("garbage_collect"), FString::Printf(
TEXT("\"frame\":%llu,\"duration_ms\":%s"),
static_cast<unsigned long long>(GFrameCounter),
*Number(DurationMilliseconds)));
});
WriteEventLocked(*GState, TEXT("header"), FString::Printf(
TEXT("\"schema\":1,\"path\":\"%s\",\"tile_record_cap\":%d"),
*JsonEscape(GState->Path), GState->TileRecordCap));
@@ -221,6 +269,21 @@ bool IsActive()
return GTraceActive.load(std::memory_order_acquire);
}
FWriteMetrics ConsumeCurrentThreadWriteMetrics()
{
FWriteMetrics Metrics;
FScopeLock Lock(&GTraceLock);
if (!GState.IsValid() || FPlatformTLS::GetCurrentThreadId() != GState->GameThreadId)
{
return Metrics;
}
Metrics.JsonWriteMilliseconds = GState->GameThreadJsonWriteSeconds * 1000.0;
Metrics.ArchiveSerializeMilliseconds = GState->GameThreadArchiveSerializeSeconds * 1000.0;
GState->GameThreadJsonWriteSeconds = 0.0;
GState->GameThreadArchiveSerializeSeconds = 0.0;
return Metrics;
}
void RecordEvent(const TCHAR* Type, const FString& FieldsJson)
{
FScopeLock Lock(&GTraceLock);
@@ -378,6 +441,18 @@ void Finish(const TCHAR* Reason)
FScopeLock Lock(&GTraceLock);
if (!GState.IsValid() || !GState->bActive) return;
GTraceActive.store(false, std::memory_order_release);
if (GState->PreGarbageCollectHandle.IsValid())
{
FCoreUObjectDelegates::GetPreGarbageCollectDelegate().Remove(
GState->PreGarbageCollectHandle);
GState->PreGarbageCollectHandle.Reset();
}
if (GState->PostGarbageCollectHandle.IsValid())
{
FCoreUObjectDelegates::GetPostGarbageCollect().Remove(
GState->PostGarbageCollectHandle);
GState->PostGarbageCollectHandle.Reset();
}
// Close any stage left open by an abnormal early exit so the summary remains useful.
const double Now = FPlatformTime::Seconds();
@@ -387,7 +462,19 @@ void Finish(const TCHAR* Reason)
}
GState->StageStarts.Reset();
WriteEventLocked(*GState, TEXT("summary"), BuildSummaryFieldsLocked(*GState, Reason));
const uint64 FlushStartCycles = FPlatformTime::Cycles64();
GState->Archive->Flush();
const double FlushMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - FlushStartCycles) * 1000.0;
WriteEventLocked(*GState, TEXT("trace_finish_flush"), FString::Printf(
TEXT("\"first_flush_ms\":%s"), *Number(FlushMilliseconds)));
const uint64 FinalFlushStartCycles = FPlatformTime::Cycles64();
GState->Archive->Flush();
const double FinalFlushMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - FinalFlushStartCycles) * 1000.0;
UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStartupTrace] finish_flush_ms=%.3f final_flush_ms=%.3f"),
FlushMilliseconds, FinalFlushMilliseconds);
delete GState->Archive;
GState->Archive = nullptr;
GState->bActive = false;
+337 -67
View File
@@ -6966,6 +6966,20 @@ void AVoxelWorld::LogReadyTransitionVisibilityMeasure()
void AVoxelWorld::Tick(float DeltaTime)
{
const bool bTraceFrameTiming = VoxelForgeStartupTrace::IsActive();
const uint64 TickStartCycles = bTraceFrameTiming ? FPlatformTime::Cycles64() : 0;
uint64 CollisionGateCycles = 0;
uint64 UpdateChunksCycles = 0;
uint64 PendingDrainCycles = 0;
uint64 PostApplyCallbacksCycles = 0;
uint64 CollisionProbeCycles = 0;
uint64 VisibilityReconcileCycles = 0;
uint64 VisibleApplyProbeCycles = 0;
uint64 LOD0ReadyCycles = 0;
uint64 SteadyTraceCycles = 0;
uint64 ModificationCycles = 0;
uint64 UnloadCycles = 0;
uint64 LifecycleAuditCycles = 0;
Super::Tick(DeltaTime);
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_Tick); // game-thread streaming orchestration breakdown
if (bUseVoxelForgeCore && VoxelForgeCoreState.IsValid()
@@ -7069,14 +7083,26 @@ void AVoxelWorld::Tick(float DeltaTime)
PeakObservedPawnSpeedCmPerSecond = FMath::Max(
PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size());
StartupTraceSpeedCmPerSecond = PlayerHeading.Size();
const uint64 CollisionGateStartCycles = bTraceFrameTiming
? FPlatformTime::Cycles64() : 0;
UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime);
if (bTraceFrameTiming)
{
CollisionGateCycles += FPlatformTime::Cycles64() - CollisionGateStartCycles;
}
RecordHeadlessDigMotionFrame(PlayerPawn, PlayerLastPos, DeltaTime);
ObserveHeadlessStrateCrossingTest(PlayerLastPos, PlayerPawn);
ObserveHeadlessCollisionGateStressTest(PlayerLastPos, PlayerPawn);
const FVector StreamingCenterPosition = bHeadlessStreamingTestCenterOverride
? LocalVoxelToWorld(HeadlessStreamingTestCenterVoxel)
: PlayerLastPos;
const uint64 UpdateChunksStartCycles = bTraceFrameTiming
? FPlatformTime::Cycles64() : 0;
UpdateChunksAroundPosition(StreamingCenterPosition, PlayerPawn, PlayerHeading);
if (bTraceFrameTiming)
{
UpdateChunksCycles += FPlatformTime::Cycles64() - UpdateChunksStartCycles;
}
if (AtmosphereManager)
{
AtmosphereManager->UpdateForPlayer(PlayerLastPos);
@@ -7110,8 +7136,14 @@ void AVoxelWorld::Tick(float DeltaTime)
{
// Headless/server worlds without a local pawn still keep the Core fallback region
// streamed; this is the same explicit anchor path used by BuildHost validation.
const uint64 UpdateChunksStartCycles = bTraceFrameTiming
? FPlatformTime::Cycles64() : 0;
UpdateChunksAroundPosition(
GetVoxelForgeCoreDefaultCenterWorld(), nullptr, FVector::ZeroVector);
if (bTraceFrameTiming)
{
UpdateChunksCycles += FPlatformTime::Cycles64() - UpdateChunksStartCycles;
}
}
ReleasePawnCollisionGate();
if (APawn* PreviousPawn = PawnTickPrerequisite.Get())
@@ -7160,18 +7192,48 @@ void AVoxelWorld::Tick(float DeltaTime)
GVoxelForgePendingTestModification = 1;
}
}
const uint64 PendingDrainStartCycles = bTraceFrameTiming
? FPlatformTime::Cycles64() : 0;
ProcessPendingChunks();
if (bTraceFrameTiming)
{
PendingDrainCycles += FPlatformTime::Cycles64() - PendingDrainStartCycles;
}
const uint64 PostApplyCallbacksStartCycles = bTraceFrameTiming
? FPlatformTime::Cycles64() : 0;
uint64 CallbackStartCycles = bTraceFrameTiming ? FPlatformTime::Cycles64() : 0;
PollStep1SupportSweepProbes();
if (bTraceFrameTiming)
{
CollisionProbeCycles += FPlatformTime::Cycles64() - CallbackStartCycles;
CallbackStartCycles = FPlatformTime::Cycles64();
}
ReconcileReadyTransitionVisibility();
if (bTraceFrameTiming)
{
VisibilityReconcileCycles += FPlatformTime::Cycles64() - CallbackStartCycles;
CallbackStartCycles = FPlatformTime::Cycles64();
}
PollStep1VisibleApplyProbes();
if (bTraceFrameTiming)
{
VisibleApplyProbeCycles += FPlatformTime::Cycles64() - CallbackStartCycles;
CallbackStartCycles = FPlatformTime::Cycles64();
}
// Near-field evidence is independent of the full horizon settle point. Record one completion
// for each desired-set epoch after this frame's result drain, including moving crossings.
RecordLOD0RingReadyIfComplete();
if (bTraceFrameTiming)
{
LOD0ReadyCycles += FPlatformTime::Cycles64() - CallbackStartCycles;
PostApplyCallbacksCycles += FPlatformTime::Cycles64() - PostApplyCallbacksStartCycles;
}
// The startup trace stops at the same point the streaming policy declares itself settled:
// every desired key is loaded, no generation task remains, and the game-thread result drain
// has run for the frame. This is intentionally after ProcessPendingChunks so request-to-apply
// includes the final submission; the separate collision_ready events carry cook latency.
const uint64 SteadyTraceStartCycles = bTraceFrameTiming ? FPlatformTime::Cycles64() : 0;
if (VoxelForgeStartupTrace::IsActive()
&& bAllChunksLoaded
&& PendingTiles.Num() == 0
@@ -7265,12 +7327,17 @@ void AVoxelWorld::Tick(float DeltaTime)
FPlatformMisc::RequestExit(false, TEXT("VoxelForge steady streaming measurement complete"));
}
}
if (bTraceFrameTiming)
{
SteadyTraceCycles += FPlatformTime::Cycles64() - SteadyTraceStartCycles;
}
// Complete the one-shot diagnostic request only after the player's level-0 centre tile is
// loaded and idle. The edit fixture selects an already-visible level-0 geometry tile, so it
// measures the normal synchronous centre remesh without depending on a coarse overlay being
// visible. No pawn is distinct from a pawn at (0,0,0); retain the actor-location fallback for
// headless/editor use without making the world-origin pawn skip streaming.
const uint64 ModificationStartCycles = bTraceFrameTiming ? FPlatformTime::Cycles64() : 0;
FVector ModificationPosition = bHasPlayer ? PlayerLastPos : GetActorLocation();
FVoxelTileKey ModificationCenterTile(
GetTileCoordAtLocalCm(WorldToLocalCm(ModificationPosition)), 0);
@@ -7360,9 +7427,25 @@ void AVoxelWorld::Tick(float DeltaTime)
ModificationCenterTile.Coord.X, ModificationCenterTile.Coord.Y,
ModificationCenterTile.Coord.Z, bProofSurvivedCarve ? 1 : 0);
}
if (bTraceFrameTiming)
{
ModificationCycles += FPlatformTime::Cycles64() - ModificationStartCycles;
}
const uint64 UnloadStartCycles = bTraceFrameTiming
? FPlatformTime::Cycles64() : 0;
ProcessUnloadQueue();
if (bTraceFrameTiming)
{
UnloadCycles += FPlatformTime::Cycles64() - UnloadStartCycles;
}
const uint64 LifecycleAuditStartCycles = bTraceFrameTiming
? FPlatformTime::Cycles64() : 0;
UpdateVisualStalenessAudit();
MaybeFinishHeadlessStreamingTest();
if (bTraceFrameTiming)
{
LifecycleAuditCycles += FPlatformTime::Cycles64() - LifecycleAuditStartCycles;
}
if (bTestCeilingViewEnabled)
{
@@ -7531,6 +7614,37 @@ void AVoxelWorld::Tick(float DeltaTime)
}
}
#endif
if (bTraceFrameTiming && VoxelForgeStartupTrace::IsActive())
{
const VoxelForgeStartupTrace::FWriteMetrics TraceWriteMetrics =
VoxelForgeStartupTrace::ConsumeCurrentThreadWriteMetrics();
const uint64 TickEndCycles = FPlatformTime::Cycles64();
VoxelForgeStartupTrace::RecordEvent(TEXT("game_thread_work_frame"), FString::Printf(
TEXT("\"frame\":%llu,\"tick_ms\":%s,\"collision_gate_ms\":%s,"
"\"update_chunks_ms\":%s,\"pending_drain_ms\":%s,\"unload_ms\":%s,"
"\"post_apply_callbacks_ms\":%s,\"steady_trace_ms\":%s,"
"\"collision_probe_ms\":%s,\"visibility_reconcile_ms\":%s,"
"\"visible_apply_probe_ms\":%s,\"lod0_ready_ms\":%s,"
"\"modification_ms\":%s,\"lifecycle_audit_ms\":%s,"
"\"trace_json_ms\":%s,\"trace_archive_serialize_ms\":%s"),
static_cast<unsigned long long>(GFrameCounter),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(
TickEndCycles - TickStartCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(CollisionGateCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(UpdateChunksCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(PendingDrainCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(UnloadCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(PostApplyCallbacksCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(SteadyTraceCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(CollisionProbeCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(VisibilityReconcileCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(VisibleApplyProbeCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(LOD0ReadyCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(ModificationCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(LifecycleAuditCycles) * 1000.0),
*FString::SanitizeFloat(TraceWriteMetrics.JsonWriteMilliseconds),
*FString::SanitizeFloat(TraceWriteMetrics.ArchiveSerializeMilliseconds)));
}
}
//=============================================================================
@@ -7760,6 +7874,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason)
MakeShared<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe>();
State->World = std::make_unique<VoxelForgeCore::World>(std::move(*Snapshot));
State->FullResolutionLodCount = State->World->configuration().fullResolutionLodCount;
CoreRenderDistanceChunks = State->World->configuration().renderDistanceChunks;
const bool bFarWorldRequired=State->World->configuration().maxLod
>=State->FullResolutionLodCount;
State->bFarWorldRequired.store(bFarWorldRequired,std::memory_order_release);
@@ -7954,6 +8069,7 @@ void AVoxelWorld::FallBackToLegacyAfterCoreInitializationFailure(const FString&
StrateManager->SetHardTransitionFallbackForAssembly(false);
}
CoreFallbackCenterLocalCm = FVector::ZeroVector;
CoreRenderDistanceChunks = 0;
if (Mesher != nullptr)
{
Mesher->SetGeometryVoxelSizeCm(VOXEL_SIZE);
@@ -8481,6 +8597,7 @@ void AVoxelWorld::RequestCollisionGateSupportTile(const FVoxelTileKey& SupportTi
void AVoxelWorld::UpdatePawnCollisionGate(APawn* Pawn, const FVector& PlayerPosition,
float DeltaTime)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdatePawnCollisionGate);
if (!IsValid(Pawn) || bShuttingDown.load(std::memory_order_relaxed))
{
return;
@@ -9280,6 +9397,10 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
return;
}
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_HandleTileCollisionCookComplete);
const bool bTraceCollisionCompletion = VoxelForgeStartupTrace::IsActive();
const uint64 TraceCollisionCompletionStartCycles = bTraceCollisionCompletion
? FPlatformTime::Cycles64() : 0;
if (bShuttingDown.load(std::memory_order_relaxed))
{
return;
@@ -9524,6 +9645,16 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
{
UpdatePawnCollisionGate(PlayerPawn, PlayerPosition, 0.0f);
}
if (bTraceCollisionCompletion && VoxelForgeStartupTrace::IsActive())
{
const double HandlerMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceCollisionCompletionStartCycles) * 1000.0;
VoxelForgeStartupTrace::RecordEvent(TEXT("collision_completion"), FString::Printf(
TEXT("\"frame\":%llu,\"tile\":[%d,%d,%d],\"handler_ms\":%s"),
static_cast<unsigned long long>(GFrameCounter),
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z,
*FString::SanitizeFloat(HandlerMilliseconds)));
}
}
void AVoxelWorld::ProcessPendingChunks()
@@ -10430,6 +10561,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
const FVisualGapAuditState* GapAudit = GVoxelForgeVisualGapAudits.Find(this);
const bool bUseLegacyVisualHandoff = GapAudit && GapAudit->bLegacyVisualHandoff;
const bool bMeasure = bMeasureReadyTransitionVisibility;
const bool bTraceReconcile = VoxelForgeStartupTrace::IsActive();
const uint64 MeasureStartCycles = bMeasure ? FPlatformTime::Cycles64() : 0;
const int32 ComponentCountAtStart = bMeasure ? TileComponents.Num() : 0;
const int32 DesiredCountAtStart = bMeasure ? DesiredSorted.Num() : 0;
@@ -10464,6 +10596,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
// queue below.
TArray<FVoxelTileKey> ReadyToHide;
ReadyToHide.Reserve(TileComponents.Num());
uint64 CandidateScanCycles = bTraceReconcile ? FPlatformTime::Cycles64() : 0;
auto GetAncestorAtLevel = [](const FVoxelTileKey& Tile, int32 AncestorLevel)
{
const int32 Divisor = VF_ClipLevelScale(AncestorLevel - Tile.Level);
@@ -10529,11 +10662,16 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
ReadyToHide.Add(OldTile);
}
}
const uint64 CandidateScanElapsedCycles = bTraceReconcile
? FPlatformTime::Cycles64() - CandidateScanCycles : 0;
uint64 QueueUnloadCycles = bTraceReconcile ? FPlatformTime::Cycles64() : 0;
for (const FVoxelTileKey& Tile : ReadyToHide)
{
QueuePendingUnload(Tile);
}
const uint64 QueueUnloadElapsedCycles = bTraceReconcile
? FPlatformTime::Cycles64() - QueueUnloadCycles : 0;
// Replacement components may have been uploaded hidden while a retired tile still covered
// only part of the transition. Reveal them only after every overlapping retired component has
@@ -10542,6 +10680,7 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
TSet<FVoxelTileKey> VisibleRetiredTiles;
TSet<FVoxelTileKey> VisibleRetiredAncestors;
int32 MaxVisibleRetiredLevel = DesiredTransitionMaxLevel;
uint64 RetiredIndexCycles = bTraceReconcile ? FPlatformTime::Cycles64() : 0;
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& OldPair : TileComponents)
{
const FVoxelTileKey& OldTile = OldPair.Key;
@@ -10558,7 +10697,10 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
VisibleRetiredAncestors.Add(GetAncestorAtLevel(OldTile, Level));
}
}
const uint64 RetiredIndexElapsedCycles = bTraceReconcile
? FPlatformTime::Cycles64() - RetiredIndexCycles : 0;
uint64 RevealCycles = bTraceReconcile ? FPlatformTime::Cycles64() : 0;
for (const TPair<FVoxelTileKey, URealtimeMeshComponent*>& NewPair : TileComponents)
{
const FVoxelTileKey& NewTile = NewPair.Key;
@@ -10582,8 +10724,24 @@ void AVoxelWorld::ReconcileReadyTransitionVisibility()
NewComp->SetVisibility(true);
}
}
const uint64 RevealElapsedCycles = bTraceReconcile
? FPlatformTime::Cycles64() - RevealCycles : 0;
RecordMeasure();
if (bTraceReconcile)
{
VoxelForgeStartupTrace::RecordEvent(TEXT("visibility_reconcile"), FString::Printf(
TEXT("\"frame\":%llu,\"tile_components\":%d,\"desired_tiles\":%d,"
"\"ready_to_hide\":%d,\"visible_retired\":%d,"
"\"candidate_scan_ms\":%s,\"queue_unload_ms\":%s,"
"\"retired_index_ms\":%s,\"reveal_ms\":%s"),
static_cast<unsigned long long>(GFrameCounter), TileComponents.Num(),
DesiredSorted.Num(), ReadyToHide.Num(), VisibleRetiredTiles.Num(),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(CandidateScanElapsedCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(QueueUnloadElapsedCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(RetiredIndexElapsedCycles) * 1000.0),
*FString::SanitizeFloat(FPlatformTime::ToSeconds64(RevealElapsedCycles) * 1000.0)));
}
}
// Integer floor-division (correct for negatives), scalar + vector.
@@ -10688,6 +10846,21 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
APawn* PlayerPawn, const FVector& PlayerHeading,
TArray<FVoxelTileKey>& OutLeavers)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildCoreDesiredTiles);
// Capture the resolved floor before rebuilding CollisionDesiredTiles. The gate request set
// drops keys as soon as they have a body, while the pawn-radius set may not include a floor
// tile below a known-empty band. Keeping the active support here prevents that body from
// being retired and re-requested every subsequent tick.
FVoxelTileKey SupportProbeTile;
FVoxelTileKey ResolvedSupportTile;
const bool bHasSupportProbe = GetPlayerSupportTile(
PlayerPawn, PlayerPosition, SupportProbeTile);
const TCHAR* SupportUnavailableReason = nullptr;
const bool bRetainResolvedSupport = bHasSupportProbe
&& IsCollisionReadyFromSupportTile(
SupportProbeTile, &ResolvedSupportTile, &SupportUnavailableReason)
&& SupportUnavailableReason == nullptr;
DesiredSorted.Reset();
CriticalDesiredTiles.Reset();
OutLeavers.Reset();
@@ -10794,6 +10967,17 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
CollisionAnchorTiles.Add(Key);
}
if (bRetainResolvedSupport)
{
AddCandidate(ResolvedSupportTile);
CollisionDesiredTiles.Add(ResolvedSupportTile);
if (!BaseVisualTiles.Contains(ResolvedSupportTile)
&& !FullVisualAnchorTiles.Contains(ResolvedSupportTile))
{
CollisionAnchorTiles.Add(ResolvedSupportTile);
}
}
const FVoxelTileKey PlayerTile(GetTileCoordAtLocalCm(WorldToLocalCm(PlayerPosition)), 0);
FVoxelTileKey OccupiedTile = PlayerTile;
FVoxelTileKey SupportTile;
@@ -11724,15 +11908,8 @@ bool AVoxelWorld::IsTileInClipRange(const FVoxelTileKey& Tile, const FIntVector&
int32 MaxLevel = MaxLevelBase, ROuter = R;
if (bUseVoxelForgeCore)
{
VoxelForgeCore::WorldConfiguration Configuration;
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
if (State.IsValid())
{
FReadScopeLock Lock(State->WorldLock);
if (State->World) Configuration = State->World->configuration();
}
VoxelClipmapDesiredTiles::FParameters Parameters;
Parameters.RenderDistanceChunks = Configuration.renderDistanceChunks;
Parameters.RenderDistanceChunks = CoreRenderDistanceChunks;
VoxelClipmapDesiredTiles::OuterShell(Parameters, R, MaxLevelBase, MaxLevel, ROuter);
}
else
@@ -11912,6 +12089,12 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
const uint64 ResultBacklogCapStopsAtUpdateEntry =
StreamingResultBacklogCapStops.load(std::memory_order_relaxed);
double DesiredRebuildMilliseconds = 0.0;
double SubmitTilesMilliseconds = 0.0;
double SubmitLevelMilliseconds = 0.0;
double LevelCandidateScanMilliseconds = 0.0;
double PassageScoreMilliseconds = 0.0;
double LevelSortMilliseconds = 0.0;
double LevelAdmissionMilliseconds = 0.0;
const auto RecordSchedulerSkip = [&](const TCHAR* Reason)
{
if (!bTraceStreamingScheduler) return;
@@ -12569,6 +12752,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
// Le travail grossier reste sous le budget total pour reserver des admissions aux tuiles proches.
//=========================================================================
const bool bSubmissionLoopRan = !bAllChunksLoaded;
const uint64 TraceSubmitTilesStartCycles = bTraceStreamingScheduler && bSubmissionLoopRan
? FPlatformTime::Cycles64() : 0;
if (bSubmissionLoopRan)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles);
@@ -12644,12 +12829,21 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
auto SubmitLevel = [&](int32 Level, EVoxelTileWorkPriority Priority, int32 AdmissionCap,
bool bPreferTravelDirection)
{
const uint64 TraceLevelStartCycles = bTraceStreamingScheduler
? FPlatformTime::Cycles64() : 0;
if (GetGenerationAdmissionCount() >= AdmissionCap)
{
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapReturns[Level]; }
if (bTraceStreamingScheduler)
{
SubmitLevelMilliseconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceLevelStartCycles) * 1000.0;
}
return;
}
TArray<FVoxelTileKey> LevelTiles;
const uint64 TraceCandidateScanStartCycles = bTraceStreamingScheduler
? FPlatformTime::Cycles64() : 0;
for (const FVoxelTileKey& T : DesiredSorted)
{
if (T.Level == Level && !PendingTiles.Contains(T) && !LoadedTiles.Contains(T))
@@ -12657,25 +12851,16 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
LevelTiles.Add(T);
}
}
// Sort only outstanding keys; re-sorting loaded or pending tiles on every tick inflated game-thread work.
// Trier uniquement les cles restantes ; re-trier chaque tick les tuiles chargees ou en attente alourdissait le thread de jeu.
TMap<FVoxelTileKey,uint32> PassageScores;
if(Level<=2&&CoreState.IsValid()&&CoreState->bReady.load(std::memory_order_acquire))
if (bTraceStreamingScheduler)
{
FReadScopeLock WorldReadLock(CoreState->WorldLock);
if(CoreState->World)
{
PassageScores.Reserve(LevelTiles.Num());
for(const FVoxelTileKey& Key:LevelTiles)
{
const VoxelForgeCore::TileKey CoreKey{
{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
PassageScores.Add(Key,CoreState->World->passageOverlapScore(CoreKey));
}
}
LevelCandidateScanMilliseconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceCandidateScanStartCycles) * 1000.0;
}
const bool bSortForPassagePriority=Level<=2&&!PassageScores.IsEmpty();
if ((bPreferTravelDirection||bSortForPassagePriority)&&LevelTiles.Num()>1)
// Keep admission work bounded. Recomputing passageOverlapScore for every outstanding
// near tile rescanned all resolved passage segments on every frame and could consume
// >120 ms on the game thread. DesiredSorted already has deterministic near/distance
// order; this optional sort only adds the current travel heading and distance.
if (bPreferTravelDirection && LevelTiles.Num() > 1)
{
FVector Heading=FVector::ZeroVector;
FVector PlayerLocalCm=FVector::ZeroVector;
@@ -12693,16 +12878,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
TileSizeCm=static_cast<double>(VF_ClipLevelExtentVoxels(Level))
*GetActiveVoxelSizeCm();
}
const uint64 TraceLevelSortStartCycles = bTraceStreamingScheduler
? FPlatformTime::Cycles64() : 0;
std::stable_sort(LevelTiles.GetData(),LevelTiles.GetData()+LevelTiles.Num(),
[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
{
if(bSortForPassagePriority)
{
const uint32 ScoreA=PassageScores.FindRef(A);
const uint32 ScoreB=PassageScores.FindRef(B);
if(ScoreA!=ScoreB)return ScoreA>ScoreB;
}
if(!bPreferTravelDirection)return false;
const FVector CenterA(
(static_cast<double>(A.Coord.X) + 0.5) * TileSizeCm,
(static_cast<double>(A.Coord.Y) + 0.5) * TileSizeCm,
@@ -12723,7 +12903,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y;
return A.Coord.Z < B.Coord.Z;
});
if (bTraceStreamingScheduler)
{
LevelSortMilliseconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceLevelSortStartCycles) * 1000.0;
}
}
const uint64 TraceLevelAdmissionStartCycles = bTraceStreamingScheduler
? FPlatformTime::Cycles64() : 0;
for (const FVoxelTileKey& T : LevelTiles)
{
if (GetGenerationAdmissionCount() >= AdmissionCap)
@@ -12736,6 +12923,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
SubmitTileWithTraceCount(T, Priority);
++Submitted;
}
if (bTraceStreamingScheduler)
{
LevelAdmissionMilliseconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceLevelAdmissionStartCycles) * 1000.0;
SubmitLevelMilliseconds += FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceLevelStartCycles) * 1000.0;
}
};
const auto AreCriticalTilesReady = [this]()
@@ -13003,6 +13197,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
bAllChunksLoaded = true;
}
}
if (TraceSubmitTilesStartCycles != 0)
{
SubmitTilesMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceSubmitTilesStartCycles) * 1000.0;
}
if (VoxelForgeStartupTrace::IsActive())
{
@@ -13220,6 +13419,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds;
const double IntervalSeconds = FMath::Max(0.0, SchedulerTraceNow - IntervalStart);
const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta);
const double SchedulerUpdateMilliseconds = (SchedulerTraceNow
- StreamingUpdateStartSeconds) * 1000.0;
VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_admission"), FString::Printf(
TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"desired_rebuilt\":%d,"
"\"submission_loop_ran\":%d,\"all_chunks_loaded\":%d,"
@@ -13238,6 +13439,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
"\"result_backlog\":%d,\"result_backlog_cap\":%d,"
"\"region_independent_submissions\":%llu,\"region_dependent_declines\":%llu,"
"\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%s,"
"\"update_chunks_ms\":%s,\"desired_rebuild_ms\":%s,"
"\"submit_tiles_ms\":%s,\"submit_level_ms\":%s,"
"\"level_candidate_scan_ms\":%s,\"passage_score_ms\":%s,"
"\"level_sort_ms\":%s,\"level_admit_ms\":%s,"
"\"desired_missing_by_lod\":%s,\"loaded_by_lod\":%s,\"pending_by_lod\":%s,"
"\"submitted_by_lod\":%s,\"load_attempts_by_lod\":%s,"
"\"admission_cap_by_lod\":%s,\"declined_rule_by_lod\":%s,"
@@ -13265,6 +13470,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
static_cast<unsigned long long>(CoreRegionDependentDeclines),
*FString::SanitizeFloat(BusySeconds),
*FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0),
*FString::SanitizeFloat(SchedulerUpdateMilliseconds),
*FString::SanitizeFloat(DesiredRebuildMilliseconds),
*FString::SanitizeFloat(SubmitTilesMilliseconds),
*FString::SanitizeFloat(SubmitLevelMilliseconds),
*FString::SanitizeFloat(LevelCandidateScanMilliseconds),
*FString::SanitizeFloat(PassageScoreMilliseconds),
*FString::SanitizeFloat(LevelSortMilliseconds),
*FString::SanitizeFloat(LevelAdmissionMilliseconds),
*IntArrayJson(DesiredMissingByLevel), *IntArrayJson(LoadedByLevel),
*IntArrayJson(PendingByLevel), *IntArrayJson(SchedulerSubmittedByLevel),
*IntArrayJson(SchedulerLoadAttemptsByLevel), *IntArrayJson(AdmissionCapByLevel),
@@ -14583,7 +14796,12 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_ApplyMeshToChunk);
const bool bProfileApply = GVoxelForgeProfileTileGeneration != 0;
const bool bTraceRmcTiming = VoxelForgeStartupTrace::IsActive();
const uint64 ApplyStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
double TraceComponentAcquireMilliseconds = 0.0;
double TraceSectionGroupMilliseconds = 0.0;
double TraceSectionConfigMilliseconds = 0.0;
double TraceCollisionHandoffMilliseconds = 0.0;
// Streams are pre-built on the worker (T1.f) and guaranteed non-empty by the caller
// (ProcessPendingChunks skips empty tiles). This path is game-thread-CHEAP: material lookup +
@@ -14704,10 +14922,19 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
bool bComponentCreated = false;
if (!MeshComp)
{
MeshComp = AcquireTileComponent(
bProfileApply ? &ComponentCreationSeconds : nullptr,
bProfileApply ? &ComponentRegistrationSeconds : nullptr,
&bComponentCreated);
const uint64 TraceStartCycles = bTraceRmcTiming ? FPlatformTime::Cycles64() : 0;
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_RMC_CreateTileComponent);
MeshComp = AcquireTileComponent(
bProfileApply ? &ComponentCreationSeconds : nullptr,
bProfileApply ? &ComponentRegistrationSeconds : nullptr,
&bComponentCreated);
}
if (bTraceRmcTiming)
{
TraceComponentAcquireMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceStartCycles) * 1000.0;
}
TileComponents.Add(Tile, MeshComp);
}
if (!IsValid(MeshComp))
@@ -14804,7 +15031,17 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
// CreateSectionGroup. Keep this as one phase: it is the exact plugin/RMC boundary we can
// budget without modifying RealtimeMeshComponent.
const uint64 StreamUploadStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
RTMesh->CreateSectionGroup(GroupKey, MoveTemp(Streams));
const uint64 TraceSectionGroupStartCycles = bTraceRmcTiming
? FPlatformTime::Cycles64() : 0;
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_RMC_CreateSectionGroup);
RTMesh->CreateSectionGroup(GroupKey, MoveTemp(Streams));
}
if (bTraceRmcTiming)
{
TraceSectionGroupMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceSectionGroupStartCycles) * 1000.0;
}
const uint64 MeshSubmittedCycles = FPlatformTime::Cycles64();
const double StreamUploadSeconds = bProfileApply
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - StreamUploadStartCycles)
@@ -14816,30 +15053,40 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
// (default config already maps material slot = polygroup index); only config sections that
// exist — the bHas* flags come from the worker. Collision at level 0 only (T1.c), both groups.
const uint64 CollisionConfigStartCycles = bProfileApply ? FPlatformTime::Cycles64() : 0;
const uint64 TraceSectionConfigStartCycles = bTraceRmcTiming
? FPlatformTime::Cycles64() : 0;
const int32 NumIndices = Result.NumTriangles * 3;
const int32 FirstCapIndex = FMath::Clamp(
NumIndices - Result.NumCeilingTriangles * 3, 0, NumIndices);
if (bHasGroundTris)
{
FRealtimeMeshSectionConfig GroundConfig(0);
GroundConfig.bCastsShadow = bCastShadow;
const FRealtimeMeshStreamRange GroundRange =
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
0, Result.NumVertices, 0, FirstCapIndex);
RTMesh->CreateSection(
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 0),
GroundConfig, GroundRange, /*bShouldCreateCollision*/ bWantsCollision);
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_RMC_CreateSections);
if (bHasGroundTris)
{
FRealtimeMeshSectionConfig GroundConfig(0);
GroundConfig.bCastsShadow = bCastShadow;
const FRealtimeMeshStreamRange GroundRange =
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
0, Result.NumVertices, 0, FirstCapIndex);
RTMesh->CreateSection(
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 0),
GroundConfig, GroundRange, /*bShouldCreateCollision*/ bWantsCollision);
}
if (bHasCeilingTris)
{
FRealtimeMeshSectionConfig CapConfig(1);
CapConfig.bCastsShadow = false; // the cap never casts (see above)
const FRealtimeMeshStreamRange CapRange =
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
0, Result.NumVertices, FirstCapIndex, NumIndices);
RTMesh->CreateSection(
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
CapConfig, CapRange, /*bShouldCreateCollision*/ bWantsCollision);
}
}
if (bHasCeilingTris)
if (bTraceRmcTiming)
{
FRealtimeMeshSectionConfig CapConfig(1);
CapConfig.bCastsShadow = false; // the cap never casts (see above)
const FRealtimeMeshStreamRange CapRange =
URealtimeMeshBlueprintFunctionLibrary::MakeStreamRange(
0, Result.NumVertices, FirstCapIndex, NumIndices);
RTMesh->CreateSection(
FRealtimeMeshSectionKey::CreateForPolyGroup(GroupKey, 1),
CapConfig, CapRange, /*bShouldCreateCollision*/ bWantsCollision);
TraceSectionConfigMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceSectionConfigStartCycles) * 1000.0;
}
const double CollisionConfigSeconds = bProfileApply
? FPlatformTime::ToSeconds64(FPlatformTime::Cycles64() - CollisionConfigStartCycles)
@@ -14888,21 +15135,30 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
// Register the serial before asking RMC to dirty collision. The current implementation
// defers the future to end-of-frame, but this ordering also keeps an already-fulfilled
// future or an inline body-update event from racing the per-tile record.
TFuture<ERealtimeMeshCollisionUpdateResult> CollisionFuture =
RTMesh->SetCollisionConfig(CollisionConfig);
TWeakObjectPtr<AVoxelWorld> WeakWorld(this);
TWeakObjectPtr<URealtimeMesh> WeakMesh(RTMesh);
CollisionFuture.Next(
[WeakWorld, WeakMesh, Tile, SubmissionId](ERealtimeMeshCollisionUpdateResult CollisionResult)
{
if (AVoxelWorld* World = WeakWorld.Get())
const uint64 TraceCollisionHandoffStartCycles = bTraceRmcTiming
? FPlatformTime::Cycles64() : 0;
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_RMC_CollisionHandoff);
TFuture<ERealtimeMeshCollisionUpdateResult> CollisionFuture =
RTMesh->SetCollisionConfig(CollisionConfig);
CollisionFuture.Next(
[WeakWorld, WeakMesh, Tile, SubmissionId](ERealtimeMeshCollisionUpdateResult CollisionResult)
{
World->HandleTileCollisionCookComplete(
Tile, SubmissionId, WeakMesh.Get(), static_cast<uint8>(CollisionResult),
FPlatformTime::Cycles64());
}
});
if (AVoxelWorld* World = WeakWorld.Get())
{
World->HandleTileCollisionCookComplete(
Tile, SubmissionId, WeakMesh.Get(), static_cast<uint8>(CollisionResult),
FPlatformTime::Cycles64());
}
});
}
if (bTraceRmcTiming)
{
TraceCollisionHandoffMilliseconds = FPlatformTime::ToSeconds64(
FPlatformTime::Cycles64() - TraceCollisionHandoffStartCycles) * 1000.0;
}
}
if (bProfileApply)
@@ -14931,6 +15187,20 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
TotalSeconds);
}
if (bTraceRmcTiming)
{
VoxelForgeStartupTrace::RecordEvent(TEXT("rmc_submit"), FString::Printf(
TEXT("\"frame\":%llu,\"tile\":[%d,%d,%d],\"level\":%d,"
"\"component_acquire_ms\":%s,\"create_section_group_ms\":%s,"
"\"create_sections_ms\":%s,\"collision_handoff_ms\":%s"),
static_cast<unsigned long long>(GFrameCounter),
Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level,
*FString::SanitizeFloat(TraceComponentAcquireMilliseconds),
*FString::SanitizeFloat(TraceSectionGroupMilliseconds),
*FString::SanitizeFloat(TraceSectionConfigMilliseconds),
*FString::SanitizeFloat(TraceCollisionHandoffMilliseconds)));
}
// Water is not spawned per tile — it's a single player-following ocean plane (UpdateWater,
// driven from Tick), so it renders at every LOD and to the horizon with no per-tile gaps.
return true;
@@ -69,11 +69,20 @@ namespace VoxelForgeStartupTrace
TStaticArray<double, 9> ApplySecondsByLevel{};
};
struct VOXELFORGE_API FWriteMetrics
{
double JsonWriteMilliseconds = 0.0;
double ArchiveSerializeMilliseconds = 0.0;
};
/** Starts when -voxel.StartupTraceFile=<absolute path> is present. */
VOXELFORGE_API void BeginFromCommandLine();
VOXELFORGE_API bool IsActive();
/** Returns and resets startup-trace writes performed by the current game-thread frame. */
VOXELFORGE_API FWriteMetrics ConsumeCurrentThreadWriteMetrics();
/** FieldsJson is the contents after the type/timestamp object prefix, without braces. */
VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString());
+1
View File
@@ -790,6 +790,7 @@ private:
FString VoxelForgeCoreResolvedLayoutJson;
FString VoxelForgeCoreResolvedLayoutIdentity;
FVector CoreFallbackCenterLocalCm = FVector::ZeroVector;
int32 CoreRenderDistanceChunks = 0;
bool bCoreRegionCompilePending = false;
TArray<FVoxelTileKey> PendingCoreRegionTiles;
TSet<FVoxelTileKey> PendingCoreRegionTileSet;
@@ -18172,11 +18172,60 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
TArray<double> SpawnTickMilliseconds;
TArray<double> WalkTickMilliseconds;
TArray<TPair<double, double>> TickSamples;
double WorldTickStartedSeconds = 0.0;
double WorldActorPhaseStartedSeconds = 0.0;
double WorldPostActorStartedSeconds = 0.0;
double WorldPreActorMilliseconds = 0.0;
double WorldActorPhaseMilliseconds = 0.0;
double WorldPostActorMilliseconds = 0.0;
const FDelegateHandle WorldTickStartHandle = FWorldDelegates::OnWorldTickStart.AddLambda(
[&](UWorld* World, ELevelTick, float)
{
if (World == TestWorld)
{
WorldTickStartedSeconds = FPlatformTime::Seconds();
}
});
const FDelegateHandle WorldPreActorTickHandle = FWorldDelegates::OnWorldPreActorTick.AddLambda(
[&](UWorld* World, ELevelTick, float)
{
if (World == TestWorld)
{
const double NowSeconds = FPlatformTime::Seconds();
WorldPreActorMilliseconds =
(NowSeconds - WorldTickStartedSeconds) * 1000.0;
WorldActorPhaseStartedSeconds = NowSeconds;
}
});
const FDelegateHandle WorldPostActorTickHandle = FWorldDelegates::OnWorldPostActorTick.AddLambda(
[&](UWorld* World, ELevelTick, float)
{
if (World == TestWorld)
{
const double NowSeconds = FPlatformTime::Seconds();
WorldActorPhaseMilliseconds =
(NowSeconds - WorldActorPhaseStartedSeconds) * 1000.0;
WorldPostActorStartedSeconds = NowSeconds;
}
});
const FDelegateHandle WorldTickEndHandle = FWorldDelegates::OnWorldTickEnd.AddLambda(
[&](UWorld* World, ELevelTick, float)
{
if (World == TestWorld)
{
WorldPostActorMilliseconds =
(FPlatformTime::Seconds() - WorldPostActorStartedSeconds) * 1000.0;
}
});
bool bInitialSetReady = false;
double InitialSetReadySeconds = -1.0;
while (FPlatformTime::Seconds() - ActorBeginSeconds < RunDurationSeconds)
{
const double TickStartSeconds = FPlatformTime::Seconds();
const uint64 TickFrame = GFrameCounter;
WorldPreActorMilliseconds = 0.0;
WorldActorPhaseMilliseconds = 0.0;
WorldPostActorMilliseconds = 0.0;
TestWorld->Tick(LEVELTICK_All, 0.016f);
const double TickMilliseconds =
(FPlatformTime::Seconds() - TickStartSeconds) * 1000.0;
@@ -18184,6 +18233,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
const double ElapsedSeconds = FPlatformTime::Seconds() - ActorBeginSeconds;
TickSamples.Emplace(ElapsedSeconds, TickMilliseconds);
const double ReadinessCheckStartSeconds = FPlatformTime::Seconds();
if (!bInitialSetReady && Actor->IsVoxelForgeCoreReady()
&& Actor->bAllChunksLoaded && Actor->PendingTiles.IsEmpty()
&& Actor->PendingCollisionCooks.IsEmpty() && Actor->PendingUnload.IsEmpty()
@@ -18198,8 +18248,30 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin
Actor->LoadedTiles.Num(), Actor->CollisionDesiredTiles.Num(),
Actor->CollisionReadyTiles.Num()));
}
const double ReadinessCheckMilliseconds =
(FPlatformTime::Seconds() - ReadinessCheckStartSeconds) * 1000.0;
const double SleepStartSeconds = FPlatformTime::Seconds();
FPlatformProcess::Sleep(0.016f);
const double SleepMilliseconds =
(FPlatformTime::Seconds() - SleepStartSeconds) * 1000.0;
VoxelForgeStartupTrace::RecordEvent(TEXT("commandlet_world_tick_frame"), FString::Printf(
TEXT("\"frame\":%llu,\"world_tick_ms\":%s,"
"\"world_pre_actor_ms\":%s,\"world_actor_phase_ms\":%s,"
"\"world_post_actor_ms\":%s,"
"\"readiness_check_ms\":%s,\"sleep_ms\":%s,\"loop_ms\":%s"),
static_cast<unsigned long long>(TickFrame),
*FString::SanitizeFloat(TickMilliseconds),
*FString::SanitizeFloat(WorldPreActorMilliseconds),
*FString::SanitizeFloat(WorldActorPhaseMilliseconds),
*FString::SanitizeFloat(WorldPostActorMilliseconds),
*FString::SanitizeFloat(ReadinessCheckMilliseconds),
*FString::SanitizeFloat(SleepMilliseconds),
*FString::SanitizeFloat((FPlatformTime::Seconds() - TickStartSeconds) * 1000.0)));
}
FWorldDelegates::OnWorldTickStart.Remove(WorldTickStartHandle);
FWorldDelegates::OnWorldPreActorTick.Remove(WorldPreActorTickHandle);
FWorldDelegates::OnWorldPostActorTick.Remove(WorldPostActorTickHandle);
FWorldDelegates::OnWorldTickEnd.Remove(WorldTickEndHandle);
for (const TPair<double, double>& Sample : TickSamples)
{