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
262 lines
18 KiB
Markdown
262 lines
18 KiB
Markdown
# 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.
|