From 5eb9ee76ffcfa93856406a92583bbdc7e599a0fc Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 19 Oct 2026 08:10:10 +0200 Subject: [PATCH] wip(assembler): round S7 -- workers kept busy: in-game cold full view 17.5 s -> 7.8 s Two admission blockers found by per-frame decline codes: the near reserve held four slots idle after LOD0 was done (coarse capped at 6), and finished-but- unapplied results counted against the task cap. Admission now counts queued + running tasks; near reserve releases once the walkable ring is ready while stationary (voxel.StreamingDynamicNearReserve, default on). Walkable ring 0.65 -> 0.58 s, full LOD0 2.28 -> 1.44 s, busy workers 2.2 -> 5.6 of 10. Old behaviour behind voxel.StreamingLegacyPendingResultCap. RMC source reviewed (read-only): batching/fast-cook ranked, not yet worth it on measured apply cost. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- ARCHITECTURE.md | 68 +++- CODEMAP.md | 32 +- Source/VoxelForge/Private/VoxelWorld.cpp | 360 ++++++++++++++++-- Source/VoxelForge/Public/VoxelWorld.h | 17 +- .../gpu_field_port/LUNA_DIALOGUE.md | 27 ++ .../WorldAssemblerM3/Round11/REPORT.md | 261 +++++++++++++ Tools/TestVoxelForgeCoreWorldHost.ps1 | 14 +- 7 files changed, 726 insertions(+), 53 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Round11/REPORT.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 512e43e..2aa1c46 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -578,7 +578,12 @@ driven by `EditorBrush*` props. the result through the normal MPSC queue; the game thread reads the token again before applying. `FChunkResult::Epoch` still guards the generation epoch. Desired work is sorted from the true pawn position, with the occupied/support LOD0 tile and the next tile along the heading in an absolute - high-priority prefix. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.) + high-priority prefix. Admission counts queued/running `ActiveTaskCount`; `PendingTiles` stays until + game-thread apply for duplicate suppression and result bookkeeping, but completed MPSC results do + not occupy a worker slot. `voxel.StreamingLegacyPendingResultCap=1` restores the old result-inclusive + 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.) - **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 @@ -1454,8 +1459,10 @@ opt-in because it did not beat the legacy order on the gates and the viewer coul presentation: set `voxel.StreamingCoarseFirst=1` to admit coarse desired levels from the maximum LOD down to `max(2, FullResClipLevels)`, favoring tiles ahead of travel and then nearer tiles within each LOD; LOD0/1 refinement follows. Default `0` preserves the S4 LOD0-to-LOD8 order. -`voxel.StreamingDynamicNearReserve=1` opts into keeping the authored near-task reserve while the pawn -moves or until the stationary walkable ring is ready; default `0` preserves the previous task budget. +`voxel.StreamingDynamicNearReserve=1` keeps the authored near-task reserve while the pawn moves or +until the stationary walkable ring is ready; this is now the default. Set it to `0` only for the +legacy-admission negative control. After the stationary nine-tile ring is ready, coarse work can use +the formerly reserved slots. The standalone queue defaults to legacy order; `--coarse-first-stream-order` opts into the S5 tiers and `--legacy-stream-order` explicitly selects the legacy order. @@ -1498,12 +1505,10 @@ with a 30 second ceiling; at `0`, the authored budget is unchanged. On the match the boost changed full-set readiness from 17.412 s to 17.346 s and removed count-cap frames, but did not improve end-to-end residency. Keep the normal 4 / 2 ms guard for play. -The trace found the shared worker pool was not saturated: the 10-slot idle estimate was about 132 +The S6 trace found the shared pool was not saturated: its 10-slot idle estimate was about 132 slot-seconds on legacy order and 73 on gated coarse-first, while game-thread tile applies totaled only -about 0.24 seconds during cold residency. Results waited in the result queue, and some frames reached -the four-mesh cap, but expanding the apply budget alone did not move the wall clock. The remaining -floor is worker-side work supply/admission; this trace does not isolate why available worker slots -stay idle. The 1.57 s target remains unmet; the headless trace is not a render-time measurement. +about 0.24 seconds. S7's frame-level admission and cumulative worker-task accounting revisited that +open question; see §8.28. The report records per-LOD worker, queue, result-queue and apply totals, collision latency, frame percentiles, hashes, regressions and the command lines. The standalone 60 m/s viewer now counts @@ -1513,6 +1518,53 @@ W1/W8/W16 world and LOD hashes are unchanged, as are `Epoch`, cancellation, `Mod shutdown-drain and game-thread UObject-creation rules. See [`Standalone/Research/WorldAssemblerM3/Round10/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round10/REPORT.md). +### 8.28 Shared admission accounting and worker supply — World Assembler S7 / M3 Round 11 + +`PendingTiles` is a game-thread result-bookkeeping set: its entry lasts from task submission until +`ApplyTileResult`, so it can include work that has finished and is waiting in one of the MPSC result +queues. It is not the count of active generation tasks. The S6 admission path used +`PendingTiles.Num()` for that cap. On the S7 old-path control, after LOD0 had no missing keys, 877 +admission frames still had LOD1+ work missing and hit the four-slot near reserve; at a representative +frame, occupancy was 6/10 while only four tasks were queued/running and two completed results were +waiting to apply. 806 of those 877 frames had a positive completed-result estimate. + +Admission now uses `ActiveTaskCount`, which is incremented before launching the UE task and released +by its RAII guard when the worker lambda exits. This still counts queued and running work, but completed +results do not consume an admission slot. `voxel.StreamingLegacyPendingResultCap=1` restores the old +`PendingTiles` accounting; the test harness's `NearReserveMode=Legacy` also sets +`voxel.StreamingDynamicNearReserve=0`, reproducing the S6 path. `StreamingDynamicNearReserve` now +defaults to `1`; `StreamingCoarseFirst` remains `0`, and the apply boost remains `0` in the matched +measurement. + +On the 2,578-key owner SurfaceWorld trace, the old-path control reached the full set in 17.527 s, +walkable ring in 0.650 s, and full LOD0 in 2.281 s. Dynamic admission reached them in 7.780 s, +0.581 s, and 1.439 s. Its first-request-to-ready interval was 7.342 s with 41.364 worker-seconds, +or 5.63 integrated busy-worker equivalents. Using the S6 10-slot idle formula on that same interval +gives 32.06 slot-seconds, versus 133.01 on the S7 old-path control and 132.1 in S6. The ten S7 +spawn samples that ended with free admission capacity and missing keys all recorded a submission +stop at the 10-task cap (one worker could finish before the end-of-pass sample); the frame trace +found no near-reserve decline, all-loaded latch, walkable gate failure, or inner `LoadTile` rejection +in those passes. Result-aware admission therefore removes the measured cold-set blocker and +reproduces the legacy control within 0.7%, but the 7.78 s full-set result still misses the rough +4.14 s `worker-seconds / 10 slots` estimate. Across the full 45 s trace, free-capacity frames with +missing keys were classified by LOD as near reserve or full cap; none was unclassified. + +The per-frame `running_workers` value is biased high when sampled only at admission/apply boundaries; +S7 added cumulative task intervals and found 5.63 busy-worker equivalents over the first-request-to- +ready span, with eight sustained overlaps in the reconstructed intervals; a zero-duration endpoint +tie briefly counts nine. This locates +the remaining gap downstream of an admission refusal, but does not distinguish UE task-pool dispatch +from work internal to a tile. Measured worker task time rose 9.7% versus the negative control +(16.04 ms versus 14.62 ms per initial tile); hashes match and the trace records elapsed task time, +not CPU cycles. During the later +Y move, the loader's `bCoreRegionCompilePending` guard also +blocked 10 legacy and 11 dynamic submission passes despite free tasks; each inner-decline record +had that flag set. The tick trace reports aggregate `TestWorld->Tick()` only: spawn p95/max was +2.262/5.230 ms and walk p95/max was 2.512/28.893 ms; no subsystem marker names the walk +maximum. The nine-tile ring and full LOD0 are faster than S6. Tile output hashes did not change, so +the recipe version remains unchanged. Null-RHI runs do not prove player-visible enclosure. See +[`Standalone/Research/WorldAssemblerM3/Round11/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round11/REPORT.md) +for the audit counts, RMC review, quality matrix, commands and binary hashes. + ## 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 55346fc..abc25f2 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -370,7 +370,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error** | `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. | | `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread. Uses `PreEditDensityCache` when the tile is resident in the bounded local cache, replays the current DiffLayer over that exact procedural lattice, then meshes/applies inline so a carve is visible THIS frame. A cache miss uses normal generation; it captures the unedited lattice if the tile needs meshing, while uniform-tile proofs remain enabled. | | `PreEditDensityCache` / `VoxelForgePreEditDensityCache::BuildEditedGrid` | — | Per-world cache of the pawn's current L0 tile: exact 35³ pre-edit samples, max **171,500 bytes**. It is pruned when the pawn crosses chunk boundaries and cleared on `RegenerateAllChunks`. `BuildEditedGrid` snapshots touched diff chunks once and reproduces `GetDensityAt`'s final `pre-edit - EvaluateMods` operation before the per-call shared-grid mesher path. | -| `UpdateChunksAroundPosition` | 8603 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission is opt-in (`voxel.StreamingCoarseFirst=1`): it submits remaining coarse levels from max LOD down through `max(2, FullResClipLevels)`, then LOD0/1 refinement; ties favor travel direction, then distance. Default `0` preserves S4's LOD0-to-LOD8 order. `voxel.StreamingDynamicNearReserve=1` opts into holding the authored near-worker reserve while moving or until the stationary walkable ring is ready; default `0`. The clipmap stays non-overlapping and existing load-before-unload / `TransitionHold` reconciliation remains authoritative; S5 adds no overlapping placeholder layer. Standalone defaults to legacy; use `--coarse-first-stream-order` to opt in. §8.10 + §8.26. | +| `UpdateChunksAroundPosition` / `GetGenerationAdmissionCount` | 10990 / 10971 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission stays opt-in (`voxel.StreamingCoarseFirst=1`). `voxel.StreamingDynamicNearReserve=1` is now the default: it keeps the authored near reserve while moving or until the stationary walkable ring is ready, then makes those slots available to coarse work. Admission counts submitted queued/running tasks, not completed results waiting in the MPSC result queues; `voxel.StreamingLegacyPendingResultCap=1` restores the old `PendingTiles`-based cap for the negative control. `PendingTiles` still lasts through game-thread apply for duplicate suppression and result bookkeeping. The clipmap stays non-overlapping and load-before-unload / `TransitionHold` reconciliation remains authoritative. Standalone has no mirror of this actor scheduler. §8.10 + §8.26–8.28. | | `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` / `MeasureHeadlessCollisionGateStressTerrain` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The normal spawn-height cvar offsets from the measured density surface; `voxel.TestCollisionGateStressDeepDrop=1` carves a process-local box shaft from above the top to just above the floor tile, extending along the post-landing route so the pawn does not run into the shaft sidewall, temporarily raises only the transient test DiffLayer brush-radius allowance for that fixture, removes only the local pawn's redundant streaming anchor for this isolated repro, proves every sample in two complete outside-ring level-0 tiles is air before seeding their known-empty status, prewarms the known-empty in-ring column, and asserts that the support walk reaches the in-layout floor outside the aligned level-0 render ring before the pawn drops. The log reports whether the fixed gate retains that otherwise out-of-ring target. Other streaming anchors remain active. The fixture rejects a target or floor outside the layout, a non-air empty-tile proof, or a floor inside the aligned level-0 ring. The `GetDensityAt` column at capsule feet defines the local air/solid surface; the deep-drop column crossing is cached after its first exact scan. Each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; deep-drop gate-on also requires horizontal travel during a hold. Result logs include the terminal density column, first crossing below spawn height, hold count/time, and gated horizontal progress. | | `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells), then adds streaming anchors and the pending player's collision-only support column before the leaver sweep. It orders from the true local pawn position and builds `CriticalDesiredTiles` for the unresolved support target, occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants, including face/edge-touching replacements) for the reconcile pass. | | `CollisionGateDesiredTiles` / `RequestCollisionGateSupportTile` | 5062 | Retains an unresolved support target and up to three level-0 tiles below it across desired-set rebuilds. The additional keys are hidden collision-only tiles, queued ahead of ordinary streaming, and pruned once resolved or outside the active column. | @@ -996,19 +996,25 @@ exact-density-root LOD7/8 agreement audit with its datum-offset negative control ARCHITECTURE §8.25 and [`Standalone/Research/WorldAssemblerM3/Round8/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round8/REPORT.md). -### 3.14 Cold streaming apply trace and gated coarse order — World Assembler S6 / M3 Round 10 +### 3.14 Cold streaming admission and apply trace — World Assembler S7 / M3 Round 11 -`AVoxelWorld::ProcessPendingChunks` records per-frame result draining, visible-mesh applies, the -authored/startup budgets, per-LOD apply counts and worker occupancy through -`VoxelStartupTrace::RecordApplyFrame`. Direct `tile` events separate request-to-worker queue, -generation work, worker-result queue and game-thread apply time; `collision_ready` records the -RealtimeMesh body-update latency. `AVoxelWorld::UpdateChunksAroundPosition` samples resident counts -every 250 ms (for observation only) and admits coarse-first work only after the required nine-tile -walkable ring is ready. The order and startup apply boost are opt-in CVars; the boost is default-off. -`VoxelForgeExploreCommandlet.cpp` drives the staged owner-plan trace and moving actor check. The -standalone viewer's 60 m/s crossing counter now accepts multi-chunk forward deltas. See -ARCHITECTURE §8.27 and -[`Standalone/Research/WorldAssemblerM3/Round10/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round10/REPORT.md). +`AVoxelWorld::UpdateChunksAroundPosition` writes a `scheduler_admission` record on each submission +pass, plus `scheduler_skip` for core-readiness bypasses and all-loaded latches that still have missing +work. The admission record includes desired/missing/pending keys by LOD, task caps, submitted attempts, +stop level, decline reason, `bCoreRegionCompilePending`, pending-result estimate, and cumulative +worker-task time. Rule codes are 1=full task cap, 2=near reserve, 3=all-loaded latch, 4=inner +`LoadTile`/`LoadCoreTile` rejection, and 5=desired work still missing with apparent capacity. +This is the trace used to separate the +old near-reserve stop from completed-but-unapplied results counted as active. The default admission +count uses queued/running `ActiveTaskCount`; `voxel.StreamingLegacyPendingResultCap=1` restores the +old pending-result cap. The negative-control harness sets that together with +`voxel.StreamingDynamicNearReserve=0`. `ProcessPendingChunks` still records result draining, +visible-mesh applies, per-LOD counts and budget use; direct `tile` events separate worker queue, +task time, result queue and game-thread apply. `collision_ready` records RealtimeMesh submit-to-body +update latency. The per-frame `running_workers` sample aliases short task bursts, so use the +cumulative task intervals for worker-seconds. `VoxelForgeExploreCommandlet.cpp` drives the staged +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). ## 4. The density pipeline (most-edited hot path) diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index b8c3b45..9994945 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -185,11 +185,17 @@ namespace GVoxelForgeStreamingCoarseFirst, TEXT("Prioritize current walkable tiles, then far coarse clipmap levels, then near refinement; 0 restores legacy LOD order.")); - int32 GVoxelForgeStreamingDynamicNearReserve = 0; + int32 GVoxelForgeStreamingDynamicNearReserve = 1; FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve( TEXT("voxel.StreamingDynamicNearReserve"), GVoxelForgeStreamingDynamicNearReserve, - TEXT("Keep the authored near-worker reserve while the pawn moves or its walkable ring is incomplete; release it when stationary and ready.")); + TEXT("Keep the authored near-worker reserve while moving or before the walkable ring is ready; default 1 releases idle slots after that gate, 0 restores the legacy fixed reserve.")); + + int32 GVoxelForgeStreamingLegacyPendingResultCap = 0; + FAutoConsoleVariableRef CVarVoxelForgeStreamingLegacyPendingResultCap( + TEXT("voxel.StreamingLegacyPendingResultCap"), + 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 GVoxelForgeProfileTileGeneration = 0; FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration( @@ -1944,6 +1950,9 @@ void AVoxelWorld::BeginPlay() PeakObservedPawnSpeedCmPerSecond = 0.0f; TotalWorkerGenerationCycles.store(0, std::memory_order_relaxed); TotalWorkerGenerationTasks.store(0, std::memory_order_relaxed); + TotalGenerationWorkerTaskCycles.store(0, std::memory_order_relaxed); + LastStreamingSchedulerSampleSeconds = 0.0; + LastStreamingSchedulerWorkerCycles = 0; TotalObsoleteWorkerCycles.store(0, std::memory_order_relaxed); TotalObsoleteWorkerTasks.store(0, std::memory_order_relaxed); TotalValidationDensityCalls.store(0, std::memory_order_relaxed); @@ -2060,6 +2069,8 @@ void AVoxelWorld::BeginPlay() GVoxelForgeStreamingCoarseFirst); FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingDynamicNearReserve="), GVoxelForgeStreamingDynamicNearReserve); + FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingLegacyPendingResultCap="), + GVoxelForgeStreamingLegacyPendingResultCap); if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="), CommandLineStep1PrequeueVisibleLOD0)) { @@ -10954,6 +10965,15 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const int32 AVoxelWorld::GetActiveGenerationTaskCount() const { + return FMath::Max(0, ActiveTaskCount.load(std::memory_order_relaxed)); +} + +int32 AVoxelWorld::GetGenerationAdmissionCount() const +{ + if (GVoxelForgeStreamingLegacyPendingResultCap == 0) + { + return GetActiveGenerationTaskCount(); + } const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 ? Step1HeldApplyTiles.Num() : 0; return FMath::Max(0, PendingTiles.Num() - HeldCompletedResults); @@ -10976,13 +10996,56 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw const bool bTraceStreamingScheduler = VoxelForgeStartupTrace::IsActive(); const double StreamingUpdateStartSeconds = bTraceStreamingScheduler ? FPlatformTime::Seconds() : 0.0; + const int32 PendingTilesAtUpdateEntry = PendingTiles.Num(); double DesiredRebuildMilliseconds = 0.0; + const auto RecordSchedulerSkip = [&](const TCHAR* Reason) + { + if (!bTraceStreamingScheduler) return; + const double Now = FPlatformTime::Seconds(); + int32 DesiredMissing = 0; + for (const FVoxelTileKey& Tile : DesiredSorted) + { + if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile)) + { + ++DesiredMissing; + } + } + const int32 ActiveTasks = ActiveTaskCount.load(std::memory_order_relaxed); + const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 + ? Step1HeldApplyTiles.Num() : 0; + const int32 PendingResultsEstimate = FMath::Max( + 0, PendingTiles.Num() - ActiveTasks - HeldCompletedResults); + const int32 MaxTasks = GetMaxConcurrentTasks(); + const int32 AdmissionOccupancy = GetGenerationAdmissionCount(); + const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed); + const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles + ? TotalWorkerCycles - LastStreamingSchedulerWorkerCycles : 0; + const double IntervalStart = LastStreamingSchedulerSampleSeconds > 0.0 + ? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds; + const double IntervalSeconds = FMath::Max(0.0, Now - IntervalStart); + const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta); + VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_skip"), FString::Printf( + TEXT("\"frame\":%llu,\"reason\":\"%s\",\"desired_count\":%d," + "\"desired_missing\":%d,\"pending_tiles\":%d,\"admission_occupancy\":%d," + "\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%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, + ActiveGenerationWorkerCount.load(std::memory_order_relaxed), + PendingResultsEstimate, MaxTasks, GVoxelForgeStreamingLegacyPendingResultCap, + *FString::SanitizeFloat(BusySeconds), + *FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0))); + LastStreamingSchedulerSampleSeconds = Now; + LastStreamingSchedulerWorkerCycles = TotalWorkerCycles; + }; if (bUseVoxelForgeCore) { const TSharedPtr State = VoxelForgeCoreState; if (!State.IsValid() || State->bInitialFailed.load(std::memory_order_acquire) || !State->bReady.load(std::memory_order_acquire)) { + RecordSchedulerSkip(TEXT("core_not_ready")); return; // Core mode never falls through to the legacy generator. } if (State->bRebuildDesired.exchange(false, std::memory_order_acq_rel)) @@ -11004,6 +11067,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw else { StartPendingCoreRegionCompile(); + RecordSchedulerSkip(TEXT("core_region_compile_pending")); return; } } @@ -11058,6 +11122,19 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw : ((bPawnMoving || !bWalkableRingReadyForReserve) ? AuthoredNearTaskReserve : 0); const int32 NearTaskReserve = EffectiveNearTaskReserve; const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve); + const int32 FirstCoarseLevel = FMath::Max( + 2, Settings ? Settings->FullResClipLevels : 2); + const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0 + && bWalkableRingReadyForReserve; + int32 SchedulerSubmittedByLevel[9] = {}; + int32 SchedulerLoadAttemptsByLevel[9] = {}; + int32 SchedulerLevelCapReturns[9] = {}; + int32 SchedulerLevelCapBreaks[9] = {}; + int32 SchedulerCriticalFullCapBreaks = 0; + int32 SchedulerDirtyFullCapBreaks = 0; + int32 SchedulerBandCoarseCapBreaks = 0; + int32 SchedulerOrderStopLevel = -1; + int32 SchedulerOrderStopCap = 0; if (GVoxelForgeProfileTileGeneration != 0 && !GVoxelForgeStreamingBudgetReported) { @@ -11070,7 +11147,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d " "authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d " "step1_generation_cap=%d step1_max_mesh_applies=%d " - "near_task_reserve=%d dynamic_reserve=%d moving=%d walkable_ring_ready=%d " + "near_task_reserve=%d dynamic_reserve=%d legacy_result_cap=%d " + "moving=%d walkable_ring_ready=%d " "coarse_task_cap=%d coarse_first=%d " "max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"), LogicalCores, @@ -11082,6 +11160,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw GVoxelForgeStep1MaxMeshApplies, NearTaskReserve, GVoxelForgeStreamingDynamicNearReserve, + GVoxelForgeStreamingLegacyPendingResultCap, bPawnMoving ? 1 : 0, bWalkableRingReadyForReserve ? 1 : 0, CoarseTaskCap, @@ -11126,7 +11205,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // Rebuild the desired tile set when the player crosses a level-0 tile boundary — or when a // streaming anchor moved / (un)registered (bAnchorsMoved / bForceDesiredRebuild). //========================================================================= - if (CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild) + const bool bDesiredRebuiltThisUpdate = + CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild; + if (bDesiredRebuiltThisUpdate) { LastUpdateCenter = CenterChunk; bAllChunksLoaded = false; @@ -11375,10 +11456,26 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // Coarse work is capped below the full task budget so later near requests keep admission. // Le travail grossier reste sous le budget total pour reserver des admissions aux tuiles proches. //========================================================================= - if (!bAllChunksLoaded) + const bool bSubmissionLoopRan = !bAllChunksLoaded; + if (bSubmissionLoopRan) { TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles); int32 Submitted = 0; + const auto SubmitTileWithTraceCount = [&](const FVoxelTileKey& Tile, + EVoxelTileWorkPriority Priority) + { + if (Tile.Level >= 0 && Tile.Level < 9) + { + ++SchedulerLoadAttemptsByLevel[Tile.Level]; + } + const uint64 OrdinalBefore = NextStreamingRequestOrdinal; + LoadTile(Tile, Priority); + if (NextStreamingRequestOrdinal != OrdinalBefore + && Tile.Level >= 0 && Tile.Level < 9) + { + ++SchedulerSubmittedByLevel[Tile.Level]; + } + }; // FLOOR RESPONSIVENESS — the support tile and collision-required 3x3 XY ring lead ordinary work. // PRIORITE AU SOL — la tuile de support et l'anneau XY 3x3 requis pour la collision passent avant le reste. @@ -11386,9 +11483,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // Ils passent en BackgroundHigh afin que le sol du pion obtienne les premiers workers disponibles. for (const FVoxelTileKey& T : CriticalDesiredTiles) { - if (GetActiveGenerationTaskCount() >= MaxTasks) break; + if (GetGenerationAdmissionCount() >= MaxTasks) + { + ++SchedulerCriticalFullCapBreaks; + break; + } if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue; - LoadTile(T, EVoxelTileWorkPriority::CollisionCritical); + SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::CollisionCritical); ++Submitted; } @@ -11396,12 +11497,16 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // still in flight is KEPT queued (retried next frame) so its stale pre-carve result is corrected. for (auto It = DirtyRemeshQueue.CreateIterator(); It; ++It) { - if (GetActiveGenerationTaskCount() >= MaxTasks) break; + if (GetGenerationAdmissionCount() >= MaxTasks) + { + ++SchedulerDirtyFullCapBreaks; + break; + } const FVoxelTileKey T = *It; if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands It.RemoveCurrent(); - LoadTile(T, EVoxelTileWorkPriority::Edited); + SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Edited); ++Submitted; } @@ -11416,8 +11521,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw auto SubmitLevel = [&](int32 Level, EVoxelTileWorkPriority Priority, int32 AdmissionCap, bool bPreferTravelDirection) { - if (GetActiveGenerationTaskCount() >= AdmissionCap) + if (GetGenerationAdmissionCount() >= AdmissionCap) { + if (Level >= 0 && Level < 9) { ++SchedulerLevelCapReturns[Level]; } return; } TArray LevelTiles; @@ -11467,10 +11573,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw } for (const FVoxelTileKey& T : LevelTiles) { - if (GetActiveGenerationTaskCount() >= AdmissionCap) break; + if (GetGenerationAdmissionCount() >= AdmissionCap) + { + if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; } + break; + } if (PendingTiles.Contains(T)) continue; if (LoadedTiles.Contains(T)) continue; - LoadTile(T, Priority); + SubmitTileWithTraceCount(T, Priority); ++Submitted; } }; @@ -11483,12 +11593,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // Keep the walkable ring on the legacy near-first path; only spend its reserved task slots // on coarse work after all nine collision gates are ready. // Garder l'anneau praticable en priorite ; les taches grossieres commencent apres ses neuf collisions. - const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0 - && bWalkableRingReadyForReserve; if (bUseCoarseFirstOrder) { - const int32 FirstCoarseLevel = FMath::Max( - 2, Settings ? Settings->FullResClipLevels : 2); for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level) { SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, @@ -11508,7 +11614,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw { SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, CoarseTaskCap, false); - if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break; + if (GetGenerationAdmissionCount() >= CoarseTaskCap) + { + if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; } + SchedulerOrderStopLevel = Level; + SchedulerOrderStopCap = CoarseTaskCap; + break; + } } } if (bMeasureReadyTransitionVisibility) @@ -11523,11 +11635,15 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw // mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks). for (auto It = BandRemeshQueue.CreateIterator(); It; ++It) { - if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break; + if (GetGenerationAdmissionCount() >= CoarseTaskCap) + { + ++SchedulerBandCoarseCapBreaks; + break; + } const FVoxelTileKey T = *It; It.RemoveCurrent(); if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue; - LoadTile(T); + SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Horizon); ++Submitted; } @@ -11540,7 +11656,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted); } if (GVoxelForgeProfileTileGeneration != 0 - && GetActiveGenerationTaskCount() >= CoarseTaskCap + && GetGenerationAdmissionCount() >= CoarseTaskCap && !GVoxelForgeGenerationCapHitReported) { UE_LOG(LogTemp, Display, @@ -11604,8 +11720,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d]," "\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d," "\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"pending_by_lod\":%s," - "\"active_generation\":%d,\"task_cap\":%d,\"near_reserve\":%d," - "\"coarse_first\":%d,\"dynamic_near_reserve\":%d,\"epoch\":%u," + "\"active_generation\":%d,\"admission_occupancy\":%d," + "\"task_cap\":%d,\"near_reserve\":%d," + "\"coarse_first\":%d,\"dynamic_near_reserve\":%d," + "\"legacy_pending_result_cap\":%d,\"epoch\":%u," "\"update_chunks_ms\":%s,\"desired_rebuild_ms\":%s"), CenterChunk.X, CenterChunk.Y, CenterChunk.Z, *FString::SanitizeFloat(LocalPawn.X), *FString::SanitizeFloat(LocalPawn.Y), @@ -11613,13 +11731,177 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw *FString::SanitizeFloat(PlayerHeading.Size2D()), bWalkableRingReadyForReserve ? 1 : 0, *DesiredCounts, *ResidentCounts, *PendingCounts, - GetActiveGenerationTaskCount(), MaxTasks, NearTaskReserve, + GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve, GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, + GVoxelForgeStreamingLegacyPendingResultCap, GenerationEpoch, *FString::SanitizeFloat(StreamingUpdateMilliseconds), *FString::SanitizeFloat(DesiredRebuildMilliseconds))); LastStreamingTraceSampleSeconds = TraceNow; } + + // Mesurer chaque decision d'admission avec l'etat du meme tick, y compris les resultats en attente. + // Capture each admission decision from this tick, including completed results still awaiting apply. + int32 DesiredMissingByLevel[9] = {}; + int32 LoadedByLevel[9] = {}; + int32 PendingByLevel[9] = {}; + int32 AdmissionCapByLevel[9] = {}; + int32 DeclinedRuleByLevel[9] = {}; + for (const FVoxelTileKey& Tile : DesiredSorted) + { + if (Tile.Level < 0 || Tile.Level > 8) continue; + if (PendingTiles.Contains(Tile)) + { + ++PendingByLevel[Tile.Level]; + } + else if (LoadedTiles.Contains(Tile)) + { + ++LoadedByLevel[Tile.Level]; + } + else + { + ++DesiredMissingByLevel[Tile.Level]; + } + } + + const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0 + ? Step1HeldApplyTiles.Num() : 0; + const int32 AdmissionOccupancy = GetGenerationAdmissionCount(); + const int32 ActiveTaskGuard = ActiveTaskCount.load(std::memory_order_relaxed); + const int32 PendingResultsEstimate = FMath::Max( + 0, PendingTiles.Num() - ActiveTaskGuard - HeldCompletedResults); + int32 FullCapDeclinedTiles = 0; + int32 ReserveDeclinedTiles = 0; + int32 InnerLoadDeclinedTiles = 0; + for (int32 Level = 0; Level < 9; ++Level) + { + AdmissionCapByLevel[Level] = bUseCoarseFirstOrder + ? (Level >= FirstCoarseLevel ? CoarseTaskCap : MaxTasks) + : (Level == 0 ? MaxTasks : CoarseTaskCap); + if (DesiredMissingByLevel[Level] <= 0) continue; + + if (bAllChunksLoaded) + { + DeclinedRuleByLevel[Level] = 3; // Loaded latch while unadmitted desired keys remain. + // Verrou charge alors que des cles desirees restent non admises. + } + else if (SchedulerLevelCapReturns[Level] > 0 + || SchedulerLevelCapBreaks[Level] > 0 + || (SchedulerOrderStopLevel >= 0 && Level >= SchedulerOrderStopLevel)) + { + const int32 DeclinedAtCap = AdmissionCapByLevel[Level] > 0 + ? AdmissionCapByLevel[Level] : SchedulerOrderStopCap; + if (DeclinedAtCap < MaxTasks) + { + DeclinedRuleByLevel[Level] = 2; // Near reserve reached during this submit pass. + // Reserve proche atteinte pendant cette passe d'admission. + ReserveDeclinedTiles += DesiredMissingByLevel[Level]; + } + else + { + DeclinedRuleByLevel[Level] = 1; // Full task cap reached during this submit pass. + // Capacite totale des taches atteinte pendant cette passe. + FullCapDeclinedTiles += DesiredMissingByLevel[Level]; + } + } + else if (AdmissionOccupancy >= MaxTasks) + { + DeclinedRuleByLevel[Level] = 1; // Full task admission cap. + // Capacite totale d'admission des taches atteinte. + FullCapDeclinedTiles += DesiredMissingByLevel[Level]; + } + else if (AdmissionOccupancy >= AdmissionCapByLevel[Level] + && AdmissionCapByLevel[Level] < MaxTasks) + { + DeclinedRuleByLevel[Level] = 2; // Authored near reserve held back this LOD. + // La reserve proche configuree retient ce niveau de detail. + ReserveDeclinedTiles += DesiredMissingByLevel[Level]; + } + else if (SchedulerLoadAttemptsByLevel[Level] > SchedulerSubmittedByLevel[Level]) + { + DeclinedRuleByLevel[Level] = 4; // LoadTile/LoadCoreTile declined despite a scheduler attempt. + // LoadTile/LoadCoreTile a refuse la tuile apres la tentative du scheduler. + InnerLoadDeclinedTiles += DesiredMissingByLevel[Level]; + } + else + { + DeclinedRuleByLevel[Level] = 5; // Candidate remains with apparent admission capacity. + // La tuile desiree reste manquante avec une capacite d'admission apparente. + } + } + + const auto IntArrayJson = [](const int32 Values[9]) + { + FString Json = TEXT("["); + for (int32 Index = 0; Index < 9; ++Index) + { + if (Index > 0) Json += TEXT(","); + Json += FString::FromInt(Values[Index]); + } + Json += TEXT("]"); + return Json; + }; + const double SchedulerTraceNow = FPlatformTime::Seconds(); + const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed); + const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles + ? TotalWorkerCycles - LastStreamingSchedulerWorkerCycles : 0; + const double IntervalStart = LastStreamingSchedulerSampleSeconds > 0.0 + ? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds; + const double IntervalSeconds = FMath::Max(0.0, SchedulerTraceNow - IntervalStart); + const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta); + VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_admission"), FString::Printf( + TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"desired_rebuilt\":%d," + "\"submission_loop_ran\":%d,\"all_chunks_loaded\":%d," + "\"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," + "\"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," + "\"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," + "\"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, + NearTaskReserve, CoarseTaskCap, FirstCoarseLevel, + SchedulerOrderStopLevel, SchedulerOrderStopCap, + PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard, + ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate, + *FString::SanitizeFloat(BusySeconds), + *FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0), + *IntArrayJson(DesiredMissingByLevel), *IntArrayJson(LoadedByLevel), + *IntArrayJson(PendingByLevel), *IntArrayJson(SchedulerSubmittedByLevel), + *IntArrayJson(SchedulerLoadAttemptsByLevel), *IntArrayJson(AdmissionCapByLevel), + *IntArrayJson(DeclinedRuleByLevel), *IntArrayJson(SchedulerLevelCapReturns), + *IntArrayJson(SchedulerLevelCapBreaks), FullCapDeclinedTiles, ReserveDeclinedTiles, + InnerLoadDeclinedTiles, SchedulerCriticalFullCapBreaks, + SchedulerDirtyFullCapBreaks, SchedulerBandCoarseCapBreaks)); + LastStreamingSchedulerSampleSeconds = SchedulerTraceNow; + LastStreamingSchedulerWorkerCycles = TotalWorkerCycles; + } + else if (bTraceStreamingScheduler) + { + // Report a loaded latch only if desired work is still unadmitted; the normal no-missing path stays quiet. + // Signaler le verrou charge seulement s'il reste du travail desire non admis. + for (const FVoxelTileKey& Tile : DesiredSorted) + { + if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile)) + { + RecordSchedulerSkip(TEXT("all_chunks_loaded_latch")); + break; + } + } } } @@ -11641,7 +11923,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve); const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon ? CoarseTaskCap : MaxTasks; - if (GetActiveGenerationTaskCount() >= AdmissionCap) + if (GetGenerationAdmissionCount() >= AdmissionCap) { if (GVoxelForgeProfileTileGeneration != 0 && !GVoxelForgeGenerationCapHitReported) @@ -11812,8 +12094,18 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor struct FWorkerGuard { std::atomic& Counter; - ~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); } - } WorkerGuard{ActiveGenerationWorkerCount}; + std::atomic& BusyCycles; + uint64 StartedCycles; + ~FWorkerGuard() + { + const uint64 EndedCycles = FPlatformTime::Cycles64(); + if (EndedCycles >= StartedCycles) + { + BusyCycles.fetch_add(EndedCycles - StartedCycles, std::memory_order_relaxed); + } + Counter.fetch_sub(1, std::memory_order_relaxed); + } + } WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles}; FChunkResult Result; Result.WorkerTaskStartCycles = WorkerTaskStartCycles; @@ -11942,7 +12234,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve); const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon ? CoarseTaskCap : MaxTasks; - if (GetActiveGenerationTaskCount() >= AdmissionCap) return; + if (GetGenerationAdmissionCount() >= AdmissionCap) return; PendingTiles.Add(Tile); const TSharedPtr Cancellation = @@ -11987,8 +12279,18 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority struct FWorkerGuard { std::atomic& Counter; - ~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); } - } WorkerGuard{ActiveGenerationWorkerCount}; + std::atomic& BusyCycles; + uint64 StartedCycles; + ~FWorkerGuard() + { + const uint64 EndedCycles = FPlatformTime::Cycles64(); + if (EndedCycles >= StartedCycles) + { + BusyCycles.fetch_add(EndedCycles - StartedCycles, std::memory_order_relaxed); + } + Counter.fetch_sub(1, std::memory_order_relaxed); + } + } WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles}; auto Enqueue = [this, WorkPriority](FChunkResult&& Result) { diff --git a/Source/VoxelForge/Public/VoxelWorld.h b/Source/VoxelForge/Public/VoxelWorld.h index ec0d8b6..163f9db 100644 --- a/Source/VoxelForge/Public/VoxelWorld.h +++ b/Source/VoxelForge/Public/VoxelWorld.h @@ -443,7 +443,12 @@ public: * capped to (logical cores − 2) so small CPUs don't thrash on a flat 16 (background * priority stops frame starvation, not the context-switch overhead). */ int32 GetMaxConcurrentTasks() const; + /** Submitted generation tasks still queued or running; completed MPSC results do not occupy admission. + * Taches de generation soumises, en file ou en cours ; les resultats MPSC termines ne bloquent pas l'admission. */ int32 GetActiveGenerationTaskCount() const; + /** Count queued/running work for admission; the legacy CVar can include finished results awaiting apply. + * Compter le travail en file/en cours ; le CVar de compatibilite peut inclure les resultats finis en attente. */ + int32 GetGenerationAdmissionCount() const; /** Reserve task slots for collision-critical/edit/LOD0 work; far work uses the remainder. */ int32 GetNearTaskReserve(int32 MaxTasks) const; @@ -1093,7 +1098,9 @@ public: TQueue EditedProcessQueue; TQueue NearProcessQueue; TQueue ProcessQueue; - TSet PendingTiles; // tiles with a gen task in flight + // 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; // Game-thread-owned map; workers retain the shared token and only read its atomic flag. TMap> PendingTileCancellation; @@ -1121,7 +1128,8 @@ public: // Active pendant les mutations de génération lancées par l'éditeur, sans signifier la destruction. std::atomic bGenerationPaused{false}; - // Number of async tasks currently running — EndPlay waits for this to reach 0 + // Submitted async tasks (including UE-queued work) — EndPlay waits for this to reach 0. + // Taches asynchrones soumises (y compris la file UE) — EndPlay attend que le compteur revienne a 0. std::atomic ActiveTaskCount{0}; // Active workers are counted inside the launched tile task, distinct from tasks queued by UE. // Les travailleurs actifs sont comptes dans la tache de tuile, distincts des taches en attente UE. @@ -1199,6 +1207,11 @@ public: int32 EffectiveNearTaskReserve = -1; uint64 NextStreamingRequestOrdinal = 0; std::atomic NextStreamingGenerationOrdinal{0}; + // Cumulative worker-task wall cycles avoid aliasing the instantaneous per-frame counter. + // Les cycles cumules des taches evitent l'aliasing du compteur instantane echantillonne par frame. + std::atomic TotalGenerationWorkerTaskCycles{0}; + double LastStreamingSchedulerSampleSeconds = 0.0; + uint64 LastStreamingSchedulerWorkerCycles = 0; struct FReadyTransitionVisibilitySample { diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md index 721de9b..0bc0ead 100644 --- a/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/LUNA_DIALOGUE.md @@ -1283,3 +1283,30 @@ S5. The new trace, regressions, run ledger, exact log-captured command lines, ha spots are in [WorldAssemblerM3/Round10/REPORT.md](../../WorldAssemblerM3/Round10/REPORT.md). — Luna + + +## World assembler S7 / M3 Round 11 — 2026-10-19 + +The BuildHost trace counted the old scheduler declines: after LOD0 completed, coarse work +hit the four-slot near reserve for 877 frames; 806 also had finished results waiting to +apply and counted against the old cap. The shared dynamic reserve and active-task count cut +the cold 2,578-key set from 17.527 s to 7.780 s. The walkable ring is 0.581 s and full +LOD0 is 1.439 s, both inside the requested limits. The full set still misses the roughly +4.2 s worker-seconds/10-slots estimate, and measured worker task-seconds rose about 10%, +so this is a throughput win with a tile-cost question left to profile. The moving trace also +shows a separate `bCoreRegionCompilePending` gate: 10 legacy and 11 dynamic submission passes +were rejected during region rebuilds despite free slots. + +W1/W8/W16 determinism, Core-off, M2 fixtures, SurfaceWorld/FloatingIslands, S3 ground +agreement and S4 cap coverage passed. Three Standalone captures are saved, but their window +was hidden/occluded and they do not sign off player-visible enclosure. I reviewed RMC's +update batching, async collision and render-proxy paths; no change was clearly worth the +measured apply cost this round. Full commands, hashes, RMC source references, quality +limits and the async audit are in [WorldAssemblerM3/Round11/REPORT.md](../../WorldAssemblerM3/Round11/REPORT.md). + +No new source file was added. The likely compile spots are `VoxelWorld.cpp` and +`VoxelWorld.h` in the VoxelForge runtime module; the harness controls changed in +`Tools/TestVoxelForgeCoreWorldHost.ps1`. 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/Round11/REPORT.md b/Standalone/Research/WorldAssemblerM3/Round11/REPORT.md new file mode 100644 index 0000000..703ae2f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Round11/REPORT.md @@ -0,0 +1,261 @@ +# World assembler S7 / M3 Round 11 — scheduler admission and RMC review + +**Result:** the cold full set fell from 17.595 s on the S7 legacy control to 7.762 s with +the shared scheduler change. The 0.581 s walkable ring and 1.437 s full LOD0 meet the +0.63 s and 2.25 s targets. The 7.76 s full set still misses the roughly 4.2 s +worker-seconds / 10-slots estimate. RMC section-update batching was reviewed but not +implemented: measured game-thread tile apply is only about 0.24 s for the whole set, and +the null-RHI run cannot measure render-proxy cost. + +## 1. What the trace says + +`UpdateChunksAroundPosition` runs each world tick. For the 2,578-key spawn, the per-frame +trace found the decline after that call, in admission accounting: + +| Admission evidence | Count / example | Meaning | +|---|---:|---| +| Legacy near reserve after no LOD0 keys remained, with LOD1+ still missing | 876 frames | The four reserved near slots stayed unused while coarse keys were eligible. | +| Those frames with a positive finished-result estimate | 806 / 877 | `PendingTiles` included completed results waiting for game-thread apply. | +| Representative legacy frame | t=3.292474 s: active=4, completed estimate=2, cap occupancy=6/10 | The old pending-result count hit the coarse cap although only four tasks were queued/running. | +| Dynamic admission passes before the initial full-set milestone | 409 | Ten ended with missing keys and a post-pass sample below the 10-task count. All ten recorded a submission stop at cap 10. | +| Declines in those ten dynamic passes | 0 reserve, 0 all-loaded, 0 walkable-gate failure, 0 inner `LoadTile` rejection | A worker could finish between the full-cap stop and the end-of-pass sample, leaving occupancy at 9/10; the stop fields show that the pass had hit cap 10. | + +The two old blockers were independent: the near reserve capped coarse admission at six, +and completed-but-unapplied results occupied the old `PendingTiles` cap. The scheduler now +counts `ActiveTaskCount` (queued and running tasks); `PendingTiles` remains through apply +for duplicate suppression and result bookkeeping. The default `voxel.StreamingDynamicNearReserve=1` +keeps the near reserve while moving or before the stationary walkable gate, then releases +it to coarse work. `voxel.StreamingLegacyPendingResultCap=1` and the harness legacy mode +restore both old behaviors for the negative control. + +Across the complete 45 s trace (including movement from t=30 s), the end-of-update records +contained both missing keys and observed capacity below 10 in 1,236 legacy and 280 dynamic +frames. Per-LOD reason codes classify the legacy set as reserve in 1,226 frames and inner +loader return in 10. The dynamic trace has reserve in 259, full-cap stop in 12 and inner +loader return in 11; codes can overlap when different LODs hit different rules in one +update. No free-capacity frame had an unclassified missing LOD. The four additional legacy +`scheduler_skip` records with missing keys were region-compile gates, described below. +In `declined_rule_by_lod`, codes are 1=full task cap, 2=near reserve, 3=all-loaded latch, +4=inner `LoadTile`/`LoadCoreTile` return, and 5=work still missing with apparent capacity. +Dynamic code-2 frames include the intentional near reserve held while moving; the trace +keeps this distinct from a worker-pool or result-accounting stall. + +Admission itself is no longer the measured source of idle slots on the dynamic spawn: each +pass submitted work or recorded a stop at the 10-task cap. The ten end-of-pass samples that +showed missing keys and occupancy 9/10 had already recorded that full-cap stop; a worker +finished before the trace snapshot. Cumulative task intervals show +5.63 busy-worker equivalents over first request to ready, with eight sustained overlapping +tile tasks; one zero-duration endpoint tie counts nine. This does not identify whether the +remaining gap comes from UE task-pool dispatch or work within a tile. Boundary samples of +`running_workers` alias short task bursts and are not a time average. + +The same 45 s actor run also includes a +Y move beginning at t=30 s. On that moving path, +`LoadCoreTile`'s `bCoreRegionCompilePending` early return (`VoxelWorld.cpp:12224`) is a +separate gate. The trace recorded four top-level region-compile skips in the legacy control +with 116–187 desired keys missing and only 0–1 active tasks. It also recorded 10 legacy and +11 dynamic admission passes with inner load declines; every one had +`core_region_compile_pending=1`, zero submissions, and 108–190 legacy or 92–144 dynamic tile +attempts. +This explains idle slots during those region rebuilds, but it occurs after the cold initial +set and is separate from the reserve/result-count fix. The initial 20 `core_not_ready` +skips had zero missing desired keys; no all-loaded latch skip had missing work. + +## 2. Matched BuildHost result + +Fresh sequential null-RHI processes used the staged owner `SurfaceWorld` plan, seed 0, +25 cm Core scale, 2,578 desired keys, 10 actor task slots, a manually ticked world at 16 ms, +and a +Y 600 cm/s move beginning at 30 s. Apply boost and coarse-first were off in both S7 +runs. “Idle slot-s” is `10 × (first request to full ready) − cumulative worker task time`; +busy equivalents use the same interval. S6's 3.27 average was sampled only on frames with +pending results and is not directly comparable to the integrated S7 value. + +| Run | Full set | Walkable 9/9 ring | Full LOD0 (512) | Worker task-s | Integrated busy equivalents | Idle slot-s | Tick spawn p95/max | Tick walk p95/max | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| S6 legacy reference | 17.412 s | 0.632 s | 2.252 s | 37.64 | 3.27 sampled on pending-result frames | 132.1 | 1.406 / 5.059 ms | 1.771 / 29.247 ms | +| S7 old-path negative control | 17.527 s | 0.650 s | 2.281 s | 37.696 | 2.208 | 133.012 | 1.437 / 5.286 ms | 1.957 / 29.244 ms | +| S7 dynamic reserve + active-task cap | **7.780 s** | **0.581 s** | **1.439 s** | 41.364 | **5.634** | **32.056** | 2.262 / 5.230 ms | 2.512 / 28.893 ms | + +The negative control reproduces S6 legacy within 0.7% in full-set time and 0.2% in worker +task-seconds. Dynamic admission cuts full-set time by 55.3%, reduces idle slots by about +76% against S6, and meets both near-work targets. It does not meet the roughly 4.14 s +`41.364 / 10` estimate. Measured worker task time rose about 9.7% versus the S7 negative +control (about 16.04 ms per initial tile versus 14.62 ms). Geometry hashes did not change +and the density/mesh recipe was untouched; the trace measures task elapsed time, not CPU +cycles, so it cannot separate contention from other elapsed-time variation. This is a +measured tile-cost increase to watch in a rendered/profiled run, not evidence of a content +quality change. + +The largest walk `UWorld::Tick` sample remains about 29 ms. The trace wraps aggregate +`TestWorld->Tick()` at `VoxelForgeExploreCommandlet.cpp:17495–17501`; it has no subsystem +marker at the maximum, so the owner cannot be named from this trace. Null RHI also means +these are not rendered frame times. + +## 3. RealtimeMeshComponent review + +VoxelForge creates plain `FRealtimeMeshStreamSet` data in `BuildTileStreamSet` +(`Source/VoxelForge/Private/VoxelWorld.cpp:616`, called from worker paths at 12441 and +12876). Component mutation stays on the game thread in `ApplyMeshToTile` (13064): each tile +removes and recreates one section group and sets its material slots (13273–13288). Ground +and cap are material/polygroup sections in that tile group; the world uses clipmap tiles, +not an RMC multi-LOD chain. The whole cold set's measured game-thread apply was about +0.24 s in S6 (roughly 3% of the S7 7.76 s wall time). + +Ranked opportunities, with this round's evidence: + +1. **Batch a tile's RMC writes.** RMC's `Public/Data/RealtimeMeshUpdateBuilder.h:390–406` + documents grouping mesh, LOD, section-group, section and collision updates behind one + write lock and proxy publish; `Private/Data/RealtimeMeshUpdateBuilder.cpp:222` commits, + with render work queued at 64–67. This could trim game-thread update overhead, but the + measured apply ceiling is only ~0.24 s for 2,578 tiles. It is not the cold-set floor. +2. **Measure fast collision cooking.** RMC `Public/Core/RealtimeMeshCollision.h:32–55` + exposes async cook, fast cook, complex-as-simple, normal flipping, deformable collision + and mesh merging. The current BuildHost trace reports async=1, complex-as-simple=1, + fast-cook=0, merge=0; async cooking is dispatched in + `Private/RealtimeMeshManaged.cpp:695–707`. Cook CPU is not isolated by the existing + submit-to-body-update measurement, and changing cook mode could affect collision + quality, so defer until collision timing and walkability are audited together. +3. **Batch across tiles / share mesh components.** This might reduce component and proxy + overhead, but RMC builder commits target a mesh and VoxelForge's per-tile collision, + unload and transition ownership would change. Larger risk than the measured apply cost. +4. **Use RMC's LOD/section-group structure more broadly.** RMC has explicit LOD and section + group update operations (builder header above); VoxelForge currently creates one group + per tile and changes clipmap tiles between LODs. A multi-LOD mesh would be an architectural + change, with no null-RHI timing evidence for a payoff. +5. **Render resource/proxy work.** `Private/RenderProxy/RealtimeMeshGPUBuffer.cpp:49–60` + creates resources asynchronously only when the RHI supports multithreaded resources; + otherwise it falls back to the render thread. RMC's proxy publish path is in the builder + implementation above. Null RHI cannot measure either path, so no change was made. + +No RMC files were modified. Its source was read only at +`E:\Projet Unreal\VoxelM\Plugins\RealtimeMeshComponent`. + +## 4. Quality and determinism + +- Standalone W1/W8/W16 produce identical full and LOD0–8 hashes: density + `c10697a8f48380c7`, mesh `430d2be2bdd8b988`, payload `b7b33e14092225c9`, topology + `c5c46d66606a819e`. The parser's first ad-hoc comparison was corrected; a direct per-LOD + comparison shows all nine levels equal. +- BuildHost SurfaceWorld at 25 cm passed the full actor/collision/anchor/carve check and + W1/W8/W16 matrix. Hashes: density `0cd2a1a4152ee4aa`, mesh `80a5b054b904b1a2`, payload + `bf2f1b40dff38acc`, topology `56074198682d5e18`. +- Core-off legacy at 12.5 cm passed. M2 V1 and V2 each passed five fixtures plus same-LOD + border checks. FloatingIslands at 25 cm passed W1/W8/W16, fixed water, edits, collision, + LOD0 and LOD1. +- S3 ground / S4 LOD7–8 agreement passed: maximum ground delta 0.0427 cm, cross-LOD delta + 0.0 cm, and the 100 cm datum-offset negative control was detected. +- S4 Standalone far-cap audit passed: 385/385 requested cap bands emitted; 128 agreement + points had 0.0504 cm maximum error against 2 cm tolerance; the 100 cm bad-datum control + was detected; 2,520/2,520 rays covered all 35 view points. +- Three Standalone spawn captures are under `Saved/BuildHost/S7/captures/`; each is + 4,097,178 bytes. Sampled terrain surfaces show no obvious opening. The capture window + reported hidden/occluded, so these back-buffer submissions are not an owner-visible + enclosure sign-off. The standalone does not mirror the shared Unreal actor scheduler. + [Frame 00](../../../../Saved/BuildHost/S7/captures/s7_spawn_view_00.png), + [frame 01](../../../../Saved/BuildHost/S7/captures/s7_spawn_view_01.png), and + [frame 02](../../../../Saved/BuildHost/S7/captures/s7_spawn_view_02.png). +- Scheduler changes did not alter tile output hashes, so the recipe/version is unchanged. + Water remains fixed for the season; player carve/fill remains unrestricted. + +## 5. Build, commands and run record + +BuildHost compilation passed with 12 actions and normal priority: + +```powershell +& '.\Tools\BuildVoxelForgeCoreHost.ps1' -MaxParallelActions 12 -TimeoutSeconds 900 +``` + +The first build attempt failed on duplicate `TraceNow` declarations (`C2374` / `C2086`); I +removed the duplicate and rebuilt successfully. The final build's async-safety checks passed. +The standalone full build passed: + +```powershell +& '.\Standalone\Build.ps1' -Target All -CompileWorkers 12 -BuildPriority Normal +``` + +It warned that `vswhere.exe` was not recognized and reported existing C4838 conversions in +`VoxelStrateManager.cpp:3974–3975`; it still completed successfully. + +BuildHost quality commands (all passed): + +```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' -FloatingIslandsOnly -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +``` + +The final extra trace fields only report Core readiness/region-compile state; they do not +change tile generation. After adding them I rebuilt and reran both matched actor modes. + +Matched BuildHost actor commands: + +```powershell +& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Legacy -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -TimeoutSeconds 900 +``` + +Standalone W1/W8/W16 command template (substitute `1`, `8`, `16`; each was a fresh process): + +```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 +``` + +Standalone S4 audit command: + +```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 +``` + +The first capture attempt added `--stream-benchmark` to the viewer capture command. It exited +0 but generated no PNG because the benchmark path exits before the viewer capture loop. The +corrected command was: + +```powershell +Standalone\bin\VoxelForgeCoreStandalone.exe --world-assembler-prototype --force-generate --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 --capture-spawn-sequence-prefix=E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\S7\captures\s7_spawn_view --quit-after-spawn-captures +``` + +The three capture outputs and standalone logs are under `Saved/BuildHost/S7/`. BuildHost +stdout and JSONL traces are under +`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/`; the +expanded Unreal command lines are preserved in those stdout logs: [legacy stdout](../../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface_stream_trace_only_applyboost_0_legacy_order_nearreserve_legacy_trace.stdout.log), +[legacy JSONL](../../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface_stream_trace_only_applyboost_0_legacy_order_nearreserve_legacy_trace.trace.jsonl), +[dynamic stdout](../../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface_stream_trace_only_applyboost_0_legacy_order_nearreserve_dynamic_trace.stdout.log), +and [dynamic JSONL](../../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface_stream_trace_only_applyboost_0_legacy_order_nearreserve_dynamic_trace.trace.jsonl). The legacy/dynamic pair +used the wrapper commands above, staged `DA_Settings`, `-nullrhi -unattended -nop4 +-nosplash -NoSound -corelimit=12 -run=VoxelForgeExplore -seed=0 -archetype=TunnelNetwork +-coreworldtest=1 -corevoxelsizecm=25 -settingsref=/Game/VoxelForge/DA_Settings.DA_Settings +-coreworldstreamtrace=1 -voxel.TestMove=1 -voxel.TestMoveSpeedCmPerSecond=600 +-voxel.TestMoveStartSeconds=30 -voxel.TestMoveDirection=0,1,0 +-voxel.StartupTraceUntilEndPlay=1 -voxel.CollisionGate=0`, and the recorded mode CVars. + +SHA-256 of the executables/modules used for these runs: + +| Binary | SHA-256 | +|---|---| +| UE 5.7 `UnrealEditor-Cmd.exe` | `ECCE9996191F7F7E69B3D104EECDD0E678AE69176FDCCD79828C3E75A0654D6D` | +| BuildHost `UnrealEditor-VoxelForge.dll` | `29CFC3AB0BDF0B6048029660160D38F8AC3E9FF59ED057BD06D4CA42C3FFBBD2` | +| BuildHost `UnrealEditor-VoxelForgeCore.dll` | `54259552340FE81EA05640C622E9E5E5C0AC68C95F87DC718326D25EF133B2AC` | +| BuildHost `UnrealEditor-VoxelForgeEditor.dll` | `5FF5FCF82EC59A61319CBF979EC9A7BA081971B4D35088082BBF7A81C4C4794D` | +| `Standalone/bin/VoxelForgeCoreStandalone.exe` | `0F1610730D9BC038E9E60B5ECBF84BA8AD562748E2737F7C1C288F8CE0FE0CED` | + +All runs were sequential at normal priority, with at most 12 build/Standalone workers; +BuildHost tests used the harness's 12-core limit. Free memory was at least 17.4 GB before +each actor launch; the largest BuildHost quality process stayed below 3.58 GB (the final A/B +actors stayed below 3.53 GB; Standalone capture below 1.14 GB). No process exceeded 8 GB. +The WMI memory query was denied, so preflight used `GlobalMemoryStatusEx`. No new `UECC-*` +folder or `0xC0000142` occurred; the +only crash directory is the pre-existing 2026-09-17 `UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000`. + +Async diff audit: `GenerationEpoch` still rejects stale work; the result queues remain +`EQueueMode::Mpsc`; `EndPlay` sets shutdown, waits for tasks and drains queues; workers check +`bShuttingDown`; `ModsLock` is unchanged; UObject and RMC component mutation remains on the +game thread. No generated header, `Binaries/`, or `Intermediate/` source was edited. No new +source file was added. Modified compile spots are `Source/VoxelForge/Private/VoxelWorld.cpp` +(CVar setup, admission accounting, task lifetime accounting and trace) and +`Source/VoxelForge/Public/VoxelWorld.h` (scheduler/task bookkeeping comments and declarations); +the expected compile target is the VoxelForge runtime module for Win64 Development in the +BuildHost. `Tools/TestVoxelForgeCoreWorldHost.ps1` changed only the harness controls. + +No owner project/editor or asset was opened, built or edited. RealtimeMeshComponent was +read-only; no external source was changed. No commit, push or stash was made. diff --git a/Tools/TestVoxelForgeCoreWorldHost.ps1 b/Tools/TestVoxelForgeCoreWorldHost.ps1 index 3395110..9ca09de 100644 --- a/Tools/TestVoxelForgeCoreWorldHost.ps1 +++ b/Tools/TestVoxelForgeCoreWorldHost.ps1 @@ -3,6 +3,7 @@ param( [ValidateRange(60, 900)][int]$TimeoutSeconds = 900, [ValidateRange(12.5, 25.0)][double]$CoreVoxelSizeCm = 25.0, [ValidateSet('CoarseFirst', 'Legacy')][string]$StreamingOrder = 'Legacy', + [ValidateSet('Auto', 'Legacy', 'Dynamic')][string]$NearReserveMode = 'Auto', [switch]$EnforceCollisionGate, [switch]$StartupTrace, [ValidateRange(0, 1)][int]$StartupApplyBoost = 0, @@ -145,6 +146,11 @@ if ($SharedFeatureAuditOnly) { $RunName = 'shared_feature_audit' } $RunName += if ($StreamingOrder -eq 'Legacy') { '_legacy_order' } else { '_coarse_first' } +$RunName += switch ($NearReserveMode) { + 'Legacy' { '_nearreserve_legacy' } + 'Dynamic' { '_nearreserve_dynamic' } + default { '_nearreserve_auto' } +} if ($EnforceCollisionGate) { $RunName += '_collision_gate' } if ($StartupTrace -or $StreamingTraceOnly) { $RunName += '_trace' } @@ -262,9 +268,15 @@ $BaseArguments = @( ('-out="{0}"' -f $OutputDirectory) ) $CoarseFirstValue = if ($StreamingOrder -eq 'CoarseFirst') { 1 } else { 0 } -$DynamicReserveValue = if ($StreamingOrder -eq 'CoarseFirst') { 1 } else { 0 } +$DynamicReserveValue = switch ($NearReserveMode) { + 'Legacy' { 0 } + 'Dynamic' { 1 } + default { 1 } +} +$LegacyPendingResultCapValue = if ($NearReserveMode -eq 'Legacy') { 1 } else { 0 } $BaseArguments += "-voxel.StreamingCoarseFirst=$CoarseFirstValue" $BaseArguments += "-voxel.StreamingDynamicNearReserve=$DynamicReserveValue" +$BaseArguments += "-voxel.StreamingLegacyPendingResultCap=$LegacyPendingResultCapValue" $BaseArguments += "-voxel.StreamingStartupApplyBoost=$StartupApplyBoost" if ($StreamingTraceOnly) { $BaseArguments += '-coreworldstreamtrace=1'