Files
VoxelForge/Standalone/Research/WorldAssemblerM3/Round11/REPORT.md
T
Fr0zkaandClaude Opus 5.5 5eb9ee76ff 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
2026-10-19 08:10:10 +02:00

18 KiB
Raw Blame History

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, 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.