diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2aa1c46..21e51be 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -584,6 +584,13 @@ driven by `EditorBrush*` props. admission count. `voxel.StreamingDynamicNearReserve=1` holds the authored near reserve while moving or before the walkable gate, then releases it to coarse work after the stationary ring is ready. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.) + Core region compilation is a separate resource gate. With the default + `voxel.CoreRegionConcurrentAdmission=1`, tile keys that need the pending region are withheld, while + independent keys may use remaining worker slots. `0` restores the all-admission wait. Region pages + are built off the lookup map, published as immutable shared pages, and reclaimed when the final + reader releases its shared owner. `voxel.StreamingResultBacklogCap=256` independently bounds queued + result payloads plus active tile tasks and held prequeue results; `0` disables this memory backstop. + Result count is not used as a worker-slot count. - **LOD** changes HOT-SWAP (`LoadTile` only, never unload-first) → no holes. LOD reconciliation lives in the PERSISTENT per-frame submit loop (same loop as new-chunk loads), NOT as a one-shot on the boundary-cross frame — a one-shot drops every chunk past the task @@ -1565,6 +1572,43 @@ the recipe version remains unchanged. Null-RHI runs do not prove player-visible [`Standalone/Research/WorldAssemblerM3/Round11/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round11/REPORT.md) for the audit counts, RMC review, quality matrix, commands and binary hashes. +### 8.29 Concurrent region admission and bounded result backlog — World Assembler S8 / M3 Round 12 + +The S7 moving trace found `bCoreRegionCompilePending` returning before all tile submissions. Each +such update could leave most slots unused while a new player center requested a region page. The +default `voxel.CoreRegionConcurrentAdmission=1` lets `LoadCoreTile` continue for keys outside the +pending and in-flight region sets. Keys that need those sets wait for publication; setting the CVar +to `0` restores the old all-admission gate for an A/B control. This changes scheduling only. + +`World::compileTileRegion` builds region pages without mutating pages already in use, then publishes +them under the region-map lock. `generateTile` copies a `shared_ptr` while holding the map's shared +lock and keeps it for the whole mesh read. Evicting or replacing a map entry therefore cannot free a +page under an existing worker. The actor's shared `WorldLock` prevents a world edit/reset while a +tile reader or region compiler is active; `EndPlay` waits for the shared active-task count before +destroying the actor's UObject state. + +The result backlog has its own backpressure control. `voxel.StreamingResultBacklogCap=256` counts +queued MPSC results, active tile-generation tasks and held visible-LOD0 prequeue results. Reaching +the cap stops new tile admissions until apply drains results or active tasks retire. It does not make +finished results consume worker slots in the normal admission count. `0` is the uncapped negative +control; the Core-off legacy test also exercises the default cap. + +On the 25 cm owner SurfaceWorld plan, the matched 6 m/s CVar-off/on runs both completed 11 region +requests (one initial desired set plus ten center moves). The old gate recorded 11 global scheduler +skips; concurrent admission recorded none and submitted 110 unrelated tiles with no dependent-key +declines. All eleven changing support centers had the nine-tile walkable ring ready immediately. +Accumulated pending windows were about 0.43 s, but measured compiler wall time totaled about 11 ms. +At 60 m/s with a 400 m teleport, the trace recorded 113 region requests, 782 independent submissions +and 983 dependent-key declines. Most support centers advanced before their nine-tile ring could +become ready, so the high-speed gate still fails its travel-time target. + +The shared result trace now records `queued_peak`, peak payload bytes, the configurable cap, and cap +stops. A slow-apply teleport stress run peaked at 256 results / 76.2 MB with the default cap; the +uncapped control reached 1,577 / 125.1 MB. Payload bytes exclude queue-node and allocator overhead. +The cap kept the observed backlog finite without reinstating the S6/S7 finished-result worker-slot +decline. Full timings, per-center records, quality checks and hashes are in +[`Standalone/Research/WorldAssemblerM3/Round12/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round12/REPORT.md). + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index abc25f2..163418a 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -331,6 +331,11 @@ game-thread `PendingTileCancellation` map whose per-request atomic flags are rea S5 assigns request and worker-start ordinals to each tile, preserving epoch, resident-at-request, priority, queue-wait and build timing in the opt-in startup trace. All four result queues remain MPSC; EndPlay still waits for workers and drains every queue. +S8 adds `StartPendingCoreRegionCompile`: by default `voxel.CoreRegionConcurrentAdmission=1` lets +`LoadCoreTile` admit keys independent of a pending region page; `0` restores the global compile gate. +`voxel.StreamingResultBacklogCap=256` limits queued result payloads + active tile tasks + held +prequeue results without folding completed results back into worker-slot admission; `0` disables it. +Core region pages are immutable shared owners while readers are active. See §8.29 and §3.15. **⭐ Espace ACTEUR / ACTOR SPACE (2026-08-17).** Le champ de densite est defini en espace **acteur** : `(0,0)` est l origine de l acteur `AVoxelWorld`, pas celle du monde Unreal. `WorldToLocalCm` / @@ -352,6 +357,8 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error** | `ValidateDeterminism()` | — | **F2 CallInEditor button (PIE)**: re-samples boundary points under left- vs right-chunk cache warm-ups + a same-alignment repeat; any non-zero delta = window-invariance regression (§8.4). Run after every "bit-identical" hot-path refactor. | | `GetMaxConcurrentTasks()` | — | T2.d — asset `MaxConcurrentTasks` capped to logical cores − 2 (all three budget checks use it). | | `GetActiveGenerationTaskCount()` | — | Normal mode returns the existing pending count. With `voxel.Step1PrequeueVisibleLOD0>0`, completed held results are excluded from active worker admission so a full render corpus can be generated before the upload release. | +| `StartPendingCoreRegionCompile()` / `LoadCoreTile()` | — | Background Core region-page compile. `voxel.CoreRegionConcurrentAdmission=0` is the global-stop control; default `1` withholds only keys in the pending/in-flight region sets. Independent tiles can proceed. Core pages use immutable `shared_ptr` ownership under a separate shared mutex. §8.29. | +| `EnqueueStreamingResult()` / `IsStreamingResultBacklogCapped()` | — | Central result accounting for all four MPSC queues. `voxel.StreamingResultBacklogCap=256` caps queued results + active tile tasks + held prequeue results; `0` disables the cap. This bounds result-payload growth separately from worker-slot admission. §8.29. | | `PostEditChangeProperty` | 1000 | Editor live-edit hook. | | `OnObjectModifiedInEditor` | 1022 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). | | `EndPlay` | 1118 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. | @@ -1016,6 +1023,24 @@ cumulative task intervals for worker-seconds. `VoxelForgeExploreCommandlet.cpp` owner-plan trace and moving actor check. See ARCHITECTURE §8.27–8.28 and [`Standalone/Research/WorldAssemblerM3/Round11/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round11/REPORT.md). +### 3.15 Moving region admission and result-backlog safety trace — World Assembler S8 / M3 Round 12 + +`core_region_compile_request/start/complete` record why the desired region is being built (initial +set, player-center move, or same-center desired-set change), center delta, pending tiles, compile wall +time, accumulated pending interval, independent submissions and dependent-key declines. While a +compile is pending, `scheduler_skip.reason=core_region_compile_pending` records per-LOD missing +counts, motion and speed. The default concurrent mode should have no global region-pending skip; +its negative control is `voxel.CoreRegionConcurrentAdmission=0`. + +`walkable_gate` logs every support-center change and whether all nine required LOD0 collision tiles +are ready there. `result_queue_frame` samples queued results, payload bytes, active tile tasks and +the cap; `result_queue_summary` stores peak queue length/bytes and admission stops. These counters +include slow-apply, teleport and high-speed controls added to `VoxelForgeExploreCommandlet.cpp` and +`Tools/TestVoxelForgeCoreWorldHost.ps1`. Keep the cap negative control (`voxel.StreamingResultBacklogCap=0`) +separate from the region-gate control. Round12 found 11/11 ready support centers at 6 m/s and 8/113 +immediately ready at 60 m/s with a 400 m teleport; see the traces under `Saved/BuildHost` and +[`Standalone/Research/WorldAssemblerM3/Round12/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round12/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 9994945..1a62acb 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -143,6 +143,7 @@ public: FRWLock WorldLock; FVoxelForgeCoreTaskScheduler Scheduler; std::unordered_set RetainedTiles; + FCriticalSection RetainedTilesLock; std::atomic bReady{false}; std::atomic bInitialFailed{false}; std::atomic bRegionCompileInFlight{false}; @@ -152,6 +153,11 @@ public: std::atomic bRebuildDesired{false}; std::atomic bShuttingDown{false}; std::atomic NextEditSequence{1}; + std::atomic RegionCompileStartCycles{0}; + std::atomic RegionCompileEndCycles{0}; + std::atomic RegionCompileTileCount{0}; + std::atomic RegionCompileSerial{0}; + std::atomic RegionCompileWorkerSeconds{0.0}; int32 FullResolutionLodCount = 0; FString InitialFailureReason; }; @@ -197,6 +203,18 @@ namespace GVoxelForgeStreamingLegacyPendingResultCap, TEXT("Compatibility control: 1 counts completed results awaiting apply against the generation task cap; default 0 caps only queued or running generation tasks.")); + int32 GVoxelForgeCoreRegionConcurrentAdmission = 1; + FAutoConsoleVariableRef CVarVoxelForgeCoreRegionConcurrentAdmission( + TEXT("voxel.CoreRegionConcurrentAdmission"), + GVoxelForgeCoreRegionConcurrentAdmission, + TEXT("Allow tiles independent of a pending Core region compile to proceed; 0 restores the global streaming stop.")); + + int32 GVoxelForgeStreamingResultBacklogCap = 256; + FAutoConsoleVariableRef CVarVoxelForgeStreamingResultBacklogCap( + TEXT("voxel.StreamingResultBacklogCap"), + GVoxelForgeStreamingResultBacklogCap, + TEXT("Maximum queued results plus admitted tile tasks; 0 disables the safety cap. This is separate from the worker-slot cap.")); + int32 GVoxelForgeProfileTileGeneration = 0; FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration( TEXT("voxel.ProfileTileGeneration"), @@ -996,10 +1014,10 @@ void AVoxelWorld::RegenerateAllChunks() if (ActiveTaskCount.load(std::memory_order_relaxed) == 0) { FChunkResult StaleResult; - while (CriticalProcessQueue.Dequeue(StaleResult)) {} - while (EditedProcessQueue.Dequeue(StaleResult)) {} - while (NearProcessQueue.Dequeue(StaleResult)) {} - while (ProcessQueue.Dequeue(StaleResult)) {} + while (CriticalProcessQueue.Dequeue(StaleResult)) { RecordStreamingResultDequeued(StaleResult); } + while (EditedProcessQueue.Dequeue(StaleResult)) { RecordStreamingResultDequeued(StaleResult); } + while (NearProcessQueue.Dequeue(StaleResult)) { RecordStreamingResultDequeued(StaleResult); } + while (ProcessQueue.Dequeue(StaleResult)) { RecordStreamingResultDequeued(StaleResult); } PendingTiles.Empty(); PendingTileCancellation.Empty(); } @@ -1597,6 +1615,27 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) } FPlatformProcess::Yield(); // Give CPU to other threads } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeResultBacklog] queued_peak=%d queued_peak_bytes=%llu queued_final=%d " + "queued_final_bytes=%llu cap=%d cap_stops=%llu"), + PeakQueuedStreamingResultCount.load(std::memory_order_relaxed), + static_cast(PeakQueuedStreamingResultBytes.load(std::memory_order_relaxed)), + QueuedStreamingResultCount.load(std::memory_order_relaxed), + static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), + GVoxelForgeStreamingResultBacklogCap, + static_cast(StreamingResultBacklogCapStops.load(std::memory_order_relaxed))); + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("result_queue_summary"), FString::Printf( + TEXT("\"queued_peak\":%d,\"queued_peak_bytes\":%llu,\"queued_final\":%d," + "\"queued_final_bytes\":%llu,\"backlog_cap\":%d,\"cap_stops\":%llu"), + PeakQueuedStreamingResultCount.load(std::memory_order_relaxed), + static_cast(PeakQueuedStreamingResultBytes.load(std::memory_order_relaxed)), + QueuedStreamingResultCount.load(std::memory_order_relaxed), + static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), + GVoxelForgeStreamingResultBacklogCap, + static_cast(StreamingResultBacklogCapStops.load(std::memory_order_relaxed)))); + } // Drain any queued results FChunkResult Discard; while (CriticalProcessQueue.Dequeue(Discard)) @@ -1951,6 +1990,28 @@ void AVoxelWorld::BeginPlay() TotalWorkerGenerationCycles.store(0, std::memory_order_relaxed); TotalWorkerGenerationTasks.store(0, std::memory_order_relaxed); TotalGenerationWorkerTaskCycles.store(0, std::memory_order_relaxed); + QueuedStreamingResultCount.store(0, std::memory_order_relaxed); + QueuedStreamingResultBytes.store(0, std::memory_order_relaxed); + PeakQueuedStreamingResultCount.store(0, std::memory_order_relaxed); + PeakQueuedStreamingResultBytes.store(0, std::memory_order_relaxed); + ActiveTileTaskCount.store(0, std::memory_order_relaxed); + StreamingResultBacklogCapStops.store(0, std::memory_order_relaxed); + bCoreRegionCompilePending = false; + PendingCoreRegionTiles.Reset(); + PendingCoreRegionTileSet.Reset(); + PendingCoreRetainedTiles.Reset(); + CoreRegionCompileTilesInFlight.Reset(); + LastCoreRegionRequestCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); + PendingCoreRegionRequestCenter = FIntVector::ZeroValue; + ActiveCoreRegionCompileCenter = FIntVector::ZeroValue; + bHasLastCoreRegionRequestCenter = false; + CoreRegionPendingSinceSeconds = 0.0; + ActiveCoreRegionCompileStartSeconds = 0.0; + ActiveCoreRegionCompileStartCycles = 0; + CoreRegionIndependentSubmissions = 0; + CoreRegionDependentDeclines = 0; + CoreRegionCompileRequestSerial = 0; + ActiveCoreRegionCompileRequestSerial = 0; LastStreamingSchedulerSampleSeconds = 0.0; LastStreamingSchedulerWorkerCycles = 0; TotalObsoleteWorkerCycles.store(0, std::memory_order_relaxed); @@ -1994,6 +2055,8 @@ void AVoxelWorld::BeginPlay() int32 CommandLineSealedSolidProof = GVoxelForgeSealedSolidProof; int32 CommandLineSampleStacks = GVoxelForgeSampleStacks; int32 CommandLineCollisionGate = GVoxelForgeCollisionGate; + int32 CommandLineCoreRegionConcurrentAdmission = GVoxelForgeCoreRegionConcurrentAdmission; + int32 CommandLineStreamingResultBacklogCap = GVoxelForgeStreamingResultBacklogCap; int32 CommandLineStep1GenerationCap = GVoxelForgeStep1GenerationCap; int32 CommandLineStep1ExitAfterFrames = GVoxelForgeStep1ExitAfterFrames; int32 CommandLineStep1CsvCaptureOnBeginPlay = GVoxelForgeStep1CsvCaptureOnBeginPlay; @@ -2071,6 +2134,17 @@ void AVoxelWorld::BeginPlay() GVoxelForgeStreamingDynamicNearReserve); FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingLegacyPendingResultCap="), GVoxelForgeStreamingLegacyPendingResultCap); + if (FParse::Value(FCommandLine::Get(), TEXT("voxel.CoreRegionConcurrentAdmission="), + CommandLineCoreRegionConcurrentAdmission)) + { + GVoxelForgeCoreRegionConcurrentAdmission = + CommandLineCoreRegionConcurrentAdmission != 0 ? 1 : 0; + } + if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingResultBacklogCap="), + CommandLineStreamingResultBacklogCap)) + { + GVoxelForgeStreamingResultBacklogCap = FMath::Max(0, CommandLineStreamingResultBacklogCap); + } if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="), CommandLineStep1PrequeueVisibleLOD0)) { @@ -2723,6 +2797,9 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest() HeadlessStreamingTestSpeedCmPerSecond = 0.0f; HeadlessStreamingTestStartSeconds = 0.0f; HeadlessStreamingTestExitSeconds = 0.0f; + HeadlessStreamingTestTeleportAtSeconds = 0.0f; + HeadlessStreamingTestTeleportDistanceCm = 0.0f; + bHeadlessStreamingTestTeleportDone = false; HeadlessStreamingTestDirection = FVector::XAxisVector; HeadlessStreamingTestLastActualPosition = FVector::ZeroVector; HeadlessStreamingTestBeginSeconds = 0.0; @@ -2933,6 +3010,14 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest() FParse::Value(CommandLine, TEXT("voxel.TestMoveSpeedCmPerSecond="), Speed); FParse::Value(CommandLine, TEXT("voxel.TestMoveStartSeconds="), StartSeconds); FParse::Value(CommandLine, TEXT("voxel.TestExitSeconds="), ExitSeconds); + FParse::Value(CommandLine, TEXT("voxel.TestTeleportAfterSeconds="), + HeadlessStreamingTestTeleportAtSeconds); + FParse::Value(CommandLine, TEXT("voxel.TestTeleportDistanceCm="), + HeadlessStreamingTestTeleportDistanceCm); + HeadlessStreamingTestTeleportAtSeconds = FMath::Max( + HeadlessStreamingTestTeleportAtSeconds, 0.0f); + HeadlessStreamingTestTeleportDistanceCm = FMath::Max( + HeadlessStreamingTestTeleportDistanceCm, 0.0f); if (FParse::Value(CommandLine, TEXT("voxel.TestExitOnSteady="), ExitOnSteadyValue)) { bHeadlessStreamingTestExitOnSteady = ExitOnSteadyValue != 0; @@ -4174,6 +4259,54 @@ void AVoxelWorld::AdvanceHeadlessStreamingTest( return; } + if (!bHeadlessStreamingTestTeleportDone + && HeadlessStreamingTestTeleportAtSeconds > 0.0f + && ElapsedSeconds - static_cast(HeadlessStreamingTestStartSeconds) + >= static_cast(HeadlessStreamingTestTeleportAtSeconds)) + { + const FVector BeforeTeleport = InOutPlayerPosition; + const FVector TeleportedPosition = BeforeTeleport + + HeadlessStreamingTestDirection * HeadlessStreamingTestTeleportDistanceCm; + const bool bTeleported = PlayerPawn->SetActorLocation( + TeleportedPosition, false, nullptr, ETeleportType::TeleportPhysics); + bHeadlessStreamingTestTeleportDone = true; + HeadlessStreamingTestLastElapsedSeconds = ElapsedSeconds; + ++HeadlessStreamingTestMoveAttempts; + if (bTeleported) + { + const FVector ActualPosition = PlayerPawn->GetActorLocation(); + HeadlessStreamingTestDistanceCm += FVector::Dist(BeforeTeleport, ActualPosition); + HeadlessStreamingTestLastActualPosition = ActualPosition; + InOutPlayerPosition = ActualPosition; + InOutPlayerHeading = HeadlessStreamingTestDirection + * HeadlessStreamingTestSpeedCmPerSecond; + if (ACharacter* Character = Cast(PlayerPawn)) + { + if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement()) + { + CharacterMovement->StopMovementImmediately(); + CharacterMovement->SetMovementMode(MOVE_None); + } + } + } + const FIntVector TeleportCenter = GetTileCoordAtLocalCm(WorldToLocalCm(InOutPlayerPosition)); + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeStreamingTest] teleport=%d distance_cm=%.3f at_s=%.3f center=(%d,%d,%d)"), + bTeleported ? 1 : 0, HeadlessStreamingTestTeleportDistanceCm, + ElapsedSeconds, TeleportCenter.X, TeleportCenter.Y, TeleportCenter.Z); + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_teleport"), FString::Printf( + TEXT("\"teleported\":%d,\"distance_cm\":%s,\"center\":[%d,%d,%d]," + "\"elapsed_s\":%s"), + bTeleported ? 1 : 0, + *FString::SanitizeFloat(HeadlessStreamingTestTeleportDistanceCm), + TeleportCenter.X, TeleportCenter.Y, TeleportCenter.Z, + *FString::SanitizeFloat(ElapsedSeconds))); + } + return; + } + // The headless game runs uncapped, so simulation DeltaTime is not a stable movement clock. Use // wall time for this explicit harness path; both A/B runs then cover exactly the same distance // regardless of how much work the scheduler is doing per frame. @@ -7468,6 +7601,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason) if (bCompiled) { State->World->retainTileRegions(InitialDesired); + FScopeLock RetainedLock(&State->RetainedTilesLock); for (const VoxelForgeCore::TileKey& Key : InitialDesired) { State->RetainedTiles.insert(Key); @@ -9226,6 +9360,15 @@ void AVoxelWorld::ProcessPendingChunks() } } VoxelForgeStartupTrace::RecordApplyFrame(Sample); + VoxelForgeStartupTrace::RecordEvent(TEXT("result_queue_frame"), FString::Printf( + TEXT("\"frame\":%llu,\"queued_results\":%d,\"queued_bytes\":%llu," + "\"active_tile_tasks\":%d,\"held_results\":%d,\"backlog_count\":%d," + "\"backlog_cap\":%d"), + static_cast(GFrameCounter), + QueuedStreamingResultCount.load(std::memory_order_relaxed), + static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), + ActiveTileTaskCount.load(std::memory_order_relaxed), Step1HeldApplyTiles.Num(), + GetStreamingResultBacklogCount(), GVoxelForgeStreamingResultBacklogCap)); }; FChunkResult DequeuedChunk; const auto DequeueNextResult = [&]() -> bool @@ -9244,10 +9387,15 @@ void AVoxelWorld::ProcessPendingChunks() } // Mirror the admission order. A horizon result that completed earlier must not consume // the game-thread apply budget ahead of collision support, an edit, or ordinary LOD0. - return CriticalProcessQueue.Dequeue(DequeuedChunk) + if (CriticalProcessQueue.Dequeue(DequeuedChunk) || EditedProcessQueue.Dequeue(DequeuedChunk) || NearProcessQueue.Dequeue(DequeuedChunk) - || ProcessQueue.Dequeue(DequeuedChunk); + || ProcessQueue.Dequeue(DequeuedChunk)) + { + RecordStreamingResultDequeued(DequeuedChunk); + return true; + } + return false; }; if (GVoxelForgeStep1PrequeueVisibleLOD0 > 0 && !bStep1UploadCorpusReleased) @@ -10311,12 +10459,14 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& BuildDesiredTransitionIndex(); bool bRetainedSetChanged = false; + int32 RetainedTileCount = 0; TSet DesiredSet; for (const FVoxelTileKey& Key : DesiredSorted) DesiredSet.Add(Key); - TArray MissingFineTiles; + TArray MissingRetainedFineTiles; { - FReadScopeLock Lock(State->WorldLock); - bRetainedSetChanged = DesiredSet.Num() != static_cast(State->RetainedTiles.size()); + FScopeLock Lock(&State->RetainedTilesLock); + RetainedTileCount = static_cast(State->RetainedTiles.size()); + bRetainedSetChanged = DesiredSet.Num() != RetainedTileCount; for (const FVoxelTileKey& Key : DesiredSorted) { const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; @@ -10324,22 +10474,79 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector& if (!bAlreadyRetained) bRetainedSetChanged = true; if (!bAlreadyRetained && Key.Level < State->FullResolutionLodCount) { - MissingFineTiles.Add(Key); + MissingRetainedFineTiles.Add(Key); + } + } + } + TArray MissingRegionTiles; + { + FReadScopeLock Lock(State->WorldLock); + if (State->World) + { + for (const FVoxelTileKey& Key : MissingRetainedFineTiles) + { + const VoxelForgeCore::TileKey CoreKey{ + {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; + if (State->World->needsTileRegion(CoreKey)) + { + MissingRegionTiles.Add(Key); + } } } } if (bRetainedSetChanged && !State->bRegionCompileFailed.load(std::memory_order_acquire)) { - PendingCoreRegionTiles = MoveTemp(MissingFineTiles); + const double Now = FPlatformTime::Seconds(); + const bool bHadPreviousCenter = bHasLastCoreRegionRequestCenter; + const bool bCenterMoved = bHadPreviousCenter && LastCoreRegionRequestCenter != Center; + const FIntVector PreviousCenter = LastCoreRegionRequestCenter; + if (!bCoreRegionCompilePending) + { + CoreRegionPendingSinceSeconds = Now; + } + PendingCoreRegionRequestCenter = Center; + LastCoreRegionRequestCenter = Center; + bHasLastCoreRegionRequestCenter = true; + PendingCoreRegionTiles = MoveTemp(MissingRegionTiles); + PendingCoreRegionTileSet.Reset(); + for (const FVoxelTileKey& Key : PendingCoreRegionTiles) + { + PendingCoreRegionTileSet.Add(Key); + } PendingCoreRetainedTiles = DesiredSorted; bCoreRegionCompilePending = true; + ++CoreRegionCompileRequestSerial; + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("core_region_compile_request"), FString::Printf( + TEXT("\"request_serial\":%d,\"reason\":\"%s\",\"center\":[%d,%d,%d]," + "\"previous_center\":[%d,%d,%d],\"center_delta\":[%d,%d,%d]," + "\"desired_tiles\":%d,\"retained_tiles\":%d,\"missing_region_tiles\":%d," + "\"desired_epoch\":%u,\"active_tile_tasks\":%d"), + CoreRegionCompileRequestSerial, + !bHadPreviousCenter ? TEXT("initial_desired_region_set") + : (bCenterMoved ? TEXT("player_center_moved") + : TEXT("desired_set_changed_same_center")), + Center.X, Center.Y, Center.Z, + PreviousCenter.X, PreviousCenter.Y, PreviousCenter.Z, + bCenterMoved ? Center.X - PreviousCenter.X : 0, + bCenterMoved ? Center.Y - PreviousCenter.Y : 0, + bCenterMoved ? Center.Z - PreviousCenter.Z : 0, + DesiredSorted.Num(), RetainedTileCount, PendingCoreRegionTiles.Num(), DesiredEpoch, + ActiveTileTaskCount.load(std::memory_order_relaxed))); + } StartPendingCoreRegionCompile(); } } void AVoxelWorld::StartPendingCoreRegionCompile() { - if (!bCoreRegionCompilePending || ActiveTaskCount.load(std::memory_order_acquire) != 0) return; + if (!bCoreRegionCompilePending + || (GVoxelForgeCoreRegionConcurrentAdmission == 0 + && ActiveTaskCount.load(std::memory_order_acquire) != 0)) + { + return; + } const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid() || !State->bReady.load(std::memory_order_acquire) || State->bRegionCompileFailed.load(std::memory_order_acquire)) @@ -10356,12 +10563,43 @@ void AVoxelWorld::StartPendingCoreRegionCompile() State->bRegionCompileSucceeded.store(false, std::memory_order_release); const TArray MissingTiles = PendingCoreRegionTiles; const TArray RetainedTiles = PendingCoreRetainedTiles; + const FIntVector CompileCenter = PendingCoreRegionRequestCenter; + const bool bAllowConcurrentGeneration = GVoxelForgeCoreRegionConcurrentAdmission != 0; const uint32 TaskEpoch = GenerationEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch; const uint32 WorkerLimit = static_cast(FMath::Clamp(GetMaxConcurrentTasks(), 1, 16)); + ActiveCoreRegionCompileCenter = CompileCenter; + ActiveCoreRegionCompileRequestSerial = CoreRegionCompileRequestSerial; + ActiveCoreRegionCompileStartSeconds = FPlatformTime::Seconds(); + ActiveCoreRegionCompileStartCycles = FPlatformTime::Cycles64(); + CoreRegionCompileTilesInFlight.Reset(); + for (const FVoxelTileKey& Key : MissingTiles) + { + CoreRegionCompileTilesInFlight.Add(Key); + } + const int32 CompileSerial = State->RegionCompileSerial.fetch_add( + 1, std::memory_order_relaxed) + 1; + State->RegionCompileStartCycles.store(ActiveCoreRegionCompileStartCycles, + std::memory_order_release); + State->RegionCompileEndCycles.store(0, std::memory_order_release); + State->RegionCompileTileCount.store(0, std::memory_order_release); + State->RegionCompileWorkerSeconds.store(0.0, std::memory_order_release); + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("core_region_compile_start"), FString::Printf( + TEXT("\"compile_serial\":%d,\"request_serial\":%d,\"center\":[%d,%d,%d]," + "\"missing_region_tiles\":%d,\"retained_tiles\":%d,\"concurrent_admission\":%d," + "\"active_tile_tasks\":%d,\"active_task_guard\":%d"), + CompileSerial, CoreRegionCompileRequestSerial, + CompileCenter.X, CompileCenter.Y, CompileCenter.Z, + MissingTiles.Num(), RetainedTiles.Num(), bAllowConcurrentGeneration ? 1 : 0, + ActiveTileTaskCount.load(std::memory_order_relaxed), + ActiveTaskCount.load(std::memory_order_relaxed))); + } ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); UE::Tasks::Launch(TEXT("VoxelForgeCoreTileRegion"), - [this, State, MissingTiles, RetainedTiles, TaskEpoch, TaskDesiredEpoch, WorkerLimit]() + [this, State, MissingTiles, RetainedTiles, TaskEpoch, TaskDesiredEpoch, WorkerLimit, + bAllowConcurrentGeneration, CompileSerial]() { struct FTaskGuard { @@ -10372,6 +10610,7 @@ void AVoxelWorld::StartPendingCoreRegionCompile() if (bShuttingDown.load(std::memory_order_acquire) || State->bShuttingDown.load(std::memory_order_acquire)) { + State->RegionCompileEndCycles.store(FPlatformTime::Cycles64(), std::memory_order_release); State->bRegionCompileInFlight.store(false, std::memory_order_release); State->bRegionCompileFinished.store(true, std::memory_order_release); return; @@ -10391,20 +10630,34 @@ void AVoxelWorld::StartPendingCoreRegionCompile() std::string Error; bool bCompiled = false; + std::size_t CompiledTileCount = 0; + VoxelForgeCore::TileRegionCompileStats CompileStats; + const auto CompileAndRetain = [&]() { - FWriteScopeLock Lock(State->WorldLock); bCompiled = State->World->compileTileRegion( - Missing, WorkerLimit, State->Scheduler, Error); + Missing, WorkerLimit, State->Scheduler, Error, + &CompiledTileCount, &CompileStats); if (bCompiled) { State->World->retainTileRegions(Retained); - State->RetainedTiles.clear(); - for (const VoxelForgeCore::TileKey& Key : Retained) - { - State->RetainedTiles.insert(Key); - } } + }; + if (bAllowConcurrentGeneration) + { + // Tile generation also takes a shared WorldLock. Core's region map publishes immutable + // shared_ptr pages under its own lock, so readers can finish with the prior page safely. + // Le maillage prend aussi ce verrou en lecture. Core publie des pages immuables partagees; + // les lecteurs deja partis gardent leur page precedente jusqu'a la fin. + FReadScopeLock Lock(State->WorldLock); + CompileAndRetain(); } + else + { + FWriteScopeLock Lock(State->WorldLock); + CompileAndRetain(); + } + State->RegionCompileWorkerSeconds.store(CompileStats.workerSeconds, + std::memory_order_release); State->bRegionCompileSucceeded.store(bCompiled, std::memory_order_release); if (!bCompiled) { @@ -10415,11 +10668,24 @@ void AVoxelWorld::StartPendingCoreRegionCompile() } else { + { + FScopeLock RetainedLock(&State->RetainedTilesLock); + State->RetainedTiles.clear(); + for (const VoxelForgeCore::TileKey& Key : Retained) + { + State->RetainedTiles.insert(Key); + } + } + State->RegionCompileTileCount.store( + static_cast(CompiledTileCount), std::memory_order_release); State->bRebuildDesired.store(true, std::memory_order_release); UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeCoreRuntime] tile_regions_compiled=%d retained=%d epoch=%u desired_epoch=%u"), - MissingTiles.Num(), RetainedTiles.Num(), TaskEpoch, TaskDesiredEpoch); + TEXT("[VoxelForgeCoreRuntime] tile_regions_compiled=%d retained=%d epoch=%u desired_epoch=%u " + "wall_ms=%.3f worker_s=%.3f serial=%d"), + static_cast(CompiledTileCount), RetainedTiles.Num(), TaskEpoch, TaskDesiredEpoch, + CompileStats.wallSeconds * 1000.0, CompileStats.workerSeconds, CompileSerial); } + State->RegionCompileEndCycles.store(FPlatformTime::Cycles64(), std::memory_order_release); State->bRegionCompileInFlight.store(false, std::memory_order_release); State->bRegionCompileFinished.store(true, std::memory_order_release); }, UE::Tasks::ETaskPriority::BackgroundNormal); @@ -10979,6 +11245,80 @@ int32 AVoxelWorld::GetGenerationAdmissionCount() const return FMath::Max(0, PendingTiles.Num() - HeldCompletedResults); } +uint64 AVoxelWorld::GetStreamingResultPayloadBytes(const FChunkResult& Result) const +{ + uint64 Bytes = Result.GeometryBytes; + if (Bytes == 0 && Result.Streams.IsValid()) + { + Result.Streams->ForEach([&Bytes](const RealtimeMesh::FRealtimeMeshStream& Stream) + { + Bytes += static_cast(Stream.GetAllocatedSize()); + }); + } + Bytes += static_cast(Result.CaptureGrid.GetAllocatedSize()); + Bytes += static_cast(Result.PreEditDensityGrid.Samples.GetAllocatedSize()); + Bytes += static_cast(Result.SurfaceWitnesses.GetAllocatedSize()); + return Bytes; +} + +int32 AVoxelWorld::GetStreamingResultBacklogCount() const +{ + const int32 Queued = FMath::Max(0, + QueuedStreamingResultCount.load(std::memory_order_acquire)); + const int32 ActiveTiles = FMath::Max(0, + ActiveTileTaskCount.load(std::memory_order_acquire)); + const int32 HeldResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 + ? Step1HeldApplyTiles.Num() : 0; + return Queued + ActiveTiles + HeldResults; +} + +bool AVoxelWorld::IsStreamingResultBacklogCapped() const +{ + const int32 Limit = FMath::Max(0, GVoxelForgeStreamingResultBacklogCap); + return Limit > 0 && GetStreamingResultBacklogCount() >= Limit; +} + +void AVoxelWorld::EnqueueStreamingResult(FChunkResult&& Result) +{ + const uint64 Bytes = GetStreamingResultPayloadBytes(Result); + const int32 Count = QueuedStreamingResultCount.fetch_add(1, std::memory_order_acq_rel) + 1; + const uint64 TotalBytes = QueuedStreamingResultBytes.fetch_add(Bytes, std::memory_order_acq_rel) + Bytes; + int32 PeakCount = PeakQueuedStreamingResultCount.load(std::memory_order_relaxed); + while (Count > PeakCount && !PeakQueuedStreamingResultCount.compare_exchange_weak( + PeakCount, Count, std::memory_order_relaxed)) + { + } + uint64 PeakBytes = PeakQueuedStreamingResultBytes.load(std::memory_order_relaxed); + while (TotalBytes > PeakBytes && !PeakQueuedStreamingResultBytes.compare_exchange_weak( + PeakBytes, TotalBytes, std::memory_order_relaxed)) + { + } + + if (Result.WorkPriority == EVoxelTileWorkPriority::CollisionCritical) + { + CriticalProcessQueue.Enqueue(MoveTemp(Result)); + } + else if (Result.WorkPriority == EVoxelTileWorkPriority::Edited) + { + EditedProcessQueue.Enqueue(MoveTemp(Result)); + } + else if (Result.WorkPriority == EVoxelTileWorkPriority::LOD0) + { + NearProcessQueue.Enqueue(MoveTemp(Result)); + } + else + { + ProcessQueue.Enqueue(MoveTemp(Result)); + } +} + +void AVoxelWorld::RecordStreamingResultDequeued(const FChunkResult& Result) +{ + QueuedStreamingResultCount.fetch_sub(1, std::memory_order_acq_rel); + QueuedStreamingResultBytes.fetch_sub( + GetStreamingResultPayloadBytes(Result), std::memory_order_acq_rel); +} + int32 AVoxelWorld::GetNearTaskReserve(int32 MaxTasks) const { const int32 AuthoredReserve = Settings ? Settings->NearTaskReserve : 4; @@ -10997,19 +11337,33 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw const double StreamingUpdateStartSeconds = bTraceStreamingScheduler ? FPlatformTime::Seconds() : 0.0; const int32 PendingTilesAtUpdateEntry = PendingTiles.Num(); + const uint64 ResultBacklogCapStopsAtUpdateEntry = + StreamingResultBacklogCapStops.load(std::memory_order_relaxed); double DesiredRebuildMilliseconds = 0.0; const auto RecordSchedulerSkip = [&](const TCHAR* Reason) { if (!bTraceStreamingScheduler) return; const double Now = FPlatformTime::Seconds(); int32 DesiredMissing = 0; + int32 DesiredMissingByLevel[9] = {}; for (const FVoxelTileKey& Tile : DesiredSorted) { if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile)) { ++DesiredMissing; + if (Tile.Level >= 0 && Tile.Level < UE_ARRAY_COUNT(DesiredMissingByLevel)) + { + ++DesiredMissingByLevel[Tile.Level]; + } } } + FString DesiredMissingByLevelJson = TEXT("["); + for (int32 Level = 0; Level < UE_ARRAY_COUNT(DesiredMissingByLevel); ++Level) + { + if (Level > 0) DesiredMissingByLevelJson += TEXT(","); + DesiredMissingByLevelJson += FString::FromInt(DesiredMissingByLevel[Level]); + } + DesiredMissingByLevelJson += TEXT("]"); const int32 ActiveTasks = ActiveTaskCount.load(std::memory_order_relaxed); const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 ? Step1HeldApplyTiles.Num() : 0; @@ -11024,21 +11378,32 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw ? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds; const double IntervalSeconds = FMath::Max(0.0, Now - IntervalStart); const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta); + const double SpeedCmPerSecond = PlayerHeading.Size2D(); VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_skip"), FString::Printf( TEXT("\"frame\":%llu,\"reason\":\"%s\",\"desired_count\":%d," - "\"desired_missing\":%d,\"pending_tiles\":%d,\"admission_occupancy\":%d," + "\"desired_missing\":%d,\"desired_missing_by_lod\":%s," + "\"moving\":%d,\"speed_cm_s\":%s," + "\"pending_tiles\":%d,\"admission_occupancy\":%d," "\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d," + "\"queued_results\":%d,\"queued_result_bytes\":%llu," + "\"result_backlog\":%d,\"result_backlog_cap\":%d," "\"task_cap\":%d,\"legacy_pending_result_cap\":%d," "\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%s"), static_cast(GFrameCounter), Reason, DesiredSorted.Num(), - DesiredMissing, PendingTiles.Num(), AdmissionOccupancy, ActiveTasks, + DesiredMissing, *DesiredMissingByLevelJson, + SpeedCmPerSecond >= 50.0 ? 1 : 0, *FString::SanitizeFloat(SpeedCmPerSecond), + PendingTiles.Num(), AdmissionOccupancy, ActiveTasks, ActiveGenerationWorkerCount.load(std::memory_order_relaxed), - PendingResultsEstimate, MaxTasks, GVoxelForgeStreamingLegacyPendingResultCap, + PendingResultsEstimate, QueuedStreamingResultCount.load(std::memory_order_relaxed), + static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), + GetStreamingResultBacklogCount(), GVoxelForgeStreamingResultBacklogCap, + MaxTasks, GVoxelForgeStreamingLegacyPendingResultCap, *FString::SanitizeFloat(BusySeconds), *FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0))); LastStreamingSchedulerSampleSeconds = Now; LastStreamingSchedulerWorkerCycles = TotalWorkerCycles; }; + bool bBlockAllCoreAdmissionWhileCompiling = false; if (bUseVoxelForgeCore) { const TSharedPtr State = VoxelForgeCoreState; @@ -11056,20 +11421,127 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw { if (State->bRegionCompileFinished.exchange(false, std::memory_order_acq_rel)) { - bCoreRegionCompilePending = false; - PendingCoreRegionTiles.Reset(); - PendingCoreRetainedTiles.Reset(); - if (State->bRegionCompileSucceeded.load(std::memory_order_acquire)) + const bool bRegionCompileSucceeded = + State->bRegionCompileSucceeded.load(std::memory_order_acquire); + const uint64 CompileStartCycles = + State->RegionCompileStartCycles.load(std::memory_order_acquire); + const uint64 CompileEndCycles = + State->RegionCompileEndCycles.load(std::memory_order_acquire); + const double CompileElapsedMilliseconds = CompileEndCycles >= CompileStartCycles + ? FPlatformTime::ToSeconds64(CompileEndCycles - CompileStartCycles) * 1000.0 + : 0.0; + const double GateElapsedMilliseconds = CoreRegionPendingSinceSeconds > 0.0 + ? FMath::Max(0.0, FPlatformTime::Seconds() - CoreRegionPendingSinceSeconds) * 1000.0 + : 0.0; + const int32 CompiledTileCount = + State->RegionCompileTileCount.load(std::memory_order_acquire); + const double CompileWorkerSeconds = + State->RegionCompileWorkerSeconds.load(std::memory_order_acquire); + const int32 CompileSerial = + State->RegionCompileSerial.load(std::memory_order_acquire); + const uint64 IndependentSubmissions = CoreRegionIndependentSubmissions; + const uint64 DependentDeclines = CoreRegionDependentDeclines; + if (VoxelForgeStartupTrace::IsActive()) + { + VoxelForgeStartupTrace::RecordEvent(TEXT("core_region_compile_complete"), FString::Printf( + TEXT("\"compile_serial\":%d,\"request_serial\":%d,\"center\":[%d,%d,%d]," + "\"success\":%d,\"compiled_tiles\":%d,\"compile_elapsed_ms\":%s," + "\"compile_worker_s\":%s,\"pending_gate_elapsed_ms\":%s," + "\"independent_submissions\":%llu," + "\"dependent_declines\":%llu,\"concurrent_admission\":%d"), + CompileSerial, ActiveCoreRegionCompileRequestSerial, + ActiveCoreRegionCompileCenter.X, ActiveCoreRegionCompileCenter.Y, + ActiveCoreRegionCompileCenter.Z, bRegionCompileSucceeded ? 1 : 0, + CompiledTileCount, *FString::SanitizeFloat(CompileElapsedMilliseconds), + *FString::SanitizeFloat(CompileWorkerSeconds), + *FString::SanitizeFloat(GateElapsedMilliseconds), + static_cast(IndependentSubmissions), + static_cast(DependentDeclines), + GVoxelForgeCoreRegionConcurrentAdmission != 0 ? 1 : 0)); + } + CoreRegionIndependentSubmissions = 0; + CoreRegionDependentDeclines = 0; + CoreRegionCompileTilesInFlight.Reset(); + if (bRegionCompileSucceeded) { bForceDesiredRebuild = true; + bool bRetainedSetMatchesDesired = false; + TArray MissingRetainedFineTiles; + { + FScopeLock RetainedLock(&State->RetainedTilesLock); + bRetainedSetMatchesDesired = + DesiredSorted.Num() == static_cast(State->RetainedTiles.size()); + for (const FVoxelTileKey& Key : DesiredSorted) + { + const VoxelForgeCore::TileKey CoreKey{ + {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; + if (!State->RetainedTiles.contains(CoreKey)) + { + bRetainedSetMatchesDesired = false; + if (Key.Level < State->FullResolutionLodCount) + { + MissingRetainedFineTiles.Add(Key); + } + } + } + } + TArray MissingRegionTiles; + { + FReadScopeLock WorldLock(State->WorldLock); + if (State->World) + { + for (const FVoxelTileKey& Key : MissingRetainedFineTiles) + { + const VoxelForgeCore::TileKey CoreKey{ + {Key.Coord.X, Key.Coord.Y, Key.Coord.Z}, Key.Level}; + if (State->World->needsTileRegion(CoreKey)) + { + MissingRegionTiles.Add(Key); + } + } + } + } + if (!bRetainedSetMatchesDesired) + { + if (CoreRegionPendingSinceSeconds <= 0.0) + { + CoreRegionPendingSinceSeconds = FPlatformTime::Seconds(); + } + PendingCoreRegionTiles = MoveTemp(MissingRegionTiles); + PendingCoreRegionTileSet.Reset(); + for (const FVoxelTileKey& Key : PendingCoreRegionTiles) + { + PendingCoreRegionTileSet.Add(Key); + } + PendingCoreRetainedTiles = DesiredSorted; + PendingCoreRegionRequestCenter = DesiredEpochCenterChunk; + bCoreRegionCompilePending = true; + StartPendingCoreRegionCompile(); + } + else + { + bCoreRegionCompilePending = false; + PendingCoreRegionTiles.Reset(); + PendingCoreRegionTileSet.Reset(); + PendingCoreRetainedTiles.Reset(); + CoreRegionPendingSinceSeconds = 0.0; + } + } + else + { + bCoreRegionCompilePending = false; + PendingCoreRegionTiles.Reset(); + PendingCoreRegionTileSet.Reset(); + PendingCoreRetainedTiles.Reset(); + CoreRegionPendingSinceSeconds = 0.0; } } else { StartPendingCoreRegionCompile(); - RecordSchedulerSkip(TEXT("core_region_compile_pending")); - return; } + bBlockAllCoreAdmissionWhileCompiling = bCoreRegionCompilePending + && GVoxelForgeCoreRegionConcurrentAdmission == 0; } } const int32 MaxTasks = GetMaxConcurrentTasks(); @@ -11104,18 +11576,30 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw || bLastWalkableGateReady != bWalkableRingReadyForReserve; if (bGateCenterChanged || bGateStateChanged) { + const FIntVector PreviousGateCenter = LastWalkableGateCenter; VoxelForgeStartupTrace::RecordEvent(TEXT("walkable_gate"), FString::Printf( TEXT("\"support_center\":[%d,%d,%d],\"stream_center\":[%d,%d,%d]," - "\"ready\":%d,\"ready_tiles\":%d,\"required_tiles\":9,\"speed_cm_s\":%s"), + "\"previous_support_center\":[%d,%d,%d],\"center_delta\":[%d,%d,%d]," + "\"center_changed\":%d,\"ready\":%d,\"ready_tiles\":%d," + "\"required_tiles\":9,\"speed_cm_s\":%s,\"desired_epoch\":%u"), WalkableCenter.X, WalkableCenter.Y, WalkableCenter.Z, CenterChunk.X, CenterChunk.Y, CenterChunk.Z, - bWalkableRingReadyForReserve ? 1 : 0, WalkableRingReadyCount, - *FString::SanitizeFloat(PlayerHeading.Size2D()))); + PreviousGateCenter.X, PreviousGateCenter.Y, PreviousGateCenter.Z, + bGateCenterChanged ? WalkableCenter.X - PreviousGateCenter.X : 0, + bGateCenterChanged ? WalkableCenter.Y - PreviousGateCenter.Y : 0, + bGateCenterChanged ? WalkableCenter.Z - PreviousGateCenter.Z : 0, + bGateCenterChanged ? 1 : 0, bWalkableRingReadyForReserve ? 1 : 0, + WalkableRingReadyCount, *FString::SanitizeFloat(PlayerHeading.Size2D()), DesiredEpoch)); bHasLastWalkableGateSample = true; LastWalkableGateCenter = WalkableCenter; bLastWalkableGateReady = bWalkableRingReadyForReserve; } } + if (bBlockAllCoreAdmissionWhileCompiling) + { + RecordSchedulerSkip(TEXT("core_region_compile_pending")); + return; + } const bool bPawnMoving = PlayerHeading.Size2D() >= 50.0f; EffectiveNearTaskReserve = GVoxelForgeStreamingDynamicNearReserve == 0 ? AuthoredNearTaskReserve @@ -11451,6 +11935,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw } //========================================================================= + if (bUseVoxelForgeCore && bCoreRegionCompilePending + && GVoxelForgeCoreRegionConcurrentAdmission == 0) + { + RecordSchedulerSkip(TEXT("core_region_compile_pending")); + return; + } + // Submit pending work (collision-critical -> edits -> coarse shell -> fine detail). // Soumettre le travail (collision critique -> modifications -> enveloppe grossiere -> detail fin). // Coarse work is capped below the full task budget so later near requests keep admission. @@ -11773,6 +12264,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw int32 FullCapDeclinedTiles = 0; int32 ReserveDeclinedTiles = 0; int32 InnerLoadDeclinedTiles = 0; + int32 ResultBacklogCapDeclinedTiles = 0; + const bool bResultBacklogCapHitThisUpdate = + StreamingResultBacklogCapStops.load(std::memory_order_relaxed) + > ResultBacklogCapStopsAtUpdateEntry; for (int32 Level = 0; Level < 9; ++Level) { AdmissionCapByLevel[Level] = bUseCoarseFirstOrder @@ -11817,6 +12312,12 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // La reserve proche configuree retient ce niveau de detail. ReserveDeclinedTiles += DesiredMissingByLevel[Level]; } + else if (bResultBacklogCapHitThisUpdate) + { + DeclinedRuleByLevel[Level] = 6; // Result backlog is at its independent safety cap. + // Le plafond independant de resultats en attente est atteint. + ResultBacklogCapDeclinedTiles += DesiredMissingByLevel[Level]; + } else if (SchedulerLoadAttemptsByLevel[Level] > SchedulerSubmittedByLevel[Level]) { DeclinedRuleByLevel[Level] = 4; // LoadTile/LoadCoreTile declined despite a scheduler attempt. @@ -11855,29 +12356,40 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw "\"walkable_ready\":%d,\"moving\":%d," "\"core_region_compile_pending\":%d,\"core_ready\":%d,\"coarse_first\":%d," "\"dynamic_near_reserve\":%d,\"legacy_pending_result_cap\":%d," + "\"core_region_concurrent_admission\":%d," "\"max_tasks\":%d,\"near_reserve\":%d," "\"coarse_cap\":%d,\"first_coarse_level\":%d," "\"submission_stop_level\":%d,\"submission_stop_cap\":%d," "\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d," "\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d," + "\"queued_results\":%d,\"queued_result_bytes\":%llu," + "\"result_backlog\":%d,\"result_backlog_cap\":%d," + "\"region_independent_submissions\":%llu,\"region_dependent_declines\":%llu," "\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%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," "\"level_cap_returns_by_lod\":%s,\"level_cap_breaks_by_lod\":%s," "\"full_cap_declined_tiles\":%d,\"reserve_declined_tiles\":%d," - "\"inner_load_declined_tiles\":%d,\"critical_full_cap_breaks\":%d," + "\"inner_load_declined_tiles\":%d,\"result_backlog_cap_declined_tiles\":%d," + "\"critical_full_cap_breaks\":%d," "\"dirty_full_cap_breaks\":%d,\"band_coarse_cap_breaks\":%d"), static_cast(GFrameCounter), CenterChunk.X, CenterChunk.Y, CenterChunk.Z, bDesiredRebuiltThisUpdate ? 1 : 0, bSubmissionLoopRan ? 1 : 0, bAllChunksLoaded ? 1 : 0, bWalkableRingReadyForReserve ? 1 : 0, bPawnMoving ? 1 : 0, bCoreRegionCompilePending ? 1 : 0, IsVoxelForgeCoreReady() ? 1 : 0, GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, - GVoxelForgeStreamingLegacyPendingResultCap, MaxTasks, + GVoxelForgeStreamingLegacyPendingResultCap, + GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks, NearTaskReserve, CoarseTaskCap, FirstCoarseLevel, SchedulerOrderStopLevel, SchedulerOrderStopCap, PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard, ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate, + QueuedStreamingResultCount.load(std::memory_order_relaxed), + static_cast(QueuedStreamingResultBytes.load(std::memory_order_relaxed)), + GetStreamingResultBacklogCount(), GVoxelForgeStreamingResultBacklogCap, + static_cast(CoreRegionIndependentSubmissions), + static_cast(CoreRegionDependentDeclines), *FString::SanitizeFloat(BusySeconds), *FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0), *IntArrayJson(DesiredMissingByLevel), *IntArrayJson(LoadedByLevel), @@ -11885,7 +12397,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw *IntArrayJson(SchedulerLoadAttemptsByLevel), *IntArrayJson(AdmissionCapByLevel), *IntArrayJson(DeclinedRuleByLevel), *IntArrayJson(SchedulerLevelCapReturns), *IntArrayJson(SchedulerLevelCapBreaks), FullCapDeclinedTiles, ReserveDeclinedTiles, - InnerLoadDeclinedTiles, SchedulerCriticalFullCapBreaks, + InnerLoadDeclinedTiles, ResultBacklogCapDeclinedTiles, SchedulerCriticalFullCapBreaks, SchedulerDirtyFullCapBreaks, SchedulerBandCoarseCapBreaks)); LastStreamingSchedulerSampleSeconds = SchedulerTraceNow; LastStreamingSchedulerWorkerCycles = TotalWorkerCycles; @@ -11914,6 +12426,11 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor } if (GVoxelForgeStep1GenerationCap == 0) return; if (PendingTiles.Contains(Tile)) return; + if (IsStreamingResultBacklogCapped()) + { + StreamingResultBacklogCapStops.fetch_add(1, std::memory_order_relaxed); + return; + } const int32 MaxTasks = GetMaxConcurrentTasks(); // T2.d — core-clamped const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; @@ -12048,14 +12565,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor Empty.BandChunkHi = BandChunkHi; Empty.WorkPriority = WorkPriority; Empty.bStep1SupportCohort = bStep1SupportCohort; - if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical) - CriticalProcessQueue.Enqueue(MoveTemp(Empty)); - else if (WorkPriority == EVoxelTileWorkPriority::Edited) - EditedProcessQueue.Enqueue(MoveTemp(Empty)); - else if (WorkPriority == EVoxelTileWorkPriority::LOD0) - NearProcessQueue.Enqueue(MoveTemp(Empty)); - else - ProcessQueue.Enqueue(MoveTemp(Empty)); + EnqueueStreamingResult(MoveTemp(Empty)); return; } } @@ -12064,6 +12574,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor MakeShared(); PendingTileCancellation.Add(Tile, Cancellation); ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); + ActiveTileTaskCount.fetch_add(1, std::memory_order_relaxed); // BackgroundNormal priority: coarse gen runs on background workers that YIELD to foreground // (game/render-thread) tasks. Without this, raising MaxConcurrentTasks past the spare @@ -12087,8 +12598,13 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor struct FTaskGuard { std::atomic& Counter; - ~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); } - } Guard{ActiveTaskCount}; + std::atomic& TileCounter; + ~FTaskGuard() + { + Counter.fetch_sub(1, std::memory_order_relaxed); + TileCounter.fetch_sub(1, std::memory_order_relaxed); + } + } Guard{ActiveTaskCount, ActiveTileTaskCount}; ActiveGenerationWorkerCount.fetch_add(1, std::memory_order_relaxed); const uint64 WorkerTaskStartCycles = FPlatformTime::Cycles64(); struct FWorkerGuard @@ -12137,14 +12653,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor Canceled.bStep1SupportCohort = bStep1SupportCohort; Canceled.bAborted = true; Canceled.bObsolete = true; - if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical) - CriticalProcessQueue.Enqueue(MoveTemp(Canceled)); - else if (WorkPriority == EVoxelTileWorkPriority::Edited) - EditedProcessQueue.Enqueue(MoveTemp(Canceled)); - else if (WorkPriority == EVoxelTileWorkPriority::LOD0) - NearProcessQueue.Enqueue(MoveTemp(Canceled)); - else - ProcessQueue.Enqueue(MoveTemp(Canceled)); + EnqueueStreamingResult(MoveTemp(Canceled)); }; if (ShouldAbortWork()) return; @@ -12207,24 +12716,36 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor if (!ShouldAbortWork()) { Result.WorkerTaskEndCycles = FPlatformTime::Cycles64(); - if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical) - CriticalProcessQueue.Enqueue(MoveTemp(Result)); - else if (WorkPriority == EVoxelTileWorkPriority::Edited) - EditedProcessQueue.Enqueue(MoveTemp(Result)); - else if (WorkPriority == EVoxelTileWorkPriority::LOD0) - NearProcessQueue.Enqueue(MoveTemp(Result)); - else - ProcessQueue.Enqueue(MoveTemp(Result)); // move: don't copy the geometry payload + EnqueueStreamingResult(MoveTemp(Result)); // move: don't copy the geometry payload } }, TaskPriority); } void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority) { - if (GVoxelForgeStep1GenerationCap == 0 || bCoreRegionCompilePending) return; + if (GVoxelForgeStep1GenerationCap == 0) return; + if (bCoreRegionCompilePending) + { + if (GVoxelForgeCoreRegionConcurrentAdmission == 0) + { + ++CoreRegionDependentDeclines; + return; + } + if (PendingCoreRegionTileSet.Contains(Tile) + || CoreRegionCompileTilesInFlight.Contains(Tile)) + { + ++CoreRegionDependentDeclines; + return; + } + } if (PendingTiles.Contains(Tile) || !IsVoxelForgeCoreReady()) return; const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid() || State->bRegionCompileFailed.load(std::memory_order_acquire)) return; + if (IsStreamingResultBacklogCapped()) + { + StreamingResultBacklogCapStops.fetch_add(1, std::memory_order_relaxed); + return; + } const int32 MaxTasks = GetMaxConcurrentTasks(); const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; @@ -12241,6 +12762,11 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority MakeShared(); PendingTileCancellation.Add(Tile, Cancellation); ActiveTaskCount.fetch_add(1, std::memory_order_relaxed); + ActiveTileTaskCount.fetch_add(1, std::memory_order_relaxed); + if (bCoreRegionCompilePending) + { + ++CoreRegionIndependentSubmissions; + } const uint32 TaskEpoch = GenerationEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch; @@ -12272,8 +12798,13 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority struct FTaskGuard { std::atomic& Counter; - ~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); } - } Guard{ActiveTaskCount}; + std::atomic& TileCounter; + ~FTaskGuard() + { + Counter.fetch_sub(1, std::memory_order_relaxed); + TileCounter.fetch_sub(1, std::memory_order_relaxed); + } + } Guard{ActiveTaskCount, ActiveTileTaskCount}; ActiveGenerationWorkerCount.fetch_add(1, std::memory_order_relaxed); const uint64 WorkerTaskStartCycles = FPlatformTime::Cycles64(); struct FWorkerGuard @@ -12294,15 +12825,9 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority auto Enqueue = [this, WorkPriority](FChunkResult&& Result) { + Result.WorkPriority = WorkPriority; Result.WorkerTaskEndCycles = FPlatformTime::Cycles64(); - if (WorkPriority == EVoxelTileWorkPriority::CollisionCritical) - CriticalProcessQueue.Enqueue(MoveTemp(Result)); - else if (WorkPriority == EVoxelTileWorkPriority::Edited) - EditedProcessQueue.Enqueue(MoveTemp(Result)); - else if (WorkPriority == EVoxelTileWorkPriority::LOD0) - NearProcessQueue.Enqueue(MoveTemp(Result)); - else - ProcessQueue.Enqueue(MoveTemp(Result)); + EnqueueStreamingResult(MoveTemp(Result)); }; if (bShuttingDown.load(std::memory_order_acquire) @@ -13816,7 +14341,10 @@ void AVoxelWorld::ApplyCoreModification(const FVoxelModification& Modification) State->bRegionCompileFinished.exchange(false, std::memory_order_acq_rel); bCoreRegionCompilePending = false; PendingCoreRegionTiles.Reset(); + PendingCoreRegionTileSet.Reset(); + CoreRegionCompileTilesInFlight.Reset(); PendingCoreRetainedTiles.Reset(); + CoreRegionPendingSinceSeconds = 0.0; } std::vector Desired; diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index 163f9db..744c4d9 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -704,6 +704,11 @@ private: TArray& OutLeavers); void StartPendingCoreRegionCompile(); void LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority); + void EnqueueStreamingResult(FChunkResult&& Result); + void RecordStreamingResultDequeued(const FChunkResult& Result); + uint64 GetStreamingResultPayloadBytes(const FChunkResult& Result) const; + int32 GetStreamingResultBacklogCount() const; + bool IsStreamingResultBacklogCapped() const; void ApplyCoreModification(const FVoxelModification& Modification); void BuildDesiredTransitionIndex(); float GetActiveVoxelSizeCm() const; @@ -768,7 +773,20 @@ private: FVector CoreFallbackCenterLocalCm = FVector::ZeroVector; bool bCoreRegionCompilePending = false; TArray PendingCoreRegionTiles; + TSet PendingCoreRegionTileSet; TArray PendingCoreRetainedTiles; + TSet CoreRegionCompileTilesInFlight; + FIntVector LastCoreRegionRequestCenter = FIntVector(MAX_int32, MAX_int32, MAX_int32); + FIntVector PendingCoreRegionRequestCenter = FIntVector::ZeroValue; + FIntVector ActiveCoreRegionCompileCenter = FIntVector::ZeroValue; + bool bHasLastCoreRegionRequestCenter = false; + double CoreRegionPendingSinceSeconds = 0.0; + double ActiveCoreRegionCompileStartSeconds = 0.0; + uint64 ActiveCoreRegionCompileStartCycles = 0; + uint64 CoreRegionIndependentSubmissions = 0; + uint64 CoreRegionDependentDeclines = 0; + int32 CoreRegionCompileRequestSerial = 0; + int32 ActiveCoreRegionCompileRequestSerial = 0; public: @@ -1098,6 +1116,16 @@ public: TQueue EditedProcessQueue; TQueue NearProcessQueue; TQueue ProcessQueue; + // The cap counts completed MPSC results plus admitted tile tasks, so a slow game-thread apply + // cannot accumulate an unbounded payload queue while completed work never consumes worker slots. + // Le plafond compte les resultats MPSC et les taches admises; un apply lent ne peut donc pas + // accumuler sans limite, et les resultats termines ne reprennent pas de slot worker. + std::atomic QueuedStreamingResultCount{0}; + std::atomic QueuedStreamingResultBytes{0}; + std::atomic PeakQueuedStreamingResultCount{0}; + std::atomic PeakQueuedStreamingResultBytes{0}; + std::atomic ActiveTileTaskCount{0}; + std::atomic StreamingResultBacklogCapStops{0}; // Keys stay pending from task submission until the game thread applies the result; completed results can wait here. // Les cles restent en attente de la soumission jusqu'a l'application sur le thread de jeu ; les resultats termines y attendent. TSet PendingTiles; @@ -1302,6 +1330,9 @@ public: float HeadlessStreamingTestSpeedCmPerSecond = 0.0f; float HeadlessStreamingTestStartSeconds = 0.0f; float HeadlessStreamingTestExitSeconds = 0.0f; + float HeadlessStreamingTestTeleportAtSeconds = 0.0f; + float HeadlessStreamingTestTeleportDistanceCm = 0.0f; + bool bHeadlessStreamingTestTeleportDone = false; FVector HeadlessStreamingTestDirection = FVector::XAxisVector; FVector HeadlessStreamingTestLastActualPosition = FVector::ZeroVector; double HeadlessStreamingTestBeginSeconds = 0.0; diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index a6d6e10..2fae682 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -17487,6 +17487,9 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin const double ActorBeginSeconds = FPlatformTime::Seconds(); constexpr double WalkStartSeconds = 30.0; constexpr double RunDurationSeconds = 45.0; + float WalkSpeedCmPerSecond = 600.0f; + FParse::Value(FCommandLine::Get(), TEXT("voxel.TestMoveSpeedCmPerSecond="), + WalkSpeedCmPerSecond); TArray SpawnTickMilliseconds; TArray WalkTickMilliseconds; TArray> TickSamples; @@ -17566,11 +17569,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin return false; } UE_LOG(LogTemp, Display, - TEXT("[VoxelForgeCoreWorldStreamTrace] pass complete=1 recipe=%s voxel_cm=%.3f walk_speed_cm_s=600 walk_direction=+Y"), + TEXT("[VoxelForgeCoreWorldStreamTrace] pass complete=1 recipe=%s voxel_cm=%.3f walk_speed_cm_s=%.0f walk_direction=+Y"), Arguments.CoreHybridMesherVersion == 2 ? TEXT("WorldAssemblerHybridV2") : (Arguments.CoreHybridMesherVersion == 1 ? TEXT("WorldAssemblerHybridV1") : TEXT("MarchingCubesV1")), - Arguments.CoreVoxelSizeCm); + Arguments.CoreVoxelSizeCm, WalkSpeedCmPerSecond); return true; } diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 376c3c9..9e1accf 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -1021,6 +1021,9 @@ namespace VoxelForgeCore bool compile(const ShapeBuildRequest& request, const JobScheduler& scheduler, std::string& error); + // True only when a fine tile needs a separately compiled region before generation. + // Vrai seulement si une tuile fine doit attendre la compilation d'une region dediee. + bool needsTileRegion(const TileKey& tile) const; // Compile missing detail regions for entering fine tiles. Generated tile keys map to // shared world-coordinate regions. The default analyticOnly path coalesces each entering // LOD slab, builds its primitives/bins, and evaluates density on demand without diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 368f113..60f9c51 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -909,6 +909,16 @@ namespace VoxelForgeCore return ok; } + bool World::needsTileRegion(const TileKey& key) const + { + if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt() + ||key.lod<0||key.lod>=impl_->configuration.fullResolutionLodCount) + return false; + if(CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))return false; + std::shared_lock lock(impl_->tileRegionMutex); + return !impl_->tileRegions.contains(key); + } + bool World::compileTileRegion(std::span tiles,std::uint32_t workerLimit, const JobScheduler& scheduler,std::string& error, std::size_t* outCompiledTiles, diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md index 0bc0ead..75996fa 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md @@ -1310,3 +1310,28 @@ No new source file was added. The likely compile spots are `VoxelWorld.cpp` and file was modified. No commit, push or stash was made. — Luna + + +## World assembler S8 / M3 Round 12 — 2026-10-19 + +The S7 trace's moving-path `bCoreRegionCompilePending` stop was real: the matched 6 m/s negative +control rejected 11 whole scheduler passes while region pages were rebuilt. With concurrent +admission enabled, those passes disappeared and 110 unrelated tiles submitted; all 11 support +centers still had a ready nine-tile ring. Overall moving worker use stayed near 2.7 of 10 slots, so +this closes the region gate without changing the separate spawn bottleneck. + +The 60 m/s + 400 m teleport run admitted independent work too, but most center changes outran the +walkable ring. Spawn remains 7.7 s: 306 LOD8 jobs make a broad tail, while no one job exceeds +95 ms. The measured queue cap held the slow-apply backlog at 256 results / 76.2 MB; disabling it +reached 1,577 / 125.1 MB. Core-off, M2 fixtures, SurfaceWorld, FloatingIslands, S3/S4 and W1/W8/W16 +checks passed. The optional tick subsystem marker was not added, leaving the 29 ms walk maximum +unattributed. + +Full metrics, the matched controls, hashes, commands and async audit are in +[`WorldAssemblerM3/Round12/REPORT.md`](../../WorldAssemblerM3/Round12/REPORT.md). + +No new source file was added. Compile spots are `VoxelWorld.cpp/.h`, the Core public header and +`VoxelForgeCore.cpp`, `VoxelForgeExploreCommandlet.cpp`, and the BuildHost harness. No owner +project/editor, asset or external plugin file was modified; no commit, push or stash was made. + +— Luna diff --git a/Standalone/Research/WorldAssemblerM3/Round12/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round12/REPORT.md new file mode 100644 index 0000000..4e35045 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round12/REPORT.md @@ -0,0 +1,233 @@ +# World assembler S8 / M3 Round 12 — moving region admission and bounded results + +**Result:** the S7 region-pending stop is removed for independent tiles. At 6 m/s, the matched +negative control logged 11 global admission skips; the new mode logged none and admitted 110 +independent tiles during those windows. All 11 changed support centers had a ready 9/9 walkable +ring. This did not materially change overall moving worker occupancy: about 110 of 150 moving +slot-seconds remained idle. A 60 m/s teleport run admitted unrelated tiles too, but most centers +still outran their walkable ring. + +The result queue is now separately bounded at 256 queued results + admitted tile tasks + held +prequeue results. Under one-result-per-frame apply with a 60 m/s run and 400 m teleport, the tracked +result payload peak was 76.2 MB; the uncapped control reached 125.1 MB. Spawn remains about 7.7 s, +not the roughly 4.1 s ideal from 41 worker-seconds / 10 slots. The timeline shows a broad LOD8 tail, +not one abnormally long tile. No safe low-cost spawn fix was supported by this measurement. + +## 1. Region gate measurement and change + +`BuildCoreDesiredTiles` asks for region pages when the desired tile center changes or its initial +desired region set is established. The old `bCoreRegionCompilePending` path stopped the whole Core +submission pass. The new default `voxel.CoreRegionConcurrentAdmission=1` continues through +`LoadCoreTile` and refuses only keys in `PendingCoreRegionTileSet` or +`CoreRegionCompileTilesInFlight`. `voxel.CoreRegionConcurrentAdmission=0` restores the old global +gate as the negative control. Core tile arithmetic and recipes are unchanged. + +Region pages are constructed away from the lookup map and published under `tileRegionMutex` as +immutable shared owners. `generateTile` copies the matching `shared_ptr` while holding the map's +shared lock, then keeps it for the complete read. Map eviction cannot free a page while an already +running tile task holds it. The actor's shared `WorldLock` prevents edits/resets from mutating the +Core world while readers or a region compile are active. + +Matched fresh BuildHost processes used the staged owner `SurfaceWorld` settings, seed 0, 25 cm Core +scale, 2,578-key desired set, Legacy order, Dynamic near reserve, apply boost 0, 10 task slots, +Normal priority, `-corelimit=12`, and a +Y move starting at 30 s. Both modes used backlog cap 256. + +| Moving 6 m/s | Region CVar 0 | Region CVar 1 | +|---|---:|---:| +| Region requests / completions | 11 / 11 | 11 / 11 | +| Reasons | 1 initial set, 10 center moves | 1 initial set, 10 center moves | +| Whole-scheduler region skips | 11 | 0 | +| Pending interval sum / worst interval | 426.3 / 40.9 ms | 433.1 / 41.4 ms | +| Compile wall sum / slowest compile | 10.8 / 2.5 ms | 10.5 / 2.4 ms | +| Independent tile submissions / dependent declines | 0 / 0 | 110 / 0 | +| Changed support centers ready 9/9 at the change | 11 / 11 | 11 / 11 | +| Walkable lag at those centers | 0 ms | 0 ms | +| Moving frames with an LOD0 key missing | 173 | 163 | +| Moving worker busy / idle slot-seconds (15 s × 10 slots) | 39.7 / 110.3 s | 39.9 / 110.1 s | + +The pending interval is the request-to-completion time recorded for each compile; it is not all +global blocking in the new mode. The compiler's measured wall time summed to only about 11 ms over +the run. The default mode removes the full-pass skip and submits keys independent of the entering +region. Its total worker supply remains low: moving work averages about 2.66 of 10 busy workers. +Each of the 11 negative-control whole-pass skips had 92–144 desired keys missing and zero active +tile tasks. The CVar1 run has no corresponding global skip. +Per-LOD counts added to `scheduler_skip` make the LOD0-missing-frame count include the 6 globally +gated frames that had LOD0 work missing in the negative control. + +The fast trace used 60 m/s and teleported 400 m after 2 s of the move. It recorded 113 region +requests (initial set plus 112 center moves), all completed, with 381.4 ms aggregate compile wall +time (30.8 ms worst) and 4,523.7 ms accumulated pending interval (69.9 ms worst). Concurrent +admission recorded 782 independent submissions, 983 dependent-key declines, and zero global +region skips. Of 113 changing support centers, 8 were already 9/9 ready on the change event, 3 +became ready before the next center change, and 102 did not reach 9/9 before leaving or test end. +The worst resolved lag was 106.3 ms. LOD0 was missing on 480 of 488 moving scheduler frames. This +speed is beyond what the current collision ring can follow. + +## 2. Spawn, worker timeline and tail + +| Fresh owner-plan spawn | S7 Dynamic reference | Round12 region CVar 0 | Round12 region CVar 1 | +|---|---:|---:|---:| +| Full set (2,578 keys, from BeginPlay) | 7.780 s | 7.820 s | 7.706 s | +| Walkable 9/9 ring | 0.581 s | 0.583 s | 0.566 s | +| Full LOD0 (512 tiles) | 1.439 s | 1.442 s | 1.424 s | +| Worker task-seconds to full set | 41.364 | 41.966 | 41.229 | +| Integrated busy workers / 10 (first request → ready) | 5.63 | 5.63 | 5.60 | +| Idle slot-seconds (same interval) | 32.056 | 32.63 | 32.37 | +| `UWorld::Tick` spawn p95 / max | 2.262 / 5.230 ms | 2.387 / 5.273 ms | 2.359 / 5.383 ms | +| `UWorld::Tick` move p95 / max | 2.512 / 28.893 ms | 2.472 / 28.257 ms | 2.377 / 28.967 ms | + +The CVar A/B changes the spawn full-set time by only 0.11 s; the region issue occurs during the +later move. The Round12 CVar1 first-request-to-ready span was 7.360 s with 41.229 worker-seconds. +In one-second bins its worker task time was 3.94, 3.91, 3.34, 6.51, 5.73, 7.75 and 7.81 worker- +seconds, followed by 2.24 worker-seconds in the final 0.36 s. Average occupancy was 5.60/10 and +idle capacity was 32.37 slot-seconds. No sustained ten-worker plateau appears. That is about +16.0 ms of elapsed task time per initial tile, similar to S7's 16.04 ms and still about 9% above +the S7 old-path control's 14.62 ms. It does not show whether the difference is CPU contention or +other elapsed-time variation. + +The 306 LOD8 jobs consumed 18.798 worker-seconds (45.6% of total), averaging 61.4 ms each; their +worker-time p95 was 79.8 ms and max 94.7 ms. The final 20 completed tile records were all LOD8 and +landed between 7.587 and 7.765 s. This is a group tail of ordinary large jobs, not one serial tile +to split. Across the initial set, worker queue wait averaged 1.71 ms per tile (p95 9.71 ms, max +41.44 ms), and game-thread mesh apply totaled 0.250 s. The trace records elapsed time, not CPU +cycles, cache misses or task-pool wakeup causes; it cannot identify memory bandwidth or thread-local +cache contention. The 10-slot cap reflects 12 logical cores with two cores reserved by +`GetMaxConcurrentTasks`; I left that reserve in place without evidence that using all 12 improves +the spawn while retaining tick time. + +The optional actor-tick subsystem markers were not added. The matched run reports the 28.967 ms +maximum walk tick, but this trace has no per-call marker at that maximum, so it remains unattributed. +These are headless/null-RHI actor tick measurements, not rendered frame timings. + +## 3. Result backlog bound + +`voxel.StreamingResultBacklogCap` defaults to 256 and counts queued results, active tile tasks, and +held visible-LOD0 prequeue results. It is distinct from worker-slot admission; finished results +still do not occupy slots. Zero disables the cap and is the safety negative control. + +| Run | Queue cap | Peak result count | Peak payload bytes | Cap stops | Peak private memory | +|---|---:|---:|---:|---:|---:| +| 6 m/s normal move | 256 | 116 | 17,024,546 | 0 | 3.48 GB | +| 60 m/s + 400 m teleport | 256 | 114 | 17,153,276 | 0 | 3.80 GB | +| 60 m/s + teleport, one mesh apply/frame | 256 | 256 | 76,201,412 | 843,198 | 3.60 GB | +| Same slow-apply control, cap disabled | 0 | 1,577 | 125,098,792 | 0 | 3.63 GB | + +The slow-apply test still reached its initial set: 23.890 s capped versus 23.493 s uncapped. The +cap reduced the tracked peak by 1,321 results and 48.9 MB of payload, at a 0.397 s startup +difference in this pair. `cap_stops` counts repeated admission attempts, not unique tile keys. The +atomic peak tracker saw 256 / 1,577 results; periodic `result_queue_frame` samples saw 255 / 1,576, +respectively, because a frame sample can miss the brief peak. Bytes are result payload estimates, +excluding queue-node and allocator overhead. The default stops new admissions as work drains; it +does not reduce active task capacity when the queue is below the cap. + +## 4. Quality, determinism and async audit + +- BuildHost Core Off legacy passed at 12.5 cm. HybridV1 and HybridV2 each passed five fixtures and + same-LOD border checks at 25 cm. +- Final-module SurfaceWorld passed the full actor, collision, anchor-move and carve checks at 25 cm. + Its W1/W8/W16 worker matrix matched: density `0cd2a1a4152ee4aa`, mesh `80a5b054b904b1a2`, payload + `bf2f1b40dff38acc`, topology `56074198682d5e18`. S3 ground agreement passed 128 points: far7/8 + maximum error 0.0427 cm, cross-LOD 0.0000 cm, 2 cm tolerance; the 100 cm offset control was + detected. +- FloatingIslands passed the final BuildHost W1/W8/W16 actor matrix at 25 cm, with fixed water, + collision, edits and LOD0/1 coverage. The standalone W1/W8/W16 `--stream-hash` runs matched on all + nine LODs. Its host scheduler clamps `parallel_for` to at most eight actual workers; W16 is the + requested worker-limit input routed through that eight-thread executor. The S4 audit passed: + 385/385 far-cap bands emitted, 128 agreement points within 0.0504 cm (2 cm tolerance), 100 cm + offset detected, and 2,520/2,520 coverage rays hit. +- No field, mesh or recipe output changed; recipe version remains unchanged. Water remains fixed + for the season, and player carve/fill remains unrestricted. + +Async review of the diff: + +- `FChunkResult::Epoch` and desired/modification epochs remain carried and checked. The region task + captures generation and desired epochs plus its compile serial. +- All four result queues remain `EQueueMode::Mpsc`; result insertion is centralized in + `EnqueueStreamingResult`. +- Tile and region workers check `bShuttingDown`; `EndPlay` sets both shutdown flags, waits for + `ActiveTaskCount` to reach zero, then drains all four result queues. +- `ModsLock` is unchanged. Workers generate data only; UObject and RMC mutation remains on the game + thread. The region map has its own shared mutex, and readers retain immutable pages with + `shared_ptr` until their tile job ends. + +## 5. Build, commands and run record + +BuildHost compiled at Normal priority with 12 actions. Link-safety passed (96,279 dumpbin lines +checked against 421 names); the five staged asset hashes and 18 manifest shims passed. The final +SurfaceWorld quality run took about 10 minutes and peaked at 3.896 GB private memory. The standalone +full build passed with the existing `vswhere.exe` lookup warning and existing C4838 conversions in +`VoxelStrateManager.cpp:3974–3975`. + +Build commands: + +```powershell +.\Tools\BuildVoxelForgeCoreHost.ps1 -MaxParallelActions 12 -TimeoutSeconds 900 +.\Standalone\Build.ps1 -Target All -CompileWorkers 12 -BuildPriority Normal +``` + +Matched region-gate commands (cap held at 256 for both sides): + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -CoreRegionConcurrentAdmission 0 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 6000 -TestTeleportAfterSeconds 2 -TestTeleportDistanceCm 40000 -TimeoutSeconds 900 +``` + +Slow-apply backlog controls used these commands; the cap-off run is the backlog negative control: + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 6000 -TestTeleportAfterSeconds 2 -TestTeleportDistanceCm 40000 -Step1MaxMeshApplies 1 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 0 -TestMoveSpeedCmPerSecond 6000 -TestTeleportAfterSeconds 2 -TestTeleportDistanceCm 40000 -Step1MaxMeshApplies 1 -TimeoutSeconds 900 +``` + +Other BuildHost checks were: + +```powershell +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -CoreOffTest -CoreVoxelSizeCm 12.5 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -HybridFixturesOnly -CoreMesherRecipe WorldAssemblerHybridV1 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -HybridFixturesOnly -CoreMesherRecipe WorldAssemblerHybridV2 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +.\Tools\TestVoxelForgeCoreWorldHost.ps1 -FloatingIslandsOnly -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +``` + +Standalone determinism used three fresh processes (`--workers=1`, `8`, and `16`), each with: + +```powershell +.\Standalone\bin\VoxelForgeCoreStandalone.exe --workers=1 --world-assembler-prototype --stream-benchmark --stream-hash --force-generate --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --process-priority=Normal --process-affinity=4095 --frame-limit=60 +``` + +The S4 standalone audit command was: + +```powershell +.\Standalone\bin\VoxelForgeCoreStandalone.exe --world-assembler-audit --workers=12 --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --process-priority=Normal --process-affinity=4095 --frame-limit=60 +``` + +SHA-256 of the executables/modules used by the final runs: + +| Binary | SHA-256 | +|---|---| +| UE 5.7 `UnrealEditor-Cmd.exe` | `ECCE9996191F7F7E69B3D104EECDD0E678AE69176FDCCD79828C3E75A0654D6D` | +| BuildHost `UnrealEditor-VoxelForge.dll` | `8B82EE6C6FBB511AE766EB3D8646A7E9491952F41445CB62D49D71D0B9783418` | +| BuildHost `UnrealEditor-VoxelForgeCore.dll` | `0A1EB7BB27C3C74A4AA0996E0A24E74224D7B8FFFFF113B1071FA42E18E8375A` | +| BuildHost `UnrealEditor-VoxelForgeEditor.dll` | `B3BC5E61172C5B21D928A5ACB5F22F8D9E00179B481B675804838DE9A620010D` | +| `Standalone/bin/VoxelForgeCoreStandalone.exe` | `9C1601D7FCACCAAC16A2BA9DF0E7826E18746028A4D08FD23EEC479D2F5745BE` | + +Trace logs and queue records are under +`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/`. The final quality +logs and standalone determinism/audit stdout are under `Saved/BuildHost/Round12/`. No new file over +300 KB was placed in `Research/`. + +BuildHost and standalone launch guards checked free physical memory before each process and waited +for at least 4 GiB. Explicit preflights for the final matched run and SurfaceWorld actor test saw +17.33 and 17.11 GiB free. All processes stayed under 8 GiB and used Normal priority / at most 12 +threads. The only crash-folder entry is the pre-existing +`UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000` from 2026-09-17; no new UECC or +`0xC0000142` occurred. Three exploratory PowerShell RAM-probe definitions failed before launching +Unreal; after correcting the interop declaration, the probe and all actual build/test runs passed +on attempt 1. There were no failed Unreal runs or retries. + +No new source file was added. Likely compile spots are `Source/VoxelForge/Private/VoxelWorld.cpp`, +`Source/VoxelForge/Public/VoxelWorld.h`, `Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp`, +`Standalone/Core/src/VoxelForgeCore.cpp` and its public header, plus the PowerShell harness. I did +not open the owner's editor, build `VoxelM.uproject`, edit any `.uasset`, or modify +RealtimeMeshComponent. No commit, push or stash was made. diff --git a/Tools/TestVoxelForgeCoreWorldHost.ps1 b/Tools/TestVoxelForgeCoreWorldHost.ps1 index 9ca09de..9f44665 100644 --- a/Tools/TestVoxelForgeCoreWorldHost.ps1 +++ b/Tools/TestVoxelForgeCoreWorldHost.ps1 @@ -8,6 +8,12 @@ param( [switch]$StartupTrace, [ValidateRange(0, 1)][int]$StartupApplyBoost = 0, [switch]$StreamingTraceOnly, + [ValidateRange(1, 6000)][int]$TestMoveSpeedCmPerSecond = 600, + [ValidateRange(0, 1)][int]$CoreRegionConcurrentAdmission = 1, + [ValidateRange(0, 4096)][int]$StreamingResultBacklogCap = 256, + [ValidateRange(0, 900)][int]$Step1MaxMeshApplies = 0, + [ValidateRange(0, 900)][int]$TestTeleportAfterSeconds = 0, + [ValidateRange(0, 1000000)][int]$TestTeleportDistanceCm = 0, [string]$WorldAssemblyManifestPath = '', [ValidateSet('MarchingCubesV1', 'WorldAssemblerHybridV1', 'WorldAssemblerHybridV2')][string]$CoreMesherRecipe = 'MarchingCubesV1', [switch]$EnableHybridFixturePlacement, @@ -73,6 +79,15 @@ if ($StreamingTraceOnly -and ($SharedFeatureAuditOnly -or $EnableHybridFixturePl $HybridCliffOnly -or $HybridFixturesOnly -or $WorldAssemblyManifestPath)) { throw '-StreamingTraceOnly uses the staged owner plan without additional fixture or manifest overrides.' } +if (-not $StreamingTraceOnly -and ($TestMoveSpeedCmPerSecond -ne 600 -or + $CoreRegionConcurrentAdmission -ne 1 -or $StreamingResultBacklogCap -ne 256 -or + $Step1MaxMeshApplies -ne 0 -or $TestTeleportAfterSeconds -ne 0 -or + $TestTeleportDistanceCm -ne 0)) { + throw 'Streaming, backlog, apply and teleport controls require -StreamingTraceOnly.' +} +if (($TestTeleportAfterSeconds -eq 0) -ne ($TestTeleportDistanceCm -eq 0)) { + throw 'Set both -TestTeleportAfterSeconds and -TestTeleportDistanceCm for the teleport probe.' +} function Assert-UnderPath([string]$Candidate, [string]$Root) { $candidateFull = [IO.Path]::GetFullPath($Candidate).TrimEnd('\', '/') @@ -153,6 +168,13 @@ $RunName += switch ($NearReserveMode) { } if ($EnforceCollisionGate) { $RunName += '_collision_gate' } if ($StartupTrace -or $StreamingTraceOnly) { $RunName += '_trace' } +if ($StreamingTraceOnly) { + $RunName += "_move_${TestMoveSpeedCmPerSecond}_region_${CoreRegionConcurrentAdmission}_backlog_${StreamingResultBacklogCap}" + if ($Step1MaxMeshApplies -gt 0) { $RunName += "_applymax_$Step1MaxMeshApplies" } + if ($TestTeleportAfterSeconds -gt 0) { + $RunName += "_teleport_${TestTeleportAfterSeconds}s_${TestTeleportDistanceCm}cm" + } +} $foreignEditors = @(Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like 'UnrealEditor*' }) @@ -278,12 +300,21 @@ $BaseArguments += "-voxel.StreamingCoarseFirst=$CoarseFirstValue" $BaseArguments += "-voxel.StreamingDynamicNearReserve=$DynamicReserveValue" $BaseArguments += "-voxel.StreamingLegacyPendingResultCap=$LegacyPendingResultCapValue" $BaseArguments += "-voxel.StreamingStartupApplyBoost=$StartupApplyBoost" +$BaseArguments += "-voxel.CoreRegionConcurrentAdmission=$CoreRegionConcurrentAdmission" +$BaseArguments += "-voxel.StreamingResultBacklogCap=$StreamingResultBacklogCap" +if ($Step1MaxMeshApplies -gt 0) { + $BaseArguments += "-voxel.Step1MaxMeshApplies=$Step1MaxMeshApplies" +} if ($StreamingTraceOnly) { $BaseArguments += '-coreworldstreamtrace=1' $BaseArguments += '-voxel.TestMove=1' - $BaseArguments += '-voxel.TestMoveSpeedCmPerSecond=600' + $BaseArguments += "-voxel.TestMoveSpeedCmPerSecond=$TestMoveSpeedCmPerSecond" $BaseArguments += '-voxel.TestMoveStartSeconds=30' $BaseArguments += '-voxel.TestMoveDirection=0,1,0' + if ($TestTeleportAfterSeconds -gt 0) { + $BaseArguments += "-voxel.TestTeleportAfterSeconds=$TestTeleportAfterSeconds" + $BaseArguments += "-voxel.TestTeleportDistanceCm=$TestTeleportDistanceCm" + } } if ($StartupTrace -or $StreamingTraceOnly) { $TracePath = Join-Path $LogDirectory "CoreWorldValidation-$RunName.trace.jsonl" @@ -417,7 +448,7 @@ if ($SharedFeatureAuditOnly) { } if ($StreamingTraceOnly) { if ($Text -notmatch '\[VoxelForgeCoreWorldStreamTrace\] initial_ready=1 .*walk_tick_ms_p95=.*walk_tick_ms_max=' -or - $Text -notmatch '\[VoxelForgeCoreWorldStreamTrace\] pass complete=1 .*walk_speed_cm_s=600 walk_direction=\+Y' -or + $Text -notmatch ("\[VoxelForgeCoreWorldStreamTrace\] pass complete=1 .*walk_speed_cm_s=$TestMoveSpeedCmPerSecond walk_direction=\+Y") -or $Text -notmatch '\[VoxelForgeCoreWorldTest\] pass complete=1') { throw "BuildHost actor-only stream trace did not prove initial readiness and a completed walk sample. See $FinalLog." }