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
18 KiB
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:
- Batch a tile's RMC writes. RMC's
Public/Data/RealtimeMeshUpdateBuilder.h:390–406documents grouping mesh, LOD, section-group, section and collision updates behind one write lock and proxy publish;Private/Data/RealtimeMeshUpdateBuilder.cpp:222commits, 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. - Measure fast collision cooking. RMC
Public/Core/RealtimeMeshCollision.h:32–55exposes 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 inPrivate/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. - 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.
- 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.
- Render resource/proxy work.
Private/RenderProxy/RealtimeMeshGPUBuffer.cpp:49–60creates 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, mesh430d2be2bdd8b988, payloadb7b33e14092225c9, topologyc5c46d66606a819e. 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, mesh80a5b054b904b1a2, payloadbf2f1b40dff38acc, topology56074198682d5e18. - 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, frame 01, and frame 02. - 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:
& '.\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:
& '.\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):
& '.\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:
& '.\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):
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:
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:
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,
legacy JSONL,
dynamic stdout,
and dynamic 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.