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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
b9c13c37c0
commit
5eb9ee76ff
7 files changed
+726
-53
No files matched your search
+60
-8
@@ -578,7 +578,12 @@ driven by `EditorBrush*` props.
|
|||||||
the result through the normal MPSC queue; the game thread reads the token again before applying.
|
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
|
`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
|
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
|
- **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),
|
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
|
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
|
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
|
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.
|
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
|
`voxel.StreamingDynamicNearReserve=1` keeps the authored near-task reserve while the pawn moves or
|
||||||
moves or until the stationary walkable ring is ready; default `0` preserves the previous task budget.
|
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
|
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.
|
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
|
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.
|
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
|
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
|
about 0.24 seconds. S7's frame-level admission and cumulative worker-task accounting revisited that
|
||||||
the four-mesh cap, but expanding the apply budget alone did not move the wall clock. The remaining
|
open question; see §8.28.
|
||||||
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.
|
|
||||||
|
|
||||||
The report records per-LOD worker, queue, result-queue and apply totals, collision latency, frame
|
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
|
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
|
shutdown-drain and game-thread UObject-creation rules. See
|
||||||
[`Standalone/Research/WorldAssemblerM3/Round10/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round10/REPORT.md).
|
[`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)
|
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||||
|
|
||||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||||
|
|||||||
+19
-13
@@ -370,7 +370,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
|||||||
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` 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`. |
|
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` 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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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
|
ARCHITECTURE §8.25 and
|
||||||
[`Standalone/Research/WorldAssemblerM3/Round8/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round8/REPORT.md).
|
[`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
|
`AVoxelWorld::UpdateChunksAroundPosition` writes a `scheduler_admission` record on each submission
|
||||||
authored/startup budgets, per-LOD apply counts and worker occupancy through
|
pass, plus `scheduler_skip` for core-readiness bypasses and all-loaded latches that still have missing
|
||||||
`VoxelStartupTrace::RecordApplyFrame`. Direct `tile` events separate request-to-worker queue,
|
work. The admission record includes desired/missing/pending keys by LOD, task caps, submitted attempts,
|
||||||
generation work, worker-result queue and game-thread apply time; `collision_ready` records the
|
stop level, decline reason, `bCoreRegionCompilePending`, pending-result estimate, and cumulative
|
||||||
RealtimeMesh body-update latency. `AVoxelWorld::UpdateChunksAroundPosition` samples resident counts
|
worker-task time. Rule codes are 1=full task cap, 2=near reserve, 3=all-loaded latch, 4=inner
|
||||||
every 250 ms (for observation only) and admits coarse-first work only after the required nine-tile
|
`LoadTile`/`LoadCoreTile` rejection, and 5=desired work still missing with apparent capacity.
|
||||||
walkable ring is ready. The order and startup apply boost are opt-in CVars; the boost is default-off.
|
This is the trace used to separate the
|
||||||
`VoxelForgeExploreCommandlet.cpp` drives the staged owner-plan trace and moving actor check. The
|
old near-reserve stop from completed-but-unapplied results counted as active. The default admission
|
||||||
standalone viewer's 60 m/s crossing counter now accepts multi-chunk forward deltas. See
|
count uses queued/running `ActiveTaskCount`; `voxel.StreamingLegacyPendingResultCap=1` restores the
|
||||||
ARCHITECTURE §8.27 and
|
old pending-result cap. The negative-control harness sets that together with
|
||||||
[`Standalone/Research/WorldAssemblerM3/Round10/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round10/REPORT.md).
|
`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)
|
## 4. The density pipeline (most-edited hot path)
|
||||||
|
|
||||||
|
|||||||
@@ -185,11 +185,17 @@ namespace
|
|||||||
GVoxelForgeStreamingCoarseFirst,
|
GVoxelForgeStreamingCoarseFirst,
|
||||||
TEXT("Prioritize current walkable tiles, then far coarse clipmap levels, then near refinement; 0 restores legacy LOD order."));
|
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(
|
FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve(
|
||||||
TEXT("voxel.StreamingDynamicNearReserve"),
|
TEXT("voxel.StreamingDynamicNearReserve"),
|
||||||
GVoxelForgeStreamingDynamicNearReserve,
|
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;
|
int32 GVoxelForgeProfileTileGeneration = 0;
|
||||||
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
|
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
|
||||||
@@ -1944,6 +1950,9 @@ void AVoxelWorld::BeginPlay()
|
|||||||
PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
||||||
TotalWorkerGenerationCycles.store(0, std::memory_order_relaxed);
|
TotalWorkerGenerationCycles.store(0, std::memory_order_relaxed);
|
||||||
TotalWorkerGenerationTasks.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);
|
TotalObsoleteWorkerCycles.store(0, std::memory_order_relaxed);
|
||||||
TotalObsoleteWorkerTasks.store(0, std::memory_order_relaxed);
|
TotalObsoleteWorkerTasks.store(0, std::memory_order_relaxed);
|
||||||
TotalValidationDensityCalls.store(0, std::memory_order_relaxed);
|
TotalValidationDensityCalls.store(0, std::memory_order_relaxed);
|
||||||
@@ -2060,6 +2069,8 @@ void AVoxelWorld::BeginPlay()
|
|||||||
GVoxelForgeStreamingCoarseFirst);
|
GVoxelForgeStreamingCoarseFirst);
|
||||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingDynamicNearReserve="),
|
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingDynamicNearReserve="),
|
||||||
GVoxelForgeStreamingDynamicNearReserve);
|
GVoxelForgeStreamingDynamicNearReserve);
|
||||||
|
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingLegacyPendingResultCap="),
|
||||||
|
GVoxelForgeStreamingLegacyPendingResultCap);
|
||||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="),
|
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="),
|
||||||
CommandLineStep1PrequeueVisibleLOD0))
|
CommandLineStep1PrequeueVisibleLOD0))
|
||||||
{
|
{
|
||||||
@@ -10954,6 +10965,15 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
|||||||
|
|
||||||
int32 AVoxelWorld::GetActiveGenerationTaskCount() 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
|
const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0
|
||||||
? Step1HeldApplyTiles.Num() : 0;
|
? Step1HeldApplyTiles.Num() : 0;
|
||||||
return FMath::Max(0, PendingTiles.Num() - HeldCompletedResults);
|
return FMath::Max(0, PendingTiles.Num() - HeldCompletedResults);
|
||||||
@@ -10976,13 +10996,56 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
const bool bTraceStreamingScheduler = VoxelForgeStartupTrace::IsActive();
|
const bool bTraceStreamingScheduler = VoxelForgeStartupTrace::IsActive();
|
||||||
const double StreamingUpdateStartSeconds = bTraceStreamingScheduler
|
const double StreamingUpdateStartSeconds = bTraceStreamingScheduler
|
||||||
? FPlatformTime::Seconds() : 0.0;
|
? FPlatformTime::Seconds() : 0.0;
|
||||||
|
const int32 PendingTilesAtUpdateEntry = PendingTiles.Num();
|
||||||
double DesiredRebuildMilliseconds = 0.0;
|
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<unsigned long long>(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)
|
if (bUseVoxelForgeCore)
|
||||||
{
|
{
|
||||||
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
|
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
|
||||||
if (!State.IsValid() || State->bInitialFailed.load(std::memory_order_acquire)
|
if (!State.IsValid() || State->bInitialFailed.load(std::memory_order_acquire)
|
||||||
|| !State->bReady.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.
|
return; // Core mode never falls through to the legacy generator.
|
||||||
}
|
}
|
||||||
if (State->bRebuildDesired.exchange(false, std::memory_order_acq_rel))
|
if (State->bRebuildDesired.exchange(false, std::memory_order_acq_rel))
|
||||||
@@ -11004,6 +11067,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
StartPendingCoreRegionCompile();
|
StartPendingCoreRegionCompile();
|
||||||
|
RecordSchedulerSkip(TEXT("core_region_compile_pending"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11058,6 +11122,19 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
: ((bPawnMoving || !bWalkableRingReadyForReserve) ? AuthoredNearTaskReserve : 0);
|
: ((bPawnMoving || !bWalkableRingReadyForReserve) ? AuthoredNearTaskReserve : 0);
|
||||||
const int32 NearTaskReserve = EffectiveNearTaskReserve;
|
const int32 NearTaskReserve = EffectiveNearTaskReserve;
|
||||||
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve);
|
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
|
if (GVoxelForgeProfileTileGeneration != 0
|
||||||
&& !GVoxelForgeStreamingBudgetReported)
|
&& !GVoxelForgeStreamingBudgetReported)
|
||||||
{
|
{
|
||||||
@@ -11070,7 +11147,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
|
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
|
||||||
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
|
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
|
||||||
"step1_generation_cap=%d step1_max_mesh_applies=%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 "
|
"coarse_task_cap=%d coarse_first=%d "
|
||||||
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
|
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
|
||||||
LogicalCores,
|
LogicalCores,
|
||||||
@@ -11082,6 +11160,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
GVoxelForgeStep1MaxMeshApplies,
|
GVoxelForgeStep1MaxMeshApplies,
|
||||||
NearTaskReserve,
|
NearTaskReserve,
|
||||||
GVoxelForgeStreamingDynamicNearReserve,
|
GVoxelForgeStreamingDynamicNearReserve,
|
||||||
|
GVoxelForgeStreamingLegacyPendingResultCap,
|
||||||
bPawnMoving ? 1 : 0,
|
bPawnMoving ? 1 : 0,
|
||||||
bWalkableRingReadyForReserve ? 1 : 0,
|
bWalkableRingReadyForReserve ? 1 : 0,
|
||||||
CoarseTaskCap,
|
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
|
// Rebuild the desired tile set when the player crosses a level-0 tile boundary — or when a
|
||||||
// streaming anchor moved / (un)registered (bAnchorsMoved / bForceDesiredRebuild).
|
// streaming anchor moved / (un)registered (bAnchorsMoved / bForceDesiredRebuild).
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
if (CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild)
|
const bool bDesiredRebuiltThisUpdate =
|
||||||
|
CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild;
|
||||||
|
if (bDesiredRebuiltThisUpdate)
|
||||||
{
|
{
|
||||||
LastUpdateCenter = CenterChunk;
|
LastUpdateCenter = CenterChunk;
|
||||||
bAllChunksLoaded = false;
|
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.
|
// 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.
|
// 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);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles);
|
||||||
int32 Submitted = 0;
|
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.
|
// 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.
|
// 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.
|
// Ils passent en BackgroundHigh afin que le sol du pion obtienne les premiers workers disponibles.
|
||||||
for (const FVoxelTileKey& T : CriticalDesiredTiles)
|
for (const FVoxelTileKey& T : CriticalDesiredTiles)
|
||||||
{
|
{
|
||||||
if (GetActiveGenerationTaskCount() >= MaxTasks) break;
|
if (GetGenerationAdmissionCount() >= MaxTasks)
|
||||||
|
{
|
||||||
|
++SchedulerCriticalFullCapBreaks;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue;
|
if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue;
|
||||||
LoadTile(T, EVoxelTileWorkPriority::CollisionCritical);
|
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::CollisionCritical);
|
||||||
++Submitted;
|
++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.
|
// still in flight is KEPT queued (retried next frame) so its stale pre-carve result is corrected.
|
||||||
for (auto It = DirtyRemeshQueue.CreateIterator(); It; ++It)
|
for (auto It = DirtyRemeshQueue.CreateIterator(); It; ++It)
|
||||||
{
|
{
|
||||||
if (GetActiveGenerationTaskCount() >= MaxTasks) break;
|
if (GetGenerationAdmissionCount() >= MaxTasks)
|
||||||
|
{
|
||||||
|
++SchedulerDirtyFullCapBreaks;
|
||||||
|
break;
|
||||||
|
}
|
||||||
const FVoxelTileKey T = *It;
|
const FVoxelTileKey T = *It;
|
||||||
if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop
|
if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop
|
||||||
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
|
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
|
||||||
It.RemoveCurrent();
|
It.RemoveCurrent();
|
||||||
LoadTile(T, EVoxelTileWorkPriority::Edited);
|
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Edited);
|
||||||
++Submitted;
|
++Submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11416,8 +11521,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
auto SubmitLevel = [&](int32 Level, EVoxelTileWorkPriority Priority, int32 AdmissionCap,
|
auto SubmitLevel = [&](int32 Level, EVoxelTileWorkPriority Priority, int32 AdmissionCap,
|
||||||
bool bPreferTravelDirection)
|
bool bPreferTravelDirection)
|
||||||
{
|
{
|
||||||
if (GetActiveGenerationTaskCount() >= AdmissionCap)
|
if (GetGenerationAdmissionCount() >= AdmissionCap)
|
||||||
{
|
{
|
||||||
|
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapReturns[Level]; }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
TArray<FVoxelTileKey> LevelTiles;
|
TArray<FVoxelTileKey> LevelTiles;
|
||||||
@@ -11467,10 +11573,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
}
|
}
|
||||||
for (const FVoxelTileKey& T : LevelTiles)
|
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 (PendingTiles.Contains(T)) continue;
|
||||||
if (LoadedTiles.Contains(T)) continue;
|
if (LoadedTiles.Contains(T)) continue;
|
||||||
LoadTile(T, Priority);
|
SubmitTileWithTraceCount(T, Priority);
|
||||||
++Submitted;
|
++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
|
// 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.
|
// on coarse work after all nine collision gates are ready.
|
||||||
// Garder l'anneau praticable en priorite ; les taches grossieres commencent apres ses neuf collisions.
|
// Garder l'anneau praticable en priorite ; les taches grossieres commencent apres ses neuf collisions.
|
||||||
const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0
|
|
||||||
&& bWalkableRingReadyForReserve;
|
|
||||||
if (bUseCoarseFirstOrder)
|
if (bUseCoarseFirstOrder)
|
||||||
{
|
{
|
||||||
const int32 FirstCoarseLevel = FMath::Max(
|
|
||||||
2, Settings ? Settings->FullResClipLevels : 2);
|
|
||||||
for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level)
|
for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level)
|
||||||
{
|
{
|
||||||
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
|
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
|
||||||
@@ -11508,7 +11614,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
{
|
{
|
||||||
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
|
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
|
||||||
CoarseTaskCap, false);
|
CoarseTaskCap, false);
|
||||||
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break;
|
if (GetGenerationAdmissionCount() >= CoarseTaskCap)
|
||||||
|
{
|
||||||
|
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; }
|
||||||
|
SchedulerOrderStopLevel = Level;
|
||||||
|
SchedulerOrderStopCap = CoarseTaskCap;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bMeasureReadyTransitionVisibility)
|
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).
|
// mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks).
|
||||||
for (auto It = BandRemeshQueue.CreateIterator(); It; ++It)
|
for (auto It = BandRemeshQueue.CreateIterator(); It; ++It)
|
||||||
{
|
{
|
||||||
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break;
|
if (GetGenerationAdmissionCount() >= CoarseTaskCap)
|
||||||
|
{
|
||||||
|
++SchedulerBandCoarseCapBreaks;
|
||||||
|
break;
|
||||||
|
}
|
||||||
const FVoxelTileKey T = *It;
|
const FVoxelTileKey T = *It;
|
||||||
It.RemoveCurrent();
|
It.RemoveCurrent();
|
||||||
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
||||||
LoadTile(T);
|
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Horizon);
|
||||||
++Submitted;
|
++Submitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11540,7 +11656,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
|
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
|
||||||
}
|
}
|
||||||
if (GVoxelForgeProfileTileGeneration != 0
|
if (GVoxelForgeProfileTileGeneration != 0
|
||||||
&& GetActiveGenerationTaskCount() >= CoarseTaskCap
|
&& GetGenerationAdmissionCount() >= CoarseTaskCap
|
||||||
&& !GVoxelForgeGenerationCapHitReported)
|
&& !GVoxelForgeGenerationCapHitReported)
|
||||||
{
|
{
|
||||||
UE_LOG(LogTemp, Display,
|
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],"
|
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
|
||||||
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%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,"
|
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"pending_by_lod\":%s,"
|
||||||
"\"active_generation\":%d,\"task_cap\":%d,\"near_reserve\":%d,"
|
"\"active_generation\":%d,\"admission_occupancy\":%d,"
|
||||||
"\"coarse_first\":%d,\"dynamic_near_reserve\":%d,\"epoch\":%u,"
|
"\"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"),
|
"\"update_chunks_ms\":%s,\"desired_rebuild_ms\":%s"),
|
||||||
CenterChunk.X, CenterChunk.Y, CenterChunk.Z,
|
CenterChunk.X, CenterChunk.Y, CenterChunk.Z,
|
||||||
*FString::SanitizeFloat(LocalPawn.X), *FString::SanitizeFloat(LocalPawn.Y),
|
*FString::SanitizeFloat(LocalPawn.X), *FString::SanitizeFloat(LocalPawn.Y),
|
||||||
@@ -11613,13 +11731,177 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
|||||||
*FString::SanitizeFloat(PlayerHeading.Size2D()),
|
*FString::SanitizeFloat(PlayerHeading.Size2D()),
|
||||||
bWalkableRingReadyForReserve ? 1 : 0,
|
bWalkableRingReadyForReserve ? 1 : 0,
|
||||||
*DesiredCounts, *ResidentCounts, *PendingCounts,
|
*DesiredCounts, *ResidentCounts, *PendingCounts,
|
||||||
GetActiveGenerationTaskCount(), MaxTasks, NearTaskReserve,
|
GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve,
|
||||||
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
||||||
|
GVoxelForgeStreamingLegacyPendingResultCap,
|
||||||
GenerationEpoch,
|
GenerationEpoch,
|
||||||
*FString::SanitizeFloat(StreamingUpdateMilliseconds),
|
*FString::SanitizeFloat(StreamingUpdateMilliseconds),
|
||||||
*FString::SanitizeFloat(DesiredRebuildMilliseconds)));
|
*FString::SanitizeFloat(DesiredRebuildMilliseconds)));
|
||||||
LastStreamingTraceSampleSeconds = TraceNow;
|
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<unsigned long long>(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 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
|
||||||
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
|
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
|
||||||
? CoarseTaskCap : MaxTasks;
|
? CoarseTaskCap : MaxTasks;
|
||||||
if (GetActiveGenerationTaskCount() >= AdmissionCap)
|
if (GetGenerationAdmissionCount() >= AdmissionCap)
|
||||||
{
|
{
|
||||||
if (GVoxelForgeProfileTileGeneration != 0
|
if (GVoxelForgeProfileTileGeneration != 0
|
||||||
&& !GVoxelForgeGenerationCapHitReported)
|
&& !GVoxelForgeGenerationCapHitReported)
|
||||||
@@ -11812,8 +12094,18 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
|||||||
struct FWorkerGuard
|
struct FWorkerGuard
|
||||||
{
|
{
|
||||||
std::atomic<int32>& Counter;
|
std::atomic<int32>& Counter;
|
||||||
~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
std::atomic<uint64>& BusyCycles;
|
||||||
} WorkerGuard{ActiveGenerationWorkerCount};
|
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;
|
FChunkResult Result;
|
||||||
Result.WorkerTaskStartCycles = WorkerTaskStartCycles;
|
Result.WorkerTaskStartCycles = WorkerTaskStartCycles;
|
||||||
@@ -11942,7 +12234,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
|||||||
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
|
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
|
||||||
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
|
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
|
||||||
? CoarseTaskCap : MaxTasks;
|
? CoarseTaskCap : MaxTasks;
|
||||||
if (GetActiveGenerationTaskCount() >= AdmissionCap) return;
|
if (GetGenerationAdmissionCount() >= AdmissionCap) return;
|
||||||
|
|
||||||
PendingTiles.Add(Tile);
|
PendingTiles.Add(Tile);
|
||||||
const TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe> Cancellation =
|
const TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe> Cancellation =
|
||||||
@@ -11987,8 +12279,18 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
|||||||
struct FWorkerGuard
|
struct FWorkerGuard
|
||||||
{
|
{
|
||||||
std::atomic<int32>& Counter;
|
std::atomic<int32>& Counter;
|
||||||
~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
std::atomic<uint64>& BusyCycles;
|
||||||
} WorkerGuard{ActiveGenerationWorkerCount};
|
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)
|
auto Enqueue = [this, WorkPriority](FChunkResult&& Result)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -443,7 +443,12 @@ public:
|
|||||||
* capped to (logical cores − 2) so small CPUs don't thrash on a flat 16 (background
|
* 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). */
|
* priority stops frame starvation, not the context-switch overhead). */
|
||||||
int32 GetMaxConcurrentTasks() const;
|
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;
|
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. */
|
/** Reserve task slots for collision-critical/edit/LOD0 work; far work uses the remainder. */
|
||||||
int32 GetNearTaskReserve(int32 MaxTasks) const;
|
int32 GetNearTaskReserve(int32 MaxTasks) const;
|
||||||
|
|
||||||
@@ -1093,7 +1098,9 @@ public:
|
|||||||
TQueue<FChunkResult, EQueueMode::Mpsc> EditedProcessQueue;
|
TQueue<FChunkResult, EQueueMode::Mpsc> EditedProcessQueue;
|
||||||
TQueue<FChunkResult, EQueueMode::Mpsc> NearProcessQueue;
|
TQueue<FChunkResult, EQueueMode::Mpsc> NearProcessQueue;
|
||||||
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
||||||
TSet<FVoxelTileKey> 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<FVoxelTileKey> PendingTiles;
|
||||||
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
|
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
|
||||||
TMap<FVoxelTileKey, TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>>
|
TMap<FVoxelTileKey, TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>>
|
||||||
PendingTileCancellation;
|
PendingTileCancellation;
|
||||||
@@ -1121,7 +1128,8 @@ public:
|
|||||||
// Active pendant les mutations de génération lancées par l'éditeur, sans signifier la destruction.
|
// Active pendant les mutations de génération lancées par l'éditeur, sans signifier la destruction.
|
||||||
std::atomic<bool> bGenerationPaused{false};
|
std::atomic<bool> 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<int32> ActiveTaskCount{0};
|
std::atomic<int32> ActiveTaskCount{0};
|
||||||
// Active workers are counted inside the launched tile task, distinct from tasks queued by UE.
|
// 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.
|
// Les travailleurs actifs sont comptes dans la tache de tuile, distincts des taches en attente UE.
|
||||||
@@ -1199,6 +1207,11 @@ public:
|
|||||||
int32 EffectiveNearTaskReserve = -1;
|
int32 EffectiveNearTaskReserve = -1;
|
||||||
uint64 NextStreamingRequestOrdinal = 0;
|
uint64 NextStreamingRequestOrdinal = 0;
|
||||||
std::atomic<uint64> NextStreamingGenerationOrdinal{0};
|
std::atomic<uint64> 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<uint64> TotalGenerationWorkerTaskCycles{0};
|
||||||
|
double LastStreamingSchedulerSampleSeconds = 0.0;
|
||||||
|
uint64 LastStreamingSchedulerWorkerCycles = 0;
|
||||||
|
|
||||||
struct FReadyTransitionVisibilitySample
|
struct FReadyTransitionVisibilitySample
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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).
|
spots are in [WorldAssemblerM3/Round10/REPORT.md](../../WorldAssemblerM3/Round10/REPORT.md).
|
||||||
|
|
||||||
— Luna
|
— 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
|
||||||
@@ -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.
|
||||||
@@ -3,6 +3,7 @@ param(
|
|||||||
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900,
|
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900,
|
||||||
[ValidateRange(12.5, 25.0)][double]$CoreVoxelSizeCm = 25.0,
|
[ValidateRange(12.5, 25.0)][double]$CoreVoxelSizeCm = 25.0,
|
||||||
[ValidateSet('CoarseFirst', 'Legacy')][string]$StreamingOrder = 'Legacy',
|
[ValidateSet('CoarseFirst', 'Legacy')][string]$StreamingOrder = 'Legacy',
|
||||||
|
[ValidateSet('Auto', 'Legacy', 'Dynamic')][string]$NearReserveMode = 'Auto',
|
||||||
[switch]$EnforceCollisionGate,
|
[switch]$EnforceCollisionGate,
|
||||||
[switch]$StartupTrace,
|
[switch]$StartupTrace,
|
||||||
[ValidateRange(0, 1)][int]$StartupApplyBoost = 0,
|
[ValidateRange(0, 1)][int]$StartupApplyBoost = 0,
|
||||||
@@ -145,6 +146,11 @@ if ($SharedFeatureAuditOnly) {
|
|||||||
$RunName = 'shared_feature_audit'
|
$RunName = 'shared_feature_audit'
|
||||||
}
|
}
|
||||||
$RunName += if ($StreamingOrder -eq 'Legacy') { '_legacy_order' } else { '_coarse_first' }
|
$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 ($EnforceCollisionGate) { $RunName += '_collision_gate' }
|
||||||
if ($StartupTrace -or $StreamingTraceOnly) { $RunName += '_trace' }
|
if ($StartupTrace -or $StreamingTraceOnly) { $RunName += '_trace' }
|
||||||
|
|
||||||
@@ -262,9 +268,15 @@ $BaseArguments = @(
|
|||||||
('-out="{0}"' -f $OutputDirectory)
|
('-out="{0}"' -f $OutputDirectory)
|
||||||
)
|
)
|
||||||
$CoarseFirstValue = if ($StreamingOrder -eq 'CoarseFirst') { 1 } else { 0 }
|
$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.StreamingCoarseFirst=$CoarseFirstValue"
|
||||||
$BaseArguments += "-voxel.StreamingDynamicNearReserve=$DynamicReserveValue"
|
$BaseArguments += "-voxel.StreamingDynamicNearReserve=$DynamicReserveValue"
|
||||||
|
$BaseArguments += "-voxel.StreamingLegacyPendingResultCap=$LegacyPendingResultCapValue"
|
||||||
$BaseArguments += "-voxel.StreamingStartupApplyBoost=$StartupApplyBoost"
|
$BaseArguments += "-voxel.StreamingStartupApplyBoost=$StartupApplyBoost"
|
||||||
if ($StreamingTraceOnly) {
|
if ($StreamingTraceOnly) {
|
||||||
$BaseArguments += '-coreworldstreamtrace=1'
|
$BaseArguments += '-coreworldstreamtrace=1'
|
||||||
|
|||||||
Reference in new issue
Block a user