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:
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(>raceLock);
|
||||
if (GState.IsValid() && GState->bActive)
|
||||
{
|
||||
GState->GarbageCollectionStartSeconds = FPlatformTime::Seconds();
|
||||
}
|
||||
});
|
||||
GState->PostGarbageCollectHandle = FCoreUObjectDelegates::GetPostGarbageCollect()
|
||||
.AddLambda([]()
|
||||
{
|
||||
FScopeLock CallbackLock(>raceLock);
|
||||
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(>raceLock);
|
||||
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(>raceLock);
|
||||
@@ -378,6 +441,18 @@ void Finish(const TCHAR* Reason)
|
||||
FScopeLock Lock(>raceLock);
|
||||
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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user