From 2ef0ca72b28de4f02671567cb40ee2404aac41e8 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Thu, 29 Oct 2026 00:09:26 +0100 Subject: [PATCH] streaming: fast-travel near readiness in Unreal (L108) Policy 3 movement traces (12 s move + 5 s dwell, 3 runs each). Unreal 30 m/s: near LOD0-2 ready while moving 31.6 -> 44.2% (LOD0 15.4 -> 34.7%), world tick p99/max 23.1/28.3 -> 11.3/13.6 ms, dwell completion 3.55 -> 3.22 s; far holes 2.5 -> 4.3%. Walking unchanged (~99%). Stale work is small (1.9% of moving LOD0 tiles finish out of range); the remaining limit is sustained generation throughput (8 task slots, p95 worker 105 ms). Near:far stays 4:1. World unchanged (pinned hashes pass). Fast tier PASS; Linux UE builds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- .../VoxelForge/Private/VoxelStartupTrace.cpp | 7 +- Source/VoxelForge/Private/VoxelWorld.cpp | 266 ++++++++-- Source/VoxelForge/Public/VoxelStartupTrace.h | 3 +- Source/VoxelForge/Public/VoxelWorld.h | 3 + .../WorldAssemblerM3/Linux108/REPORT.md | 31 ++ .../Linux108/RunL108Movement.py | 475 ++++++++++++++++++ .../RunL108Movement.cpython-313.pyc | Bin 0 -> 27458 bytes Standalone/Source/StreamBenchmarkPipeline.h | 19 +- Standalone/Source/StreamingWorld.h | 50 +- 9 files changed, 816 insertions(+), 38 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux108/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux108/RunL108Movement.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux108/__pycache__/RunL108Movement.cpython-313.pyc diff --git a/Source/VoxelForge/Private/VoxelStartupTrace.cpp b/Source/VoxelForge/Private/VoxelStartupTrace.cpp index 16aaf1e..c7c4d90 100644 --- a/Source/VoxelForge/Private/VoxelStartupTrace.cpp +++ b/Source/VoxelForge/Private/VoxelStartupTrace.cpp @@ -291,15 +291,16 @@ void RecordEvent(const TCHAR* Type, const FString& FieldsJson) WriteEventLocked(*GState, Type, FieldsJson); } -void RecordRequest(const FTileSample& Sample, uint32 Epoch) +void RecordRequest(const FTileSample& Sample, uint32 Epoch, uint32 DesiredEpoch) { FScopeLock Lock(>raceLock); if (!GState.IsValid() || !GState->bActive) return; WriteEventLocked(*GState, TEXT("request"), FString::Printf( TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," - "\"request_ordinal\":%llu,\"epoch\":%u,\"resident_at_request\":%d"), + "\"request_ordinal\":%llu,\"epoch\":%u,\"desired_epoch\":%u," + "\"resident_at_request\":%d"), Sample.TileX, Sample.TileY, Sample.TileZ, Sample.Level, Sample.WorkPriority, - static_cast(Sample.RequestOrdinal), Epoch, + static_cast(Sample.RequestOrdinal), Epoch, DesiredEpoch, Sample.bResidentAtRequest ? 1 : 0)); } diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 60fe8a4..469a01b 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -10627,16 +10627,31 @@ void AVoxelWorld::QueuePendingUnload(const FVoxelTileKey& Tile) void AVoxelWorld::HideOverlappingDesiredVisuals(const FVoxelTileKey& RetiredTile) { - for (const TPair& Pair : TileComponents) + TArray> OverlappingDesired; + if (const TArray* Descendants = DesiredTransitionDescendants.Find(RetiredTile)) { - if (Pair.Key == RetiredTile || !Pair.Value || !IsDesired(Pair.Key) - || CollisionOnlyTiles.Contains(Pair.Key)) + OverlappingDesired.Append(*Descendants); + } + for (int32 Level = RetiredTile.Level + 1; Level <= DesiredTransitionMaxLevel; ++Level) + { + TArray> Ancestors; + VF_AppendTouchingAncestors(RetiredTile, Level, Ancestors); + for (const FVoxelTileKey& Ancestor : Ancestors) + { + OverlappingDesired.Add(Ancestor); + } + } + for (const FVoxelTileKey& Candidate : OverlappingDesired) + { + if (Candidate == RetiredTile || !IsDesired(Candidate) + || CollisionOnlyTiles.Contains(Candidate) + || !VF_TileFootprintsOverlap(RetiredTile, Candidate)) { continue; } - if (VF_TileFootprintsOverlap(RetiredTile, Pair.Key)) + if (URealtimeMeshComponent* Comp = TileComponents.FindRef(Candidate)) { - Pair.Value->SetVisibility(false); + Comp->SetVisibility(false); } } } @@ -11764,7 +11779,18 @@ void AVoxelWorld::CancelObsoleteTileWork() if (TSharedPtr* Cancellation = PendingTileCancellation.Find(Tile)) { - (*Cancellation)->bObsolete.store(true, std::memory_order_release); + const bool bWasObsolete = (*Cancellation)->bObsolete.exchange( + true, std::memory_order_acq_rel); + if (!bWasObsolete && VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("tile_cancelled"), FString::Printf( + TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," + "\"request_ordinal\":%llu,\"request_epoch\":%u,\"desired_epoch\":%u"), + Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, + (*Cancellation)->WorkPriority, + static_cast((*Cancellation)->RequestOrdinal), + (*Cancellation)->RequestEpoch, DesiredEpoch)); + } } } } @@ -12178,6 +12204,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw const uint64 ResultBacklogCapStopsAtUpdateEntry = StreamingResultBacklogCapStops.load(std::memory_order_relaxed); double DesiredRebuildMilliseconds = 0.0; + double DesiredReconcileMilliseconds = 0.0; double SubmitTilesMilliseconds = 0.0; double SubmitLevelMilliseconds = 0.0; double LevelCandidateScanMilliseconds = 0.0; @@ -12391,10 +12418,15 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Keep several bounded worker-sets ready so frame-paced admission can feed the // background lane continuously without moving mesh application onto the game thread. - const int32 ReadyQueueDepth = FMath::Clamp( + const int32 ConfiguredReadyQueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4)); + const bool bFastTravelAdmission = GVoxelForgeStreamingSchedulerPolicy == 3 + && bSpeedAwareFastMode; + const int32 ReadyQueueDepth = bFastTravelAdmission + ? FMath::Max(ConfiguredReadyQueueDepth, 24) : ConfiguredReadyQueueDepth; const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks); + const int32 FastTravelTotalCap = TotalAdmissionCap; const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition)); FVoxelTileKey WalkableSupportTile; const FIntVector WalkableCenter = GetPlayerSupportTile( @@ -12488,8 +12520,16 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw ? AuthoredNearTaskReserve : ((bPawnMoving || !bWalkableRingReadyForReserve) ? QueueAwareNearTaskReserve : 0); const int32 NearTaskReserve = EffectiveNearTaskReserve; - const int32 NearAdmissionCap = bPawnMoving ? MaxTasks : TotalAdmissionCap; - const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve) + ReadyQueueDepth; + const bool bPolicy3FastTravel = SchedulerPolicy == 3 && bSpeedAwareFastMode; + // Fast travel keeps three quarters of the bounded ready queue for near detail and + // leaves the final quarter open for horizon coverage. The walkable ring is submitted first. + const int32 FastTravelNearCap = MaxTasks + (ReadyQueueDepth * 3) / 4; + const int32 NearAdmissionCap = bPawnMoving + ? (bPolicy3FastTravel ? FastTravelNearCap : MaxTasks) + : TotalAdmissionCap; + const int32 CoarseTaskCap = bPolicy3FastTravel + ? FastTravelTotalCap + : FMath::Max(1, MaxTasks - NearTaskReserve) + ReadyQueueDepth; constexpr int32 FirstFarLevel = 3; const int32 FirstCoarseLevel = FMath::Max( 2, Settings ? Settings->FullResClipLevels : 2); @@ -12583,6 +12623,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw //========================================================================= const bool bDesiredRebuiltThisUpdate = CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild; + const double DesiredReconcileStartSeconds = bTraceStreamingScheduler + ? FPlatformTime::Seconds() : 0.0; if (bDesiredRebuiltThisUpdate) { LastUpdateCenter = CenterChunk; @@ -12826,6 +12868,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // Teardown différé — ProcessUnloadQueue étale les destructions sur plusieurs frames. } } + if (bTraceStreamingScheduler) + { + DesiredReconcileMilliseconds = (FPlatformTime::Seconds() + - DesiredReconcileStartSeconds) * 1000.0; + } //========================================================================= if (bUseVoxelForgeCore && bCoreRegionCompilePending @@ -12969,6 +13016,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw } const uint64 TraceLevelSortStartCycles = bTraceStreamingScheduler ? FPlatformTime::Cycles64() : 0; + const bool bUseFastNearLeadOrder = bPolicy3FastTravel && Level <= 2; std::stable_sort(LevelTiles.GetData(),LevelTiles.GetData()+LevelTiles.Num(), [&](const FVoxelTileKey& A,const FVoxelTileKey& B) { @@ -12982,6 +13030,36 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw (static_cast(B.Coord.Z) + 0.5) * TileSizeCm); const FVector OffsetA = CenterA - PlayerLocalCm; const FVector OffsetB = CenterB - PlayerLocalCm; + if (bUseFastNearLeadOrder && !Heading.IsNearlyZero()) + { + const double ProjectionA = FVector::DotProduct(OffsetA, Heading); + const double ProjectionB = FVector::DotProduct(OffsetB, Heading); + const auto LeadRank = [TileSizeCm](double Projection) + { + if (Projection >= TileSizeCm && Projection <= TileSizeCm * 2.0) return 0; + if (Projection > 0.0 && Projection < TileSizeCm) return 1; + if (Projection > TileSizeCm * 2.0) return 2; + return 3; + }; + const int32 LeadRankA = LeadRank(ProjectionA); + const int32 LeadRankB = LeadRank(ProjectionB); + if (LeadRankA != LeadRankB) return LeadRankA < LeadRankB; + if (LeadRankA == 0) + { + const double TargetLead = TileSizeCm * 1.5; + const double LeadErrorA = FMath::Abs(ProjectionA - TargetLead); + const double LeadErrorB = FMath::Abs(ProjectionB - TargetLead); + if (LeadErrorA != LeadErrorB) return LeadErrorA < LeadErrorB; + } + else if (LeadRankA == 1 && ProjectionA != ProjectionB) + { + return ProjectionA > ProjectionB; + } + else if (LeadRankA == 2 && ProjectionA != ProjectionB) + { + return ProjectionA < ProjectionB; + } + } const bool bAheadA = !Heading.IsNearlyZero() && FVector::DotProduct(OffsetA, Heading) > 0.0; const bool bAheadB = !Heading.IsNearlyZero() && FVector::DotProduct(OffsetB, Heading) > 0.0; if (bAheadA != bAheadB) return bAheadA; @@ -13104,19 +13182,43 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw } // Policy 0 reconstructs L77. Policy 3 enters that order only after sustained fast travel. - const bool bPolicy3FastTravel = SchedulerPolicy == 3 && bSpeedAwareFastMode; - if (SchedulerPolicy == 0 || bPolicy3FastTravel) + if (bPolicy3FastTravel) { - const bool bPolicy3MayAdmit = !bPolicy3FastTravel - || (bWalkableRingReadyForReserve && AreCriticalTilesReady()); - const bool bCoarseFirstForPolicy = bPolicy3FastTravel - ? bWalkableRingReadyForReserve : bUseCoarseFirstOrder; - if (bPolicy3MayAdmit && bCoarseFirstForPolicy) + if (bWalkableRingReadyForReserve && AreCriticalTilesReady()) + { + const int32 ShareAdmissionCap = FastTravelTotalCap; + const int32 StartingAdmissions = GetGenerationAdmissionCount(); + const int32 AvailableAdmissions = FMath::Max( + 0, ShareAdmissionCap - StartingAdmissions); + const int32 NearShareAdmissions = (AvailableAdmissions * 4 + 4) / 5; + const int32 NearShareCap = StartingAdmissions + NearShareAdmissions; + const int32 LOD0ShareCap = StartingAdmissions + (NearShareAdmissions + 1) / 2; + const int32 LOD1ShareCap = StartingAdmissions + + (NearShareAdmissions * 3 + 3) / 4; + if (MaxDesiredLevel >= 0) + { + SubmitLevel(0, EVoxelTileWorkPriority::LOD0, LOD0ShareCap, true); + } + if (MaxDesiredLevel >= 1) + SubmitLevel(1, EVoxelTileWorkPriority::Horizon, LOD1ShareCap, true); + if (MaxDesiredLevel >= 2) + SubmitLevel(2, EVoxelTileWorkPriority::Horizon, NearShareCap, true); + for (int32 Level = MaxDesiredLevel; Level >= FirstFarLevel; --Level) + { + SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, + ShareAdmissionCap, true); + if (GetGenerationAdmissionCount() >= ShareAdmissionCap) break; + } + } + } + else if (SchedulerPolicy == 0) + { + if (bUseCoarseFirstOrder) { for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level) { SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, - CoarseTaskCap, true); + CoarseTaskCap, false); } for (int32 Level = 0; Level < FirstCoarseLevel && Level <= MaxDesiredLevel; ++Level) { @@ -13124,10 +13226,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw ? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon; SubmitLevel(Level, Priority, Priority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : CoarseTaskCap, - true); + false); } } - else if (bPolicy3MayAdmit) + else { SubmitLevel(0, EVoxelTileWorkPriority::LOD0, NearAdmissionCap, false); for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level) @@ -13362,6 +13464,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw "\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"settled_by_lod\":%s," "\"pending_by_lod\":%s," "\"covered_by_lod\":%s," + "\"walkable_ready_tiles\":%d,\"walkable_required_tiles\":9," "\"active_generation\":%d,\"admission_occupancy\":%d," "\"task_cap\":%d,\"near_reserve\":%d," "\"coarse_first\":%d,\"dynamic_near_reserve\":%d," @@ -13373,6 +13476,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw *FString::SanitizeFloat(PlayerHeading.Size()), bWalkableRingReadyForReserve ? 1 : 0, *DesiredCounts, *ResidentCounts, *SettledCounts, *PendingCounts, *CoveredCounts, + WalkableRingReadyCount, GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve, GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, @@ -13529,6 +13633,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw "\"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," + "\"desired_reconcile_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," @@ -13561,6 +13666,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw *FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0), *FString::SanitizeFloat(SchedulerUpdateMilliseconds), *FString::SanitizeFloat(DesiredRebuildMilliseconds), + *FString::SanitizeFloat(DesiredReconcileMilliseconds), *FString::SanitizeFloat(SubmitTilesMilliseconds), *FString::SanitizeFloat(SubmitLevelMilliseconds), *FString::SanitizeFloat(LevelCandidateScanMilliseconds), @@ -13608,16 +13714,26 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor } const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); // Core and worker lane clamped. / Core et voie worker plafonnes. - const int32 ReadyQueueDepth = FMath::Clamp( + const int32 ConfiguredReadyQueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4)); + const bool bFastTravelAdmission = GVoxelForgeStreamingSchedulerPolicy == 3 + && bSpeedAwareFastMode; + const int32 ReadyQueueDepth = bFastTravelAdmission + ? FMath::Max(ConfiguredReadyQueueDepth, 24) : ConfiguredReadyQueueDepth; const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon || Tile.Level <= 2; const int32 NearReserve = EffectiveNearTaskReserve >= 0 ? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1)) : GetNearTaskReserve(MaxTasks); - const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap; - const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth; + const int32 FastTravelTotalCap = TotalAdmissionCap; + const int32 FastTravelNearCap = MaxTasks + (ReadyQueueDepth * 3) / 4; + const int32 NearAdmissionCap = bStreamingPlayerMoving + ? (bFastTravelAdmission ? FastTravelNearCap : MaxTasks) + : TotalAdmissionCap; + const int32 CoarseTaskCap = bFastTravelAdmission + ? FastTravelTotalCap + : FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth; const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon && Tile.Level > 2 ? CoarseTaskCap @@ -13650,7 +13766,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor RequestSample.WorkPriority = static_cast(WorkPriority); RequestSample.RequestOrdinal = RequestOrdinal; RequestSample.bResidentAtRequest = bResidentAtRequest; - VoxelForgeStartupTrace::RecordRequest(RequestSample, GenerationEpoch); + VoxelForgeStartupTrace::RecordRequest(RequestSample, GenerationEpoch, DesiredEpoch); } if (bStartupCollisionGateTimingCaptured && StartupCollisionGateSupportTile.Coord == Tile.Coord @@ -13758,6 +13874,9 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor const TSharedPtr Cancellation = MakeShared(); + Cancellation->RequestOrdinal = RequestOrdinal; + Cancellation->RequestEpoch = GenerationEpoch; + Cancellation->WorkPriority = static_cast(WorkPriority); PendingTileCancellation.Add(Tile, Cancellation); ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); ActiveTileTaskCount.fetch_add(1, std::memory_order_relaxed); @@ -13820,6 +13939,25 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor { return; } + if (VoxelForgeStartupTrace::IsActive()) + { + const uint64 WorkerEndCycles = FPlatformTime::Cycles64(); + VoxelForgeStartupTrace::RecordEvent(TEXT("tile_worker_end"), FString::Printf( + TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," + "\"request_ordinal\":%llu,\"generation_ordinal\":%llu," + "\"epoch\":%u,\"generated\":%d,\"aborted\":%d,\"obsolete\":1," + "\"worker_task_s\":%s"), + Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, + static_cast(WorkPriority), + static_cast(RequestOrdinal), + static_cast(Result.GenerationOrdinal), TaskEpoch, + Result.GenerationEndCycles != 0 && !Result.bAborted ? 1 : 0, + Result.bAborted ? 1 : 0, + *FString::SanitizeFloat(WorkerTaskStartCycles != 0 + && WorkerEndCycles >= WorkerTaskStartCycles + ? FPlatformTime::ToSeconds64(WorkerEndCycles - WorkerTaskStartCycles) + : 0.0))); + } FChunkResult Canceled; Canceled.Tile = Tile; Canceled.Epoch = TaskEpoch; @@ -13901,6 +14039,25 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor if (!ShouldAbortWork()) { Result.WorkerTaskEndCycles = FPlatformTime::Cycles64(); + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("tile_worker_end"), FString::Printf( + TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," + "\"request_ordinal\":%llu,\"generation_ordinal\":%llu," + "\"epoch\":%u,\"generated\":%d,\"aborted\":%d,\"obsolete\":0," + "\"worker_task_s\":%s"), + Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, + static_cast(WorkPriority), + static_cast(RequestOrdinal), + static_cast(Result.GenerationOrdinal), TaskEpoch, + Result.GenerationEndCycles != 0 && !Result.bAborted ? 1 : 0, + Result.bAborted ? 1 : 0, + *FString::SanitizeFloat(WorkerTaskStartCycles != 0 + && Result.WorkerTaskEndCycles >= WorkerTaskStartCycles + ? FPlatformTime::ToSeconds64( + Result.WorkerTaskEndCycles - WorkerTaskStartCycles) + : 0.0))); + } EnqueueStreamingResult(MoveTemp(Result)); // move: don't copy the geometry payload } }, TaskPriority); @@ -13959,16 +14116,26 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority } const int32 MaxTasks = GetMaxConcurrentStreamingTasks(); - const int32 ReadyQueueDepth = FMath::Clamp( + const int32 ConfiguredReadyQueueDepth = FMath::Clamp( GVoxelForgeStreamingReadyQueueDepth, 0, FMath::Min(64, FMath::Max(1, MaxTasks) * 4)); + const bool bFastTravelAdmission = GVoxelForgeStreamingSchedulerPolicy == 3 + && bSpeedAwareFastMode; + const int32 ReadyQueueDepth = bFastTravelAdmission + ? FMath::Max(ConfiguredReadyQueueDepth, 24) : ConfiguredReadyQueueDepth; const int32 TotalAdmissionCap = MaxTasks + ReadyQueueDepth; const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon || Tile.Level <= 2; const int32 NearReserve = EffectiveNearTaskReserve >= 0 ? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1)) : GetNearTaskReserve(MaxTasks); - const int32 NearAdmissionCap = bStreamingPlayerMoving ? MaxTasks : TotalAdmissionCap; - const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth; + const int32 FastTravelTotalCap = TotalAdmissionCap; + const int32 FastTravelNearCap = MaxTasks + (ReadyQueueDepth * 3) / 4; + const int32 NearAdmissionCap = bStreamingPlayerMoving + ? (bFastTravelAdmission ? FastTravelNearCap : MaxTasks) + : TotalAdmissionCap; + const int32 CoarseTaskCap = bFastTravelAdmission + ? FastTravelTotalCap + : FMath::Max(1, MaxTasks - NearReserve) + ReadyQueueDepth; const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon && Tile.Level > 2 ? CoarseTaskCap @@ -13993,6 +14160,9 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority const uint32 TaskModificationEpoch = ModificationEpoch; const uint64 RequestStartCycles = FPlatformTime::Cycles64(); const uint64 RequestOrdinal = ++NextStreamingRequestOrdinal; + Cancellation->RequestOrdinal = RequestOrdinal; + Cancellation->RequestEpoch = TaskEpoch; + Cancellation->WorkPriority = static_cast(WorkPriority); const bool bResidentAtRequest = LoadedTiles.Contains(Tile); if (VoxelForgeStartupTrace::IsActive()) { @@ -14004,7 +14174,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority RequestSample.WorkPriority = static_cast(WorkPriority); RequestSample.RequestOrdinal = RequestOrdinal; RequestSample.bResidentAtRequest = bResidentAtRequest; - VoxelForgeStartupTrace::RecordRequest(RequestSample, TaskEpoch); + VoxelForgeStartupTrace::RecordRequest(RequestSample, TaskEpoch, TaskDesiredEpoch); } const bool bCaptureHashes = bCaptureVoxelForgeCoreHashesForValidation; const bool bLegacyDirectMappedColumnCache=GVoxelForgeCoreColumnCacheDirectMapped!=0; @@ -14048,10 +14218,30 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority } } WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles}; - auto Enqueue = [this, WorkPriority](FChunkResult&& Result) + auto Enqueue = [this, WorkPriority, Cancellation](FChunkResult&& Result) { Result.WorkPriority = WorkPriority; Result.WorkerTaskEndCycles = FPlatformTime::Cycles64(); + if (VoxelForgeStartupTrace::IsActive() && !Result.bObsolete) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("tile_worker_end"), FString::Printf( + TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," + "\"request_ordinal\":%llu,\"generation_ordinal\":%llu," + "\"epoch\":%u,\"generated\":%d,\"aborted\":%d,\"obsolete\":%d," + "\"worker_task_s\":%s"), + Result.Tile.Coord.X, Result.Tile.Coord.Y, Result.Tile.Coord.Z, + Result.Tile.Level, static_cast(WorkPriority), + static_cast(Result.RequestOrdinal), + static_cast(Result.GenerationOrdinal), Result.Epoch, + Result.GenerationEndCycles != 0 && !Result.bAborted ? 1 : 0, + Result.bAborted ? 1 : 0, + Cancellation->bObsolete.load(std::memory_order_acquire) ? 1 : 0, + *FString::SanitizeFloat(Result.WorkerTaskStartCycles != 0 + && Result.WorkerTaskEndCycles >= Result.WorkerTaskStartCycles + ? FPlatformTime::ToSeconds64( + Result.WorkerTaskEndCycles - Result.WorkerTaskStartCycles) + : 0.0))); + } EnqueueStreamingResult(MoveTemp(Result)); }; @@ -14094,10 +14284,28 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Canceled.WorkerThreadId = Result.WorkerThreadId; Canceled.bResidentAtRequest = bResidentAtRequest; Canceled.GenerationStartCycles = Result.GenerationStartCycles; - Canceled.GenerationEndCycles = FPlatformTime::Cycles64(); + Canceled.GenerationEndCycles = Result.GenerationEndCycles; Canceled.WorkPriority = WorkPriority; Canceled.bAborted = true; Canceled.bObsolete = true; + if (VoxelForgeStartupTrace::IsActive()) + { + const uint64 WorkerEndCycles = FPlatformTime::Cycles64(); + VoxelForgeStartupTrace::RecordEvent(TEXT("tile_worker_end"), FString::Printf( + TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d," + "\"request_ordinal\":%llu,\"generation_ordinal\":%llu," + "\"epoch\":%u,\"generated\":%d,\"aborted\":1,\"obsolete\":1," + "\"worker_task_s\":%s"), + Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, + static_cast(WorkPriority), + static_cast(RequestOrdinal), + static_cast(Result.GenerationOrdinal), TaskEpoch, + Result.GenerationEndCycles != 0 && !Result.bAborted ? 1 : 0, + *FString::SanitizeFloat(WorkerTaskStartCycles != 0 + && WorkerEndCycles >= WorkerTaskStartCycles + ? FPlatformTime::ToSeconds64(WorkerEndCycles - WorkerTaskStartCycles) + : 0.0))); + } if (!bShuttingDown.load(std::memory_order_acquire) && !State->bShuttingDown.load(std::memory_order_acquire)) { diff --git a/Source/VoxelForge/Public/VoxelStartupTrace.h b/Source/VoxelForge/Public/VoxelStartupTrace.h index 6b10f55..2a6b114 100644 --- a/Source/VoxelForge/Public/VoxelStartupTrace.h +++ b/Source/VoxelForge/Public/VoxelStartupTrace.h @@ -86,7 +86,8 @@ namespace VoxelForgeStartupTrace /** FieldsJson is the contents after the type/timestamp object prefix, without braces. */ VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString()); - VOXELFORGE_API void RecordRequest(const FTileSample& Sample, uint32 Epoch); + VOXELFORGE_API void RecordRequest(const FTileSample& Sample, uint32 Epoch, + uint32 DesiredEpoch = 0); VOXELFORGE_API void StageBegin(const TCHAR* Name); VOXELFORGE_API void StageEnd(const TCHAR* Name); diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index 78bebd8..b9fa34c 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -52,6 +52,9 @@ struct FVoxelForgeCoreTileHashRecord struct FVoxelTileCancellationState { std::atomic bObsolete{false}; + uint64 RequestOrdinal = 0; + uint32 RequestEpoch = 0; + int32 WorkPriority = 0; }; /** diff --git a/Standalone/Research/WorldAssemblerM3/Linux108/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux108/REPORT.md new file mode 100644 index 0000000..2187e63 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux108/REPORT.md @@ -0,0 +1,31 @@ +# Linux108 movement results + +Baseline is `ef22b03`. Policy 3; three runs per speed and platform; 12 s moving plus 5 s dwell. Percentages and dwell times are the median with the three-run range. UE tick is `commandlet_world_tick_frame.world_tick_ms`; headless records sampled queue-update work, not frame ticks. Tick/update values are medians of the per-run p99 and max. Columns show baseline → current. + +| Speed / platform | Near LOD0–2 | Walkable ring | Far holes | Tick or update p99 / max (ms) | Dwell complete (s) | +|---|---:|---:|---:|---:|---:| +| 1.4 m/s / headless | 89.40 [89.23–89.43] → 89.57 [89.43–89.85] | 97.92 → 97.92 | 13.72 [13.59–14.11] → 13.46 [12.98–13.69] | 4.37 / 4.37 → 4.44 / 4.44 | 0.00 → 0.00 | +| 1.4 m/s / UE | 99.27 [99.21–99.28] → 99.31 [99.28–99.43] | 100 → 100 | 0.62 [0.56–0.85] → 1.26 [1.18–1.36] | 11.48 / 26.39 → 6.25 / 14.28 | 0.07 [0.04–0.18] → 0.02 [0.02–0.24] | +| 30 m/s / headless | 80.64 [80.60–81.10] → 82.61 [82.41–82.77] | 95.14 → 96.53 [96.30–96.53] | 6.55 [6.35–6.64] → 14.42 [14.14–14.66] | 4.22 / 4.22 → 4.35 / 4.35 | 0.25 → 0.25 | +| 30 m/s / UE | 31.55 [29.13–31.83] → 44.24 [43.85–45.70] | 100 → 100 | 2.52 [2.14–3.15] → 4.25 [4.03–4.66] | 23.11 / 28.29 → 11.29 / 13.60 | 3.55 [3.44–3.61] → 3.22 [3.18–3.29] | + +UE per-LOD readiness, baseline → current (median [range]): + +| Speed | LOD0 | LOD1 | LOD2 | +|---|---:|---:|---:| +| 1.4 m/s | 99.02 [99.00–99.06] → 99.11 [99.06–99.14] | 99.79 [99.77–99.82] → 99.84 [99.82–99.84] | 98.90 [98.83–99.04] → 98.92 [98.90–99.38] | +| 30 m/s | 15.42 [13.16–15.49] → 34.68 [34.50–35.94] | 25.62 [23.29–26.44] → 42.06 [41.44–43.91] | 59.07 [57.33–61.11] → 59.60 [59.20–60.88] | + +## What the traces show + +At 30 m/s, the scheduler changes desired center 45 times per run, about once per 8 m tile crossing. 88.4% of moving LOD0 admissions are ahead of the heading; median lead is 13.2 m (p95 27.6 m). Of 4,835 generated LOD0 tiles admitted during movement, 94 (1.94%) finish outside the desired range. 29 (0.60%) are cancelled before worker completion. Stale work is not the main readiness loss. + +LOD0 readiness starts at 83–85% in the first two seconds and falls to 14–16% in the final two. About 512–516 LOD0 tiles remain desired; the trace shows a mean of about 328 missing. The worker task cap is 8, with p95 worker time 105 ms and p95 request-to-apply time 155 ms. This points to sustained generation/backlog throughput as the remaining fast-travel limit. The 2 ms apply budget is rarely hit (4–7 frames per run); tile-apply max is under 2.5 ms. `UWorld::Tick` max is 14.7 ms; the custom streaming-work tick max is 13.7 ms. The commandlet loop includes a deliberate ~16 ms sleep and is excluded from these tick values. + +Fast near readiness improved by 12.7 points, but remains 44.2% in UE versus 82.6% headless. Near-first service also costs far coverage: 30 m/s far holes rose from 2.52% to 4.25% in UE and 6.55% to 14.42% headless. A headless 2:1 near/far trial measured 80.1% near and 14.8% far holes, so the final scheduler retains 4:1. Walk/fast hysteresis remains 75/35 and 1200/800 cm/s. + +## Checks + +The Unreal build succeeded with `-NoUBA -MaxParallelActions=4` before timing. Unreal timings used `taskset` only; headless used `taskset` plus the 6 GiB `prlimit`. The final headless build stamp is `f3ec37a389bb6c984bac0fa360b96e49b6b53fbda92c6461c1246b690e21afaf`, matching the fast-tier manifest. Fast tier passed: 274 checks, 80 runs; pinned L100 hashes and W1/W8/W12 comparisons passed. `git diff --check` is clean. No passage-support or mesher geometry semantics changed. + +Artifacts: `Saved/Linux/l108/movement/{baseline,after}/summary.json`, `after/tile-timelines.csv`, Unreal traces, and `Saved/Linux/l108/verify-fast/fast-summary.json`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux108/RunL108Movement.py b/Standalone/Research/WorldAssemblerM3/Linux108/RunL108Movement.py new file mode 100644 index 0000000..436f6cf --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux108/RunL108Movement.py @@ -0,0 +1,475 @@ +#!/usr/bin/env python3 +"""Three-run L80 movement protocol for headless and Unreal, policy 3 only.""" +from __future__ import annotations + +import argparse +import csv +import json +import math +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +HOST = Path("/home/jahni/VFHost") +EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd") +DATA = ROOT / "Standalone/data/owner_world.json" +HEADLESS = { + "baseline": ROOT / "Saved/Linux/l108/bin/VoxelForgeCoreHeadless-l108-baseline-clang", + "after": ROOT / "Saved/Linux/l108/bin/VoxelForgeCoreHeadless-l108-after-clang", +} +OUT = ROOT / "Saved/Linux/l108/movement" +HOST_OUT = HOST / "Plugins/VoxelForge/Saved/Linux/l108/movement" +SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375) +SPEEDS = {"walk": 1.4, "fast": 30.0} +SPEED_CM_S = {"walk": 140.0, "fast": 3000.0} +TRACE_EVENT = re.compile(r"^\{.*\}$") +HEADLESS_SAMPLE = re.compile(r"^\[L79MovingSample\] (.*)$", re.M) +HEADLESS_SUMMARY = re.compile(r"^\[L79MovingSummary\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") + + +def p99(values: list[float]) -> float | None: + if not values: + return None + values.sort() + return values[max(0, math.ceil(0.99 * len(values)) - 1)] + + +def mean(values: list[float]) -> float | None: + return sum(values) / len(values) if values else None + + +def parse_events(path: Path) -> list[dict]: + events = [] + if not path.is_file(): + return events + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + if not TRACE_EVENT.match(line): + continue + try: + events.append(json.loads(line)) + except json.JSONDecodeError: + pass + return events + + +def summarize_rows(rows: list[dict], moving_key: str = "move") -> dict: + moving = [r for r in rows if r["phase"] == moving_key] + dwell = [r for r in rows if r["phase"] == "dwell"] + dwell_end = dwell[-1] if dwell else {} + full_dwell = next((r for r in dwell + if r["near_ready_pct"] >= 99.999 + and r["far_hole_pct"] <= 0.001), None) + return { + "moving_samples": len(moving), + "dwell_samples": len(dwell), + "near_ready_moving_mean_pct": mean([r["near_ready_pct"] for r in moving]), + "lod0_ready_moving_mean_pct": mean([r["lod0_ready_pct"] for r in moving if "lod0_ready_pct" in r]), + "lod1_ready_moving_mean_pct": mean([r["lod1_ready_pct"] for r in moving if "lod1_ready_pct" in r]), + "lod2_ready_moving_mean_pct": mean([r["lod2_ready_pct"] for r in moving if "lod2_ready_pct" in r]), + "walkable_ring_moving_mean_pct": mean([r["walkable_ring_ready_pct"] for r in moving]), + "far_holes_moving_mean_pct": mean([r["far_hole_pct"] for r in moving]), + "update_work_p99_ms": p99([float(r["queue_update_ms"]) for r in moving + if "queue_update_ms" in r]), + "update_work_max_ms": max([float(r["queue_update_ms"]) for r in moving + if "queue_update_ms" in r], default=None), + "near_ready_dwell_end_pct": dwell_end.get("near_ready_pct"), + "walkable_ring_dwell_end_pct": dwell_end.get("walkable_ring_ready_pct"), + "far_holes_dwell_end_pct": dwell_end.get("far_hole_pct"), + "dwell_complete_after_stop_s": full_dwell.get("since_stop_s") if full_dwell else None, + } + + +def run_headless(phase: str, speed_name: str, repetition: int) -> dict: + label = f"{speed_name}-r{repetition}" + folder = OUT / phase / "headless" + folder.mkdir(parents=True, exist_ok=True) + log = folder / f"{label}.log" + camera = ",".join(f"{v:.12f}" for v in SPAWN_M) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(HEADLESS[phase]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--stream-scheduler-policy=3", + f"--moving-camera-speed-mps={SPEEDS[speed_name]:.1f}", + "--moving-camera-duration-s=12", "--moving-camera-dwell-s=5", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + rows = [] + for match in HEADLESS_SAMPLE.finditer(content): + row = dict(FIELDS.findall(match.group(1))) + row.update({key: float(row[key]) for key in ( + "t_s", "near_ready_pct", "walkable_ring_ready_pct", "far_hole_pct", + "queue_update_ms")}) + row["phase"] = row["phase"] + row["walkable_ring_ready_pct"] = float(row.get("walkable_ring_ready_pct", 0.0)) + row["since_stop_s"] = max(0.0, float(row["t_s"]) - 12.0) + rows.append(row) + summary_match = HEADLESS_SUMMARY.search(content) + engine_summary = dict(FIELDS.findall(summary_match.group(1))) if summary_match else {} + if result.returncode or not rows or int(engine_summary.get("generation_failures", "-1")) != 0: + raise RuntimeError(f"headless movement run failed: {log}; exit={result.returncode}") + info = { + "phase": phase, "platform": "headless", "speed": speed_name, + "speed_mps": SPEEDS[speed_name], "repetition": repetition, + "exit_code": result.returncode, "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), **summarize_rows(rows), + } + return info + + +def run_unreal(phase: str, speed_name: str, repetition: int) -> tuple[dict, list[dict], list[dict]]: + label = f"{speed_name}-r{repetition}" + folder = OUT / phase / "unreal" + host_folder = HOST_OUT / phase / "unreal" + folder.mkdir(parents=True, exist_ok=True) + trace = folder / f"{label}.trace.jsonl" + abslog = folder / f"{label}.log" + stdout = folder / f"{label}.stdout.log" + export = HOST / "Plugins/VoxelForge/Saved/Linux/l108/output" / f"{phase}-{label}" + userdir = HOST / "Saved/Linux/l108/user" / f"{phase}-{label}" + zendata = HOST / "Saved/Linux/l108/zen" / f"{phase}-{label}" + ddc = HOST / "Saved/Linux/l108/DDC" + for path in (export, userdir, zendata, ddc): + path.mkdir(parents=True, exist_ok=True) + spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M) + command = [ + str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended", + "-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch", + "-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer", + "-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}", + f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0", + "-archetype=TunnelNetwork", "-slot=0", "-modes=export", "-gameconfig=1", + "-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25", + "-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0", "-coreplantest=0", + "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings", "-corehybridmesher=0", + f"-out={export}", "-voxel.StreamingCoarseFirst=1", + "-voxel.StreamingDynamicNearReserve=1", "-voxel.StreamingLegacyPendingResultCap=0", + "-voxel.CoreRegionConcurrentAdmission=1", "-voxel.StreamingResultBacklogCap=256", + "-voxel.StreamingReadyQueueDepth=8", "-voxel.StreamingBackgroundWorkerCap=1", + "-voxel.CoreColumnCacheDirectMapped=0", "-voxel.CoreMCGradientMemoization=1", + "-voxel.StreamingCollisionLookahead=1", "-voxel.TestMove=1", + f"-voxel.TestMoveSpeedCmPerSecond={SPEED_CM_S[speed_name]:.1f}", + "-voxel.TestMoveStartSeconds=6.0", "-voxel.TestMoveDurationSeconds=12.0", + "-voxel.TestExitSeconds=23.0", "-voxel.TestMoveDirection=1,0,0", + "-voxel.StreamingSchedulerPolicy=3", "-voxel.TestExitOnSteady=0", + "-coreworldstreamtrace=1", f"-voxel.StartupTraceSpawnCm={spawn_cm}", + "-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1", + "-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}", + f"-abslog={abslog}", + ] + wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command] + started = time.monotonic() + with stdout.open("w", encoding="utf-8") as stream: + result = subprocess.run(wrapped, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + events = parse_events(trace) + samples = [event for event in events if event.get("type") == "streaming_sample"] + move_samples = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0] + move_start_t = min((float(event.get("t_s", 0.0)) for event in move_samples), default=float("inf")) + stop = next((event for event in events + if event.get("type") == "streaming_test_move_stopped"), None) + stop_t = float(stop.get("t_s", 0.0)) if stop else float("inf") + dwell_samples = [event for event in samples + if float(event.get("t_s", 0.0)) >= stop_t + and float(event.get("speed_cm_s", 0.0)) < 50.0] + rows = [] + for event in samples: + desired = event.get("desired_by_lod", [0] * 9) + resident = event.get("resident_by_lod", [0] * 9) + covered = event.get("covered_by_lod", resident) + desired_near = sum(desired[:3]) + resident_near = sum(resident[:3]) + desired_far = sum(desired[3:]) + covered_far = sum(covered[3:]) + event_time = float(event.get("t_s", 0.0)) + speed = float(event.get("speed_cm_s", 0.0)) + row_phase = "move" if speed >= 50.0 else ( + "dwell" if event_time >= stop_t else "arrival") + ring_pct = 100.0 * float(event.get("walkable_ready_tiles", 0)) / max( + 1.0, float(event.get("walkable_required_tiles", 9))) + rows.append({ + "t_s": event_time, "speed_cm_s": speed, "phase": row_phase, + "since_stop_s": max(0.0, event_time - stop_t) if row_phase == "dwell" else None, + "near_ready_pct": 100.0 * resident_near / max(1, desired_near), + "lod0_ready_pct": 100.0 * resident[0] / max(1, desired[0]), + "lod1_ready_pct": 100.0 * resident[1] / max(1, desired[1]), + "lod2_ready_pct": 100.0 * resident[2] / max(1, desired[2]), + "walkable_ring_ready_pct": ring_pct, + "far_hole_pct": 100.0 * (desired_far - covered_far) / max(1, desired_far), + "near_desired": desired_near, "near_resident": resident_near, + "walkable_ready_tiles": event.get("walkable_ready_tiles"), + "epoch": event.get("epoch"), + }) + if len(move_samples) < 40 or len(dwell_samples) < 16: + raise RuntimeError(f"incomplete Unreal movement trace: {trace}; " + f"move={len(move_samples)} dwell={len(dwell_samples)} " + f"exit={result.returncode}") + frame_samples = [event for event in events if event.get("type") == "apply_frame" + and move_start_t <= float(event.get("t_s", 0.0)) < stop_t] + tick_samples = [event for event in events if event.get("type") == "game_thread_work_frame" + and move_start_t <= float(event.get("t_s", 0.0)) < stop_t] + tick_ms = [float(event.get("tick_ms", 0.0)) for event in tick_samples] + update_ms = [float(event.get("update_chunks_ms", 0.0)) for event in tick_samples] + drain_ms = [float(event.get("pending_drain_ms", 0.0)) for event in tick_samples] + apply_ms = [float(event.get("apply_ms", 0.0)) for event in frame_samples] + gate_epochs = [event for event in events if event.get("type") == "walkable_gate" + and move_start_t <= float(event.get("t_s", 0.0)) < stop_t] + summary = summarize_rows(rows) + summary.update({ + "game_thread_tick_p99_ms": p99(tick_ms), "game_thread_tick_max_ms": max(tick_ms, default=None), + "stream_update_p99_ms": p99(update_ms), "stream_update_max_ms": max(update_ms, default=None), + "pending_drain_p99_ms": p99(drain_ms), "pending_drain_max_ms": max(drain_ms, default=None), + "tile_apply_p99_ms": p99(apply_ms), "tile_apply_max_ms": max(apply_ms, default=None), + "apply_budget_ms": max((float(event.get("apply_budget_ms", 0.0)) + for event in frame_samples), default=None), + "apply_budget_hit_frames": sum(int(event.get("time_limit_reached", 0)) + for event in frame_samples), + "desired_epochs_during_move": len({event.get("desired_epoch") for event in gate_epochs}), + "desired_centers_during_move": len({tuple(event.get("support_center", [])) for event in gate_epochs}), + }) + info = { + "phase": phase, "platform": "unreal", "speed": speed_name, + "speed_mps": SPEEDS[speed_name], "repetition": repetition, + "exit_code": result.returncode, "wall_s": round(time.monotonic() - started, 3), + "moving_samples": len(move_samples), "dwell_samples": len(dwell_samples), + "trace": str(trace.relative_to(ROOT)), "stdout": str(stdout.relative_to(ROOT)), + **summary, + } + return info, events, rows + + +def write_tile_timelines(phase: str, unreal_runs: list[tuple[dict, list[dict]]]) -> dict: + path = OUT / phase / "tile-timelines.csv" + path.parent.mkdir(parents=True, exist_ok=True) + columns = ["speed", "repetition", "ordinal", "tile", "level", "priority", "epoch", + "desired_epoch", "in_move_window", + "request_s", "cancel_s", "worker_end_s", "generated", "aborted", "obsolete", + "applied_s", "request_to_apply_s", "worker_queue_s", "worker_task_s", + "result_queue_s", "discard_s", "discard_reason", "ahead_m_at_request", + "request_to_worker_end_s", "cancel_before_worker_end", + "out_of_range_at_worker_end"] + stale_by_run = {} + with path.open("w", newline="", encoding="utf-8") as stream: + writer = csv.DictWriter(stream, fieldnames=columns) + writer.writeheader() + for run, events in unreal_runs: + requests, cancelled, finished, applied, discarded = {}, {}, {}, {}, {} + samples = sorted((event for event in events if event.get("type") == "streaming_sample"), + key=lambda event: float(event.get("t_s", 0.0))) + move_samples = [event for event in samples + if float(event.get("speed_cm_s", 0.0)) >= 50.0] + move_start_s = min((float(event.get("t_s", 0.0)) for event in move_samples), + default=float("inf")) + move_stop = next((event for event in events + if event.get("type") == "streaming_test_move_stopped"), None) + move_stop_s = float(move_stop.get("t_s", float("inf"))) if move_stop else float("inf") + + def pawn_x_at(t_s: float) -> float | None: + if not samples: + return None + before = None + for sample in samples: + sample_t = float(sample.get("t_s", 0.0)) + if sample_t >= t_s: + if before is None: + return float(sample.get("position_cm", [0.0])[0]) / 100.0 + t0 = float(before.get("t_s", 0.0)) + t1 = sample_t + x0 = float(before.get("position_cm", [0.0])[0]) / 100.0 + x1 = float(sample.get("position_cm", [0.0])[0]) / 100.0 + alpha = 0.0 if t1 <= t0 else (t_s - t0) / (t1 - t0) + return x0 + alpha * (x1 - x0) + before = sample + return float(samples[-1].get("position_cm", [0.0])[0]) / 100.0 + + for event in events: + ordinal = event.get("request_ordinal") + if ordinal is None: + continue + ordinal = int(ordinal) + kind = event.get("type") + if kind == "request": + requests[ordinal] = event + elif kind == "tile_cancelled": + cancelled[ordinal] = event + elif kind == "tile_worker_end": + finished[ordinal] = event + elif kind == "tile": + applied[ordinal] = event + elif kind == "tile_discard": + discarded[ordinal] = event + + generated_l0 = 0 + stale_l0 = 0 + out_of_range_l0 = 0 + l0_cancelled_while_running = 0 + ahead_distances = [] + gate_epochs = sorted((event for event in events if event.get("type") == "walkable_gate"), + key=lambda event: float(event.get("t_s", 0.0))) + + def desired_epoch_at(t_s: float) -> int | None: + previous = [event for event in gate_epochs if float(event.get("t_s", 0.0)) <= t_s] + return int(previous[-1]["desired_epoch"]) if previous else None + + for ordinal, request in requests.items(): + key = request.get("tile", [None, None, None]) + level = int(request.get("level", -1)) + cancel = cancelled.get(ordinal) + end = finished.get(ordinal) + tile = applied.get(ordinal) + discard = discarded.get(ordinal) + request_t = float(request.get("t_s", 0.0)) + end_t = float(end.get("t_s", 0.0)) if end else None + cancel_t = float(cancel.get("t_s", 0.0)) if cancel else None + # LOD0 clip tiles are 8 m across; the live x-range is [center-4, center+3]. + if end and int(end.get("generated", 0)) == 1 and end_t is not None: + finish_x = pawn_x_at(end_t) + center_x = math.floor(finish_x / 8.0) if finish_x is not None else 0 + physically_out = bool(level == 0 and finish_x is not None and key[0] is not None + and (int(key[0]) < center_x - 4 + or int(key[0]) > center_x + 3)) + else: + physically_out = False + in_move_window = bool(end_t is not None and move_start_s <= end_t < move_stop_s) + if level == 0 and end and int(end.get("generated", 0)) == 1 and in_move_window: + generated_l0 += 1 + stale_l0 += int(cancel_t is not None and cancel_t < end_t) + out_of_range_l0 += int(physically_out) + if level == 0 and end and int(end.get("aborted", 0)) == 1 \ + and cancel_t is not None and end_t is not None and cancel_t < end_t: + l0_cancelled_while_running += 1 + pawn_x = pawn_x_at(request_t) + extent_m = 8.0 * (1 << max(0, level)) + tile_center_m = (float(key[0]) + 0.5) * extent_m if key[0] is not None else None + ahead_m = (tile_center_m - pawn_x) if tile_center_m is not None and pawn_x is not None else None + if level == 0 and move_start_s <= request_t < move_stop_s and ahead_m is not None: + ahead_distances.append(ahead_m) + writer.writerow({ + "speed": run["speed"], "repetition": run["repetition"], + "ordinal": ordinal, "tile": ",".join(str(v) for v in key), + "level": level, "priority": request.get("priority"), + "epoch": request.get("epoch"), + "desired_epoch": request.get("desired_epoch", desired_epoch_at(request_t)), + "in_move_window": int(move_start_s <= request_t < move_stop_s), + "request_s": request_t, + "cancel_s": cancel_t, "worker_end_s": end_t, + "generated": end.get("generated") if end else None, + "aborted": end.get("aborted") if end else None, + "obsolete": end.get("obsolete") if end else None, + "applied_s": float(tile.get("t_s", 0.0)) if tile else None, + "request_to_apply_s": tile.get("request_to_apply_s") if tile else None, + "worker_queue_s": tile.get("worker_queue_s") if tile else None, + "worker_task_s": tile.get("worker_task_s") if tile else ( + end.get("worker_task_s") if end else None), + "result_queue_s": tile.get("result_queue_s") if tile else None, + "discard_s": float(discard.get("t_s", 0.0)) if discard else None, + "discard_reason": discard.get("reason") if discard else None, + "ahead_m_at_request": ahead_m, + "request_to_worker_end_s": end_t - request_t if end_t is not None else None, + "cancel_before_worker_end": int(bool(cancel_t is not None + and end_t is not None + and cancel_t < end_t)), + "out_of_range_at_worker_end": int(physically_out), + }) + key = f"{run['speed']}-r{run['repetition']}" + stale_by_run[key] = { + "generated_lod0_tiles_finished_during_move": generated_l0, + "generated_lod0_out_of_range_at_finish": out_of_range_l0, + "out_of_range_fraction_pct": 100.0 * out_of_range_l0 / max(1, generated_l0), + "generated_lod0_finished_after_cancel": stale_l0, + "cancel_before_finish_fraction_pct": 100.0 * stale_l0 / max(1, generated_l0), + "lod0_cancelled_while_worker_active": l0_cancelled_while_running, + "lod0_admissions_in_move": len(ahead_distances), + "lod0_ahead_at_admission_pct": 100.0 * sum(value > 0.0 for value in ahead_distances) + / max(1, len(ahead_distances)), + "lod0_ahead_median_m": statistics.median(ahead_distances) if ahead_distances else None, + } + return {"tile_timelines_csv": str(path.relative_to(ROOT)), "stale_by_run": stale_by_run} + + +def run_phase(phase: str) -> None: + partial_path = OUT / phase / "runs-partial.json" + results = [] + if partial_path.is_file(): + try: + stored = json.loads(partial_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + stored = [] + for item in stored: + if item.get("phase") != phase or int(item.get("exit_code", -1)) != 0: + continue + if item.get("platform") == "unreal": + trace = ROOT / item.get("trace", "") + if not trace.is_file() or not Path(str(trace) + ".done").is_file(): + continue + elif item.get("platform") == "headless": + log = ROOT / item.get("log", "") + if not log.is_file() or "[L79MovingSummary]" not in log.read_text( + encoding="utf-8", errors="replace"): + continue + content = log.read_text(encoding="utf-8", errors="replace") + update_ms = [] + for match in HEADLESS_SAMPLE.finditer(content): + row = dict(FIELDS.findall(match.group(1))) + if row.get("phase") == "move" and "queue_update_ms" in row: + update_ms.append(float(row["queue_update_ms"])) + item["update_work_p99_ms"] = p99(update_ms) + item["update_work_max_ms"] = max(update_ms, default=None) + results.append(item) + completed = {(item["platform"], item["speed"], item["repetition"]) + for item in results} + unreal_runs = [] + for item in results: + if item["platform"] == "unreal": + trace = ROOT / item["trace"] + unreal_runs.append((item, parse_events(trace))) + + def save_partial() -> None: + partial_path.parent.mkdir(parents=True, exist_ok=True) + partial_path.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + + for repetition in range(1, 4): + speed_order = ("walk", "fast") if repetition % 2 else ("fast", "walk") + for speed_name in speed_order: + headless_key = ("headless", speed_name, repetition) + if headless_key not in completed: + headless_info = run_headless(phase, speed_name, repetition) + results.append(headless_info) + completed.add(headless_key) + save_partial() + print("[L108MovementRun] " + json.dumps(headless_info, sort_keys=True), flush=True) + unreal_key = ("unreal", speed_name, repetition) + if unreal_key not in completed: + run_info, events, _rows = run_unreal(phase, speed_name, repetition) + results.append(run_info) + unreal_runs.append((run_info, events)) + completed.add(unreal_key) + save_partial() + print("[L108MovementRun] " + json.dumps(run_info, sort_keys=True), flush=True) + timeline = write_tile_timelines(phase, unreal_runs) + (OUT / phase / "summary.json").write_text( + json.dumps({"phase": phase, "runs": results, "timeline": timeline}, indent=2) + "\n", + encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("phase", choices=("baseline", "after")) + args = parser.parse_args() + if not DATA.is_file() or not HEADLESS[args.phase].is_file() or not EDITOR.is_file(): + raise SystemExit("missing movement runner input, headless executable or Unreal editor") + run_phase(args.phase) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux108/__pycache__/RunL108Movement.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux108/__pycache__/RunL108Movement.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70236ea33cbb3c83e4546ca5c24243366387b461 GIT binary patch literal 27458 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 literal 0 HcmV?d00001 diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 6907d10..a4c3d18 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -5631,6 +5631,7 @@ namespace StandaloneStreamBenchmark return 9; } std::size_t NearDesired=0,NearResident=0,FarDesired=0,FarResident=0,FarCovered=0; + std::size_t WalkableRingResident=0; double FarStaleAgeSum=0.0,FarStaleAgeMax=0.0; const auto FloorDiv=[](int32 Value,int32 Divisor) { @@ -5672,6 +5673,19 @@ namespace StandaloneStreamBenchmark } } } + for(int32 Dy=-1;Dy<=1;++Dy) + for(int32 Dx=-1;Dx<=1;++Dx) + { + const FVoxelTileKey WalkableKey( + FIntVector(CurrentDesired.CenterChunk.X+Dx, + CurrentDesired.CenterChunk.Y+Dy, + CurrentDesired.CenterChunk.Z),0); + const bool bWalkableKeyDesired=std::find( + CurrentDesired.Tiles.begin(),CurrentDesired.Tiles.end(),WalkableKey) + !=CurrentDesired.Tiles.end(); + if(bWalkableKeyDesired&&Resident.contains(WalkableKey)) + ++WalkableRingResident; + } FarMissingSince.swap(CurrentFarMissing); const double NearReadyPercent=100.0*static_cast(NearResident) /static_cast(std::max(1,NearDesired)); @@ -5681,12 +5695,15 @@ namespace StandaloneStreamBenchmark /static_cast(std::max(1,FarDesired)); const double FarStaleAgeMean=FarDesired>FarResident ?FarStaleAgeSum/static_cast(FarDesired-FarResident):0.0; + const double WalkableRingReadyPercent=100.0 + *static_cast(WalkableRingResident)/9.0; if(FarDesired>0&&FarDesired==FarResident&&FirstAllFarReadySeconds<0.0) FirstAllFarReadySeconds=ElapsedSeconds; const char* Phase=ElapsedSeconds PassageScores; if(!PassagePrioritySegments.empty()) @@ -1026,6 +1027,40 @@ private: const std::size_t ScoreB=PassageScores.at(B); if(ScoreA!=ScoreB)return ScoreA>ScoreB; } + if(SpeedAwareMode==2&&bNearFirst + &&TravelDirection.X*TravelDirection.X + +TravelDirection.Y*TravelDirection.Y>1.0e-12f) + { + const double TileScale=static_cast(1<(A.Coord.X)+0.5)*TileScale + -NewDesired.CenterChunk.X)*TravelDirection.X + +((static_cast(A.Coord.Y)+0.5)*TileScale + -NewDesired.CenterChunk.Y)*TravelDirection.Y; + const double AlongB= + ((static_cast(B.Coord.X)+0.5)*TileScale + -NewDesired.CenterChunk.X)*TravelDirection.X + +((static_cast(B.Coord.Y)+0.5)*TileScale + -NewDesired.CenterChunk.Y)*TravelDirection.Y; + const auto LeadRank=[TileScale](double Projection) + { + if(Projection>=TileScale&&Projection<=TileScale*2.0)return 0; + if(Projection>0.0&&ProjectionTileScale*2.0)return 2; + return 3; + }; + const int LeadRankA=LeadRank(AlongA),LeadRankB=LeadRank(AlongB); + if(LeadRankA!=LeadRankB)return LeadRankAAlongB; + else if(LeadRankA==2&&AlongA!=AlongB)return AlongA