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:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 08:10:10 +02:00
1 parent b9c13c37c0
commit 5eb9ee76ff
7 files changed
+726 -53

No files matched your search

@@ -1283,3 +1283,30 @@ S5. The new trace, regressions, run ledger, exact log-captured command lines, ha
spots are in [WorldAssemblerM3/Round10/REPORT.md](../../WorldAssemblerM3/Round10/REPORT.md).
— Luna
## World assembler S7 / M3 Round 11 — 2026-10-19
The BuildHost trace counted the old scheduler declines: after LOD0 completed, coarse work
hit the four-slot near reserve for 877 frames; 806 also had finished results waiting to
apply and counted against the old cap. The shared dynamic reserve and active-task count cut
the cold 2,578-key set from 17.527 s to 7.780 s. The walkable ring is 0.581 s and full
LOD0 is 1.439 s, both inside the requested limits. The full set still misses the roughly
4.2 s worker-seconds/10-slots estimate, and measured worker task-seconds rose about 10%,
so this is a throughput win with a tile-cost question left to profile. The moving trace also
shows a separate `bCoreRegionCompilePending` gate: 10 legacy and 11 dynamic submission passes
were rejected during region rebuilds despite free slots.
W1/W8/W16 determinism, Core-off, M2 fixtures, SurfaceWorld/FloatingIslands, S3 ground
agreement and S4 cap coverage passed. Three Standalone captures are saved, but their window
was hidden/occluded and they do not sign off player-visible enclosure. I reviewed RMC's
update batching, async collision and render-proxy paths; no change was clearly worth the
measured apply cost this round. Full commands, hashes, RMC source references, quality
limits and the async audit are in [WorldAssemblerM3/Round11/REPORT.md](../../WorldAssemblerM3/Round11/REPORT.md).
No new source file was added. The likely compile spots are `VoxelWorld.cpp` and
`VoxelWorld.h` in the VoxelForge runtime module; the harness controls changed in
`Tools/TestVoxelForgeCoreWorldHost.ps1`. No owner project/editor, asset or external plugin
file was modified. No commit, push or stash was made.
— Luna
@@ -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.