voxel.StreamingReadyQueueDepth=8 admits one extra worker-set of prioritized tasks; voxel.StreamingBackgroundWorkerCap=1 sizes slots to the 8 TaskGraph background workers actually running tile tasks. Full set 5.96 -> 4.91 s, LOD0 1.40 -> 1.09 s, ring 0.49 -> 0.51 s, 40 m/s ring 75/75. Tick p95 +0.6 ms spawn / +0.26 ms walk, max flat. No geometry change. Report: Standalone/Research/WorldAssemblerM3/Round16. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
19 KiB
World Assembler S12 — Round 16
2026-10-19 · worker supply and tick-independent task pickup
Result
The matched 25 cm SurfaceWorld run completed its 2,578-tile set in 4.911 s on the actor startup clock, down from Round15's 5.960 s. From the first tile submission to readiness it took 4.537 s, down from 5.603 s. Full LOD0's last apply moved from 1.403 s to 1.087 s. The stationary walkable ring measured 0.509 s (Round15: 0.488 s). The 40 m/s trace kept the 3×3 collision-ready ring at 9/9 for every moving gate sample.
The measured ceiling was eight tile workers, not ten: with 12 CPUs in the process affinity, Unreal reported 10 TaskGraph workers split into two foreground and eight background workers. Tile tasks use background priorities, so I sized their admission slots to the eight background workers. A short, ordered TaskGraph queue keeps those workers supplied between actor ticks. No density, triangulation, collision, or Core recipe output changed; recipe versions stay unchanged.
Pool count and priority
| Measurement | BuildHost result |
|---|---|
| Process affinity / priority | 0xFFF (12 CPUs) / Normal |
UE::Tasks scheduler workers / reported maximum |
10 / 20 |
FTaskGraphInterface workers |
10 total: 2 foreground, 8 background |
GThreadPool workers |
10; separate from TaskGraph |
| Step1 cook pool | 0 threads |
| Tile task API / priority | UE::Tasks::Launch; near BackgroundHigh, horizon BackgroundNormal |
| Scheduler worker / background priority fields | 5 / 2 in worker_backend_config |
| Unique tile worker thread IDs / peak simultaneous tile tasks | 8 / 8 |
RMC's inspected async collision path calls DoOnAllowedThread(AsyncThread); that implementation
queues work on GThreadPool (RealtimeMeshFuture.h:147–165,
RealtimeMeshManaged.cpp:703–707). It therefore does not consume the eight TaskGraph background
worker IDs used by tile generation. Other UE background work can share TaskGraph's workers; this
trace attributes tile work by OS thread ID but does not label every unrelated engine task. The
external process sampler counted 40 OS threads after initialization and 58–60 while running,
including engine service threads. The 12-CPU affinity limits simultaneous CPU execution; it is not
a 12-OS-thread process cap. VoxelForge tile generation itself peaked at eight workers, and the
BuildHost process priority stayed Normal.
Shared scheduler and trace
Two read-back CVars control the shipped behavior:
| CVar | Default | Negative control | Effect |
|---|---|---|---|
voxel.StreamingReadyQueueDepth |
8 | 0 | Admit up to one extra worker-set of prioritized tasks |
voxel.StreamingBackgroundWorkerCap |
1 | 0 | Size streaming slots to TaskGraph background workers; 0 restores the logical-core cap |
With the selected defaults, effective tile slots are 8 and the total admission cap is 16. Near
tasks retain BackgroundHigh; horizon tasks retain BackgroundNormal. While moving, the dynamic
near reserve is 7, the near admission cap is 8, and the coarse cap is 9. Collision look-ahead and
critical/edit admission retain their existing precedence. The game-thread scheduler continues to
select and submit from the desired-tile ordering; there is no worker-owned continuation list. UE
dispatches an already-admitted task as a background worker becomes free. bStreamingPlayerMoving is
a game-thread snapshot shared by LoadTile and LoadCoreTile.
Queued tasks retain GenerationEpoch, DesiredEpoch, and per-request cancellation checks. A desired
set change obsoletes stale queued work, workers still check bShuttingDown, and results still return
through the MPSC queues for game-thread application. No UObject or RMC component mutation moved to a
worker. The trace records CVar readbacks in worker_backend_config and
streaming_worker_cap_readback, and records each tile's submission, worker start/end, thread ID,
next scheduler pass, next submission, and next task start on that worker.
Initial-set timeline
The following busy-worker equivalents integrate the 2,578 tile intervals in 500 ms bins from the
first submission (submitted_t_s=0.456843 on the trace clock). The final short bin is 10.7 ms.
| Time from first submission | Busy worker equivalents, out of 8 |
|---|---|
| 0–0.5 s | 6.16 |
| 0.5–1.0 s | 5.94 |
| 1.0–1.5 s | 4.82 |
| 1.5–2.0 s | 4.38 |
| 2.0–2.5 s | 7.83 |
| 2.5–3.0 s | 7.98 |
| 3.0–3.5 s | 6.58 |
| 3.5–4.0 s | 7.47 |
| 4.0–4.5 s | 7.67 |
| 4.5–4.511 s | 1.75 |
All eight worker IDs were used. The initial set accumulated 29.432 worker-seconds; the first worker started at 0.456872 and the last ended at 4.967520 on the trace clock, a 4.511 s worker span. That workload has a 3.679 s ideal at eight fully occupied slots (2.943 s at ten); the measured worker span was 4.511 s. Per-slot idle gaps between one tile ending and that worker's next tile starting summed to 6.573 slot-seconds over 2,570 gaps: p50 0.006 ms, p95 13.604 ms, max 16.800 ms.
| Worker thread ID | Tiles | Inter-task idle seconds | Gap p95 / max |
|---|---|---|---|
| 20144 | 324 | 0.838 | 13.780 / 15.607 ms |
| 21468 | 318 | 0.829 | 13.489 / 16.800 ms |
| 21536 | 304 | 0.830 | 13.237 / 15.902 ms |
| 23392 | 327 | 0.832 | 13.476 / 15.691 ms |
| 23572 | 331 | 0.821 | 13.365 / 15.916 ms |
| 23616 | 331 | 0.847 | 13.765 / 15.773 ms |
| 2716 | 321 | 0.734 | 13.486 / 15.801 ms |
| 5392 | 322 | 0.843 | 13.657 / 15.556 ms |
The scheduler recorded 247 admission passes, 243 with submissions, averaging 10.61 tiles per non-empty pass. Peak initial scheduler backlog was 32; the whole trace's result-backlog peak was 35 tiles / 1,620,010 bytes against the 256-tile cap, with zero cap stops. The completed-result queue itself peaked at five during initial readiness.
Saved/BuildHost/Round16/initial_tile_worker_timeline.csv contains one row per initial tile,
including submitted/start/end, next scheduler admission, next submission, same-thread next start,
and the calculated idle gap. The 500 ms busy bins, per-LOD totals, and per-thread idle table are
also saved there. Files larger than 300 KB remain under Saved/BuildHost/.
Per-LOD completion and work
S12 worker-end/apply values below are normalized to the actor startup clock; S11 apply values are from the Round15 matched report. S12 worker task totals were higher in this sample even though the set finished sooner; the scheduler changes do not change tile contents.
| LOD | Tiles | Worker seconds S11 → S12 | S11 last apply | S12 last worker / last apply |
|---|---|---|---|---|
| 0 | 512 | 3.725 → 4.018 | 1.403 s | 1.084 / 1.087 s |
| 1 | 448 | 3.895 → 4.099 | 2.283 s | 1.749 / 1.757 s |
| 2 | 320 | 0.806 → 0.825 | 2.849 s | 2.107 / 2.122 s |
| 3 | 272 | 2.064 → 2.296 | 3.330 s | 2.469 / 2.484 s |
| 4 | 272 | 6.590 → 7.682 | 4.276 s | 3.431 / 3.440 s |
| 5 | 208 | 1.528 → 1.732 | 4.660 s | 3.706 / 3.709 s |
| 6 | 144 | 2.077 → 2.160 | 5.060 s | 4.048 / 4.062 s |
| 7 | 96 | 1.928 → 2.044 | 5.338 s | 4.289 / 4.293 s |
| 8 | 306 | 4.171 → 4.576 | 6.019 s | 4.885 / 4.894 s |
LOD4 remains the largest worker-cost group: 7.682 s across 272 tiles, or 28.24 ms per tile.
The tile trace attributes this to the overall generation task (generation_s); it does not break
the worker interval into density/classification/meshing stages (classify_s and mesh_s are not
populated for these rows). Its last apply was 3.440 s, so it did not set the tail. I made no LOD4
content optimization without the requested stage attribution and quality-capture work.
Apply, tick, and motion results
| Metric | Round15 S11 | Round16 S12 |
|---|---|---|
| Full set ready, actor startup clock | 5.960 s | 4.911 s |
| First submission to full ready | 5.603 s | 4.537 s |
| Worker span from first submission | 5.581 s | 4.511 s |
| Worker-seconds | 26.786 s | 29.432 s |
| Walkable ring, 9/9 | 0.488 s | 0.509 s |
| Full LOD0 last apply | 1.403 s | 1.087 s |
| Apply frames through readiness | 332 | 268 |
| Apply-count limit frames | 0 | 11 |
| Max visible mesh applies / frame | 10 | 12 |
| Total measured apply time | 236.14 ms | 255.17 ms |
| Max apply time / frame | 2.357 ms | 2.697 ms |
| Result-wait p95 / max | 16.62 / 19.26 ms | 16.90 / 21.03 ms |
| Result backlog peak / cap | 10 / 256 | 35 / 256 |
UWorld::Tick spawn and normal-walk windows were measured in fresh processes:
| Window | S11 p95 / max | S12 p95 / max | Change |
|---|---|---|---|
| Spawn | 2.740 / 5.175 ms | 3.344 / 5.219 ms | +0.604 / +0.044 ms |
| Normal walk | 2.483 / 28.876 ms | 2.744 / 28.752 ms | +0.261 / −0.124 ms |
The maxima stayed effectively flat or lower, while p95 rose by 0.60 ms at spawn and 0.26 ms while walking. I record this as a small p95 increase rather than claiming strict p95 non-regression. Both candidate p95 values remained below 3.4 ms.
The 40 m/s run used the same queue and slot defaults. It completed the initial set in 4.919 s;
all 75/75 moving walkable_gate events reported 9/9, and all 58 moving streaming samples
reported the ring ready. Spawn tick p95/max was 3.173/8.071 ms; the accelerated walk window was
29.562/39.227 ms. The 40 m/s trace's collision look-ahead, dynamic reserve, and moving near cap
readbacks are present in the JSONL.
Negative controls and rejected variants
| Read-back settings | Effective slots / actual worker IDs | Full set ready | Result |
|---|---|---|---|
| Queue depth 0, worker cap 1 | 8 / 8 | 7.024 s | Disabling the ready queue removes the speedup |
| Queue depth 8, worker cap 0 | 10 / 8 | 4.669 s | Restores logical-core admission; still only eight background IDs ran |
| Queue depth 8, worker cap 1 | 8 / 8 | 4.911 s | Selected default, sized to actual background workers |
The first 40 m/s queue prototype lacked the queue-aware moving reserve and LOD0 admission cap; it missed the ring in 56/77 gate events and 47/57 streaming samples. Adding the reserve alone was insufficient. The final moving near cap and reserve passed 75/75 gate events. Reducing the shared Core task cap itself to eight increased per-tile generation cost and made the full set 5.30 s, so that change was reverted; only streaming admission is background-worker-capped.
Foreground-priority variants were also rejected. Raising all tile work to foreground priorities used ten worker IDs but produced 39.422/50.432 ms spawn and 49.137/83.184 ms walk p95/max, with a 5.215 s set. Raising only near work produced an 86.225 ms walk maximum. The final build keeps the original background priorities.
Determinism and quality gates
- Standalone W1/W8/W12 all passed with identical full-clipmap hashes: density
c10697a8f48380c7, meshe75c9119a1498cd5, payload49a9a8d1835a6c8c, topologyedcb23288b5e8724. - BuildHost SurfaceWorld W1/W8/W16 passed with stable density/mesh/payload/topology hashes
0cd2a1a4152ee4aa / a06243aee16b65d5 / c700162935c262d7 / 0af901aa42b452bc. - BuildHost FloatingIslands passed at 25 cm; W1/W8/W16 were stable with hashes
4dcfb37585d7d986 / f14f76fbf065c6ed / 53035c45fd1a082e / 23550100654a4ec3. - Core-off legacy gate passed at 12.5 cm (
legacy_unchanged=PASS). M2 Hybrid V1 and V2 analytic fixture and border suites passed at 25 cm. - The current Standalone far-cap audit passed S4 agreement: 128 shared points, maximum far/near difference 0.0504 cm against a 2 cm tolerance; the 100 cm negative control was detected. Coverage was 2520/2520 (100%), with zero missing mesh/profile crossings. S3's prior 72-point audit remains at 0.0427 cm; scheduler changes do not touch that Core content path.
- The 25 cm SurfaceWorld and FloatingIslands gates both passed. No world recipe version changed.
Builds, commands, and hashes
Builds completed:
.\Tools\BuildVoxelForgeCoreHost.ps1 -MaxParallelActions 12 -TimeoutSeconds 900
.\Standalone\Build.ps1 -Target All -CompileWorkers 12 -BuildPriority Normal
The matched SurfaceWorld trace command was:
.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -StreamingReadyQueueDepth 8 -StreamingBackgroundWorkerCap 1 -TimeoutSeconds 900
The 40 m/s and negative-control commands were:
.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 4000 -StreamingReadyQueueDepth 8 -StreamingBackgroundWorkerCap 1 -TimeoutSeconds 900
.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -StreamingReadyQueueDepth 0 -StreamingBackgroundWorkerCap 1 -TimeoutSeconds 900
.\Tools\TestVoxelForgeCoreWorldHost.ps1 -StreamingTraceOnly -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -StreamingCollisionLookahead 1 -TestMoveDirection '0,1,0' -CoreRegionConcurrentAdmission 1 -StreamingResultBacklogCap 256 -TestMoveSpeedCmPerSecond 600 -StreamingReadyQueueDepth 8 -StreamingBackgroundWorkerCap 0 -TimeoutSeconds 900
Quality commands:
.\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 -CoreVoxelSizeCm 25 -TimeoutSeconds 900
.\Tools\TestVoxelForgeCoreWorldHost.ps1 -FloatingIslandsOnly -CoreVoxelSizeCm 25 -TimeoutSeconds 900
The S3/S4 Standalone agreement and coverage audit command was:
.\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 > Saved/BuildHost/Round16/standalone_S3S4_audit.stdout.log 2> Saved/BuildHost/Round16/standalone_S3S4_audit.stderr.log
Standalone determinism ran serially at Normal priority, affinity 4095, and frame limit 60. Each
worker count used the same remaining arguments and output paths below:
.\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 --far-horizon-lod8-step=0 --process-priority=Normal --process-affinity=4095 --frame-limit=60 > Saved/BuildHost/Round16/standalone_W1.stdout.log 2> Saved/BuildHost/Round16/standalone_W1.stderr.log
.\Standalone\bin\VoxelForgeCoreStandalone.exe --workers=8 --world-assembler-prototype --stream-benchmark --stream-hash --force-generate --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --far-horizon-lod8-step=0 --process-priority=Normal --process-affinity=4095 --frame-limit=60 > Saved/BuildHost/Round16/standalone_W8.stdout.log 2> Saved/BuildHost/Round16/standalone_W8.stderr.log
.\Standalone\bin\VoxelForgeCoreStandalone.exe --workers=12 --world-assembler-prototype --stream-benchmark --stream-hash --force-generate --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --far-horizon-lod8-step=0 --process-priority=Normal --process-affinity=4095 --frame-limit=60 > Saved/BuildHost/Round16/standalone_W12.stdout.log 2> Saved/BuildHost/Round16/standalone_W12.stderr.log
SHA-256 records. Streaming and quality runs used the first VoxelForge module hash. After those runs, I added the French half of one inline comment and rebuilt the current tree; that comment-only rebuild produced the second module hash. The executable code did not change between those two hashes.
| Binary | SHA-256 |
|---|---|
BuildHost UnrealEditor-VoxelForge.dll used for timing and gates |
C9D5DA90B48C5C1F844221C4A6A195947016C522F6A296EBC488A1AD8CCC6EEF |
Current rebuilt BuildHost UnrealEditor-VoxelForge.dll |
12E7883DFB8825012890F1A2B5BACD038AA40FD1C342AA70711EC55AABF9057F |
BuildHost UnrealEditor-VoxelForgeCore.dll |
D87CC56BACCDE6559705EEAEA7DB73D1A6ABF79484D37A3887B94A9FD62BABD3 |
BuildHost UnrealEditor-VoxelForgeEditor.dll |
805FB845F00D013B48E29D4FD5D56EF1531E43F68047AE499DC617B04864DF80 |
Standalone VoxelForgeCoreStandalone.exe |
4EBE946E4D7B245458CD4FD0FBC3FCD6988B98000685C0043B25CD629AFC0F30 |
Failed attempts and safety review
- One intermediate BuildHost compile failed because
PlayerHeadingwas out of scope in two loader admission helpers. I replaced those reads with the game-threadbStreamingPlayerMovingsnapshot; the final BuildHost and Standalone builds passed. - Two early external-sampler attempts failed before launching Unreal (a PowerShell job argument
was flattened, then
Get-Counterrequested an unsupported counter). The later sampler ran. No Unreal run failed; no0xC0000142occurred and no retry was needed. - The final SurfaceWorld command completed under 15 minutes with 3.75 GB peak private memory; FloatingIslands completed with 3.26 GB. The Windows free-memory query was denied during the latter launch, but the BuildHost runner's preflight guard allowed it. No Unreal process remains running.
- Standalone compiled with existing warnings:
vswhere.exewas not found onPATH, andVoxelStrateManager.cppemitted C4838 double-to-float conversion warnings. TheAllbuild completed successfully. Saved/BuildHost/HostProject/Saved/Crashes/contains only the pre-existingUECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000; no new crash folder appeared.- Diff review confirmed the result queues remain
EQueueMode::Mpsc;EndPlaywaits for active tasks and drains queues; workers checkbShuttingDown; epoch and cancellation checks remain;ModsLockstill guards mod data; core pages retain shared ownership through readers; and UObject / RMC component mutation remains on the game thread. No queue mode or page ownership changed. - No source file was added. C++ compile spots are
Source/VoxelForge/Private/VoxelWorld.cpp,Source/VoxelForge/Public/VoxelWorld.h,Source/VoxelForge/Private/VoxelStartupTrace.cpp, andSource/VoxelForge/Public/VoxelStartupTrace.hin the BuildHost VoxelForge module. The test-harness change is PowerShell inTools/TestVoxelForgeCoreWorldHost.ps1; no Core source, generated header,.uasset, owner.uproject, or editor was modified or built. - No commit, push, or stash was created. RealtimeMeshComponent was read-only.
Dialogue
User: Count the workers and their idle gaps, preserve the streaming priorities and safety rules, and get the full set under six seconds without losing the 40 m/s walkable ring.
Codex: The actor had ten TaskGraph workers available but only eight background workers for its background-priority tile tasks. A queue of eight plus an eight-slot admission cap reduced the 2,578-tile startup to 4.911 s; the moving 3×3 ring stayed ready in all 75 gate samples. The process remains Normal priority and limited to 12 CPUs. The measured p95 tick increase is small but recorded, and the worker pool shows why this round did not reach the 3 s ideal for ten workers.