wip(assembler): round S5 -- streaming gates + trace; coarse-first order kept opt-in (no win)

Walkable/enclosed/refined gates defined; JSONL trace joins request, worker start,
build/apply and discards. Coarse-first + dynamic near reserve behind
voxel.StreamingCoarseFirst / voxel.StreamingDynamicNearReserve (default 0 =
S4 order): matched runs showed no walkability or full-view win and slower
LOD0/1 refinement. Instruments settled: 3.1 s standalone reproduced (16.7 s was
another generation route); black early captures were clear-only frames.
Hashes equal S4; Core-off, M2, SurfaceWorld/FloatingIslands BuildHost pass.

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 04:42:41 +02:00
1 parent 9d3c50ca95
commit c0596c5f24
32 files changed
+1049 -65

No files matched your search

+41
View File
@@ -1435,6 +1435,47 @@ LOD7/8 shell repair is skipped for certified SurfaceWorld tiles, the renderer do
backfaces, and the strips predate S4 in S2/S3. Refining all steep faces would multiply far mesh work; backfaces, and the strips predate S4 in S2/S3. Refining all steep faces would multiply far mesh work;
S4 leaves that cost/quality tradeoff open. S4 leaves that cost/quality tradeoff open.
### 8.26 Streaming readiness gates and admission order — World Assembler S5 / M3 Round 9
Do not use “every tile in the current desired clipmap is resident” as a moving-player readiness
gate: the center and desired keys change continuously, so that condition can remain false even when
the player has safe ground and a closed view. Report three independent gates from the first actor
tick: (1) **walkable now** — the support tile and collision-required level-0 tiles in the 3×3 XY
neighborhood have installed collision (or are explicitly collision-not-required); (2) **enclosed
now** — the active view has ground, horizon and the family-appropriate ceiling, with no exposed
coverage hole or unexpected sky; (3) **refined soon** — each target LOD resolves within its stated
time or travel distance after entering range. While moving, report the largest frame/time gap in
enclosure and maximum distance that unresolved work trails the pawn. FloatingIslands has open sky by
design, so its enclosure check excludes a planar roof.
The shared `AVoxelWorld::UpdateChunksAroundPosition` scheduler serves both the Core snapshot path and
Core-off legacy path. It admits the collision-critical 3×3 ring first. The measured S5 order remains
opt-in because it did not beat the legacy order on the gates and the viewer could not confirm screen
presentation: set `voxel.StreamingCoarseFirst=1` to admit coarse desired levels from the maximum LOD
down to `max(2, FullResClipLevels)`, favoring tiles ahead of travel and then nearer tiles within each
LOD; LOD0/1 refinement follows. Default `0` preserves the S4 LOD0-to-LOD8 order.
`voxel.StreamingDynamicNearReserve=1` opts into keeping the authored near-task reserve while the pawn
moves or until the stationary walkable ring is ready; default `0` preserves the previous task budget.
The standalone queue defaults to legacy order; `--coarse-first-stream-order` opts into the S5 tiers
and `--legacy-stream-order` explicitly selects the legacy order.
This order changes only which key starts first. Tile field arithmetic, hashes, `Epoch`, cancellation
checks, `ModsLock`, the four `EQueueMode::Mpsc` result queues and `EndPlay` draining remain unchanged.
The clipmap remains a non-overlapping partition, and `TransitionHold` still performs load-before-
unload reconciliation. No duplicate coarse placeholder is admitted: the current transition rules
do not make a temporary overlapping parent safe at initial spawn without a separate all-descendants
hide/swap gate. A coarse tile completion count alone is not proof of enclosure; verify the actual
view and family-specific ground/ceiling coverage.
`VoxelStartupTrace` records request and worker-start ordinals, LOD, priority, epoch, resident-at-
request, queue wait, build time, result wait, apply time and discarded/cancelled result reasons.
Use `-voxel.StartupTraceUntilEndPlay=1` for a moving trace. Round 9 found similar cold walkable-gate
times, no demonstrated enclosure win, and fewer cancellations at 60 m/s, while that route's center
deltas were invalid. DXGI marked the capture window occluded, so its images are back-buffer evidence
rather than proof of screen presentation. Keep S5 opt-in until an owner-visible run measures the
three gates and closes that gap. Full results and limits are in
`Standalone/Research/WorldAssemblerM3/Round9/REPORT.md`.
## 9. Multiplayer model (listen-server first, dedicated-friendly) ## 9. Multiplayer model (listen-server first, dedicated-friendly)
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
+7 -3
View File
@@ -328,6 +328,9 @@ the four MPSC result queues `CriticalProcessQueue` / `EditedProcessQueue` / `Nea
`ProcessQueue`, and `PendingTiles` (TSet) (VoxelWorld.h:908-912). `ProcessQueue`, and `PendingTiles` (TSet) (VoxelWorld.h:908-912).
**Async state:** `bShuttingDown`, `ActiveTaskCount` (atomics), `GenerationEpoch`, and the **Async state:** `bShuttingDown`, `ActiveTaskCount` (atomics), `GenerationEpoch`, and the
game-thread `PendingTileCancellation` map whose per-request atomic flags are read-only on workers. game-thread `PendingTileCancellation` map whose per-request atomic flags are read-only on workers.
S5 assigns request and worker-start ordinals to each tile, preserving epoch, resident-at-request,
priority, queue-wait and build timing in the opt-in startup trace. All four result queues remain MPSC;
EndPlay still waits for workers and drains every queue.
**⭐ Espace ACTEUR / ACTOR SPACE (2026-08-17).** Le champ de densite est defini en espace **acteur** : **⭐ Espace ACTEUR / ACTOR SPACE (2026-08-17).** Le champ de densite est defini en espace **acteur** :
`(0,0)` est l origine de l acteur `AVoxelWorld`, pas celle du monde Unreal. `WorldToLocalCm` / `(0,0)` est l origine de l acteur `AVoxelWorld`, pas celle du monde Unreal. `WorldToLocalCm` /
@@ -359,7 +362,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
| `AdvanceHeadlessDigMotionTest` / `RecordHeadlessDigMotionFrame` | 3060 / 3217 | Explicit `-voxel.TestDigMotion=1` repro: optional near digs, then repeats underfoot digs and samples `Generator->GetDensityAt` immediately after each edit, on its first subsequent simulated frame, and on every following observation frame. `-voxel.TestDigMotionFieldOverride=1` is a measurement-only prototype that clears `CurrentFloor` and switches CMC to falling when the sampled post-edit density is air; it is inactive by default. The test counts false support, false air and downward velocity beyond one gravity step. `-voxel.TestDigMotionUnderfootDigCount` is clamped to 1–100; `-voxel.TestDigMotionUnderfootIntervalSeconds=0` performs per-frame rapid edits. The accompanying gap audit is scoped to the dig session. | | `AdvanceHeadlessDigMotionTest` / `RecordHeadlessDigMotionFrame` | 3060 / 3217 | Explicit `-voxel.TestDigMotion=1` repro: optional near digs, then repeats underfoot digs and samples `Generator->GetDensityAt` immediately after each edit, on its first subsequent simulated frame, and on every following observation frame. `-voxel.TestDigMotionFieldOverride=1` is a measurement-only prototype that clears `CurrentFloor` and switches CMC to falling when the sampled post-edit density is air; it is inactive by default. The test counts false support, false air and downward velocity beyond one gravity step. `-voxel.TestDigMotionUnderfootDigCount` is clamped to 1–100; `-voxel.TestDigMotionUnderfootIntervalSeconds=0` performs per-frame rapid edits. The accompanying gap audit is scoped to the dig session. |
| `MaybeFinishHeadlessStreamingTest` | — | The existing `voxel.TestExitSeconds` diagnostic emits `VoxelForgeStreamingExitDiag` at most every 30 elapsed seconds, including threshold and early-return test flags; no output when the timer is unconfigured. | | `MaybeFinishHeadlessStreamingTest` | — | The existing `voxel.TestExitSeconds` diagnostic emits `VoxelForgeStreamingExitDiag` at most every 30 elapsed seconds, including threshold and early-return test flags; no output when the timer is unconfigured. |
| `UpdateVisualStalenessAudit` | 5034 | Test-only camera-frustum coverage audit: tracks up to 24 real pre-skirt isosurface points per visible component, then counts frames where no visible tile component covers a previously visible, still-desired point (excluding edited bounds). `voxel.TestVisualGapInjectEarlyHide=1` hides a valid component for three frames and must be caught. `voxel.TestGapAuditLegacyHandoff=1` restores old handoff culling for A/B baselines. | | `UpdateVisualStalenessAudit` | 5034 | Test-only camera-frustum coverage audit: tracks up to 24 real pre-skirt isosurface points per visible component, then counts frames where no visible tile component covers a previously visible, still-desired point (excluding edited bounds). `voxel.TestVisualGapInjectEarlyHide=1` hides a valid component for three frames and must be caught. `voxel.TestGapAuditLegacyHandoff=1` restores old handoff culling for A/B baselines. |
| `ProcessPendingChunks` | 7435 | Drains critical, edited, near, then ordinary result queues under the per-frame budget; reads the per-request cancellation token before removing its map entry, drops obsolete/aborted results, then applies each live result via `ApplyTileResult`. Collision profiling timestamps the result's arrival here to separate worker completion from game-thread apply wait. Step 1's `voxel.Step1PrequeueVisibleLOD0` holds completed results until the requested non-empty L0 count is ready, then releases them through this same drain and emits the `VFStep1UploadCorpusReleased` CSV event for UE's delayed CSV capture. | | `ProcessPendingChunks` | 7435 | Drains `CriticalProcessQueue`, `EditedProcessQueue`, `NearProcessQueue`, then `ProcessQueue` under the per-frame budget; reads the per-request cancellation token before removing its map entry, traces stale/obsolete/aborted discards with epoch and request ordinal, then applies each live result via `ApplyTileResult`. Collision profiling timestamps result arrival separately from game-thread apply wait. Step 1's `voxel.Step1PrequeueVisibleLOD0` still releases through this drain and emits `VFStep1UploadCorpusReleased`. |
| `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale generation epochs and keys no longer desired, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). | | `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale generation epochs and keys no longer desired, marks loaded, ingests capture, EMPTY releases the tile's existing component (a re-gen can flip content→empty on band change — old geometry must not linger), else `ApplyMeshToTile`. Returns true iff a visible mesh uploaded (counts the budget). Doesn't touch `PendingTiles` (caller's). |
| `FVoxelStackSampler` / `FScopedVoxelStackRegistration` (`VoxelStackSampler.h/.cpp`) | — | **Diagnostic raw stack sampler, off by default.** A sampler thread captures registered generation workers' stacks (`CaptureThreadStackBackTrace`) every N µs and tags each with the tile's LOD. It writes bounded raw PCs to CSV plus a symbolization-free summary under `Saved/`; symbol resolution is deferred until after process exit so the measured process never initializes/calls DbgHelp for this diagnostic. Lifecycle teardown is serialized and registration TLS is session-tagged. Registration is per tile TASK in `LoadTile` (and the export's tasks), never per sample; disabled costs one relaxed atomic load. Game `-voxel.SampleStacks=<us>`, commandlet `-samplestacks[=us]`. | | `FVoxelStackSampler` / `FScopedVoxelStackRegistration` (`VoxelStackSampler.h/.cpp`) | — | **Diagnostic raw stack sampler, off by default.** A sampler thread captures registered generation workers' stacks (`CaptureThreadStackBackTrace`) every N µs and tags each with the tile's LOD. It writes bounded raw PCs to CSV plus a symbolization-free summary under `Saved/`; symbol resolution is deferred until after process exit so the measured process never initializes/calls DbgHelp for this diagnostic. Lifecycle teardown is serialized and registration TLS is session-tagged. Registration is per tile TASK in `LoadTile` (and the export's tasks), never per sample; disabled costs one relaxed atomic load. Game `-voxel.SampleStacks=<us>`, commandlet `-samplestacks[=us]`. |
| Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **LOD-gated:** fused path at LOD ≥ `voxel.TileCacheWindowMinLOD` (3), op-stack at LOD ≥ `voxel.TileCacheWindowOpStackMinLOD` (4); lower LODs keep the 4-chunk worker window, because the tile window slowed LOD0 evaluation ~6%. Strates whose authored terrain ops exceed the proven 256-voxel extent (or are malformed/unloaded) fall back to the legacy window (`VF_ComputeTerrainOpEnvelope`). `voxel.SpatialIndex` is -1 auto (off at fused LOD0, on elsewhere), 0 or 1. **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). | | Room-cache performance (2026-09-13): `VF_FindPlayerFitPointForRoomMemoized` / `GPlayerFitMemo` (`VoxelCaveMorphology.cpp`), `voxel.TileCacheWindow` + `VoxelGenLOD::TileOriginVoxels/TileCellsPerAxis` (`VoxelGenerator.h`), `FChunkSDFSpatialIndex` | — | **Player-fit memo:** exact 97-word key, process-wide, `voxel.PlayerFitMemo`. **Tile cache window:** the room cache covers the requesting tile's footprint plus a fixed collect margin. It is set via `TGuardValue` in `GenerateMesh`/classify, and is window-invariant by construction (the margin does not grow with the tile). **LOD-gated:** fused path at LOD ≥ `voxel.TileCacheWindowMinLOD` (3), op-stack at LOD ≥ `voxel.TileCacheWindowOpStackMinLOD` (4); lower LODs keep the 4-chunk worker window, because the tile window slowed LOD0 evaluation ~6%. Strates whose authored terrain ops exceed the proven 256-voxel extent (or are malformed/unloaded) fall back to the legacy window (`VF_ComputeTerrainOpEnvelope`). `voxel.SpatialIndex` is -1 auto (off at fused LOD0, on elsewhere), 0 or 1. **Spatial index:** ordered XY buckets over cache primitives, exact rejection. The `static_assert` in `VoxelStrateTypes.h` ties `VF_STRATE_PARAM_FIELDS` to `FStrateGenerationParams` (83 fields + 8 B padding). |
@@ -367,7 +370,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. | | `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. |
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread. Uses `PreEditDensityCache` when the tile is resident in the bounded local cache, replays the current DiffLayer over that exact procedural lattice, then meshes/applies inline so a carve is visible THIS frame. A cache miss uses normal generation; it captures the unedited lattice if the tile needs meshing, while uniform-tile proofs remain enabled. | | `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread. Uses `PreEditDensityCache` when the tile is resident in the bounded local cache, replays the current DiffLayer over that exact procedural lattice, then meshes/applies inline so a carve is visible THIS frame. A cache miss uses normal generation; it captures the unedited lattice if the tile needs meshing, while uniform-tile proofs remain enabled. |
| `PreEditDensityCache` / `VoxelForgePreEditDensityCache::BuildEditedGrid` | — | Per-world cache of the pawn's current L0 tile: exact 35³ pre-edit samples, max **171,500 bytes**. It is pruned when the pawn crosses chunk boundaries and cleared on `RegenerateAllChunks`. `BuildEditedGrid` snapshots touched diff chunks once and reproduces `GetDensityAt`'s final `pre-edit - EvaluateMods` operation before the per-call shared-grid mesher path. | | `PreEditDensityCache` / `VoxelForgePreEditDensityCache::BuildEditedGrid` | — | Per-world cache of the pawn's current L0 tile: exact 35³ pre-edit samples, max **171,500 bytes**. It is pruned when the pawn crosses chunk boundaries and cleared on `RegenerateAllChunks`. `BuildEditedGrid` snapshots touched diff chunks once and reproduces `GetDensityAt`'s final `pre-edit - EvaluateMods` operation before the per-call shared-grid mesher path. |
| `UpdateChunksAroundPosition` | 8603 | Builds desired set, cancels pending keys that left it, sorts from the **true pawn position**, submits the occupied/support LOD0 tile and the next tile along the pawn heading as an absolute `BackgroundHigh` prefix, then loads/unloads and handles LOD changes. **Delta cull**: `BuildDesiredTiles` returns the LEAVERS (stamped `DesiredStamped` map, one sweep) — only those + `TransitionHold` are considered per crossing, not every loaded tile. `BuildDesiredTiles` also applies `RenderDistanceChunks`: the outermost shell widens to cover the distance as level-MaxClipLevel MC tiles (`VF_OuterShell` shared with `IsTileInClipRange` so the cull sees the same horizon), dz pre-clamped to the vertical band. **§9.3 anchors:** also prunes dead `StreamingAnchors` + detects their chunk crossings → rebuilds the desired set when an anchor moves/(un)registers (`bAnchorsMoved`/`bForceDesiredRebuild`), same cadence as player movement. §8.10. | | `UpdateChunksAroundPosition` | 8603 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission is opt-in (`voxel.StreamingCoarseFirst=1`): it submits remaining coarse levels from max LOD down through `max(2, FullResClipLevels)`, then LOD0/1 refinement; ties favor travel direction, then distance. Default `0` preserves S4's LOD0-to-LOD8 order. `voxel.StreamingDynamicNearReserve=1` opts into holding the authored near-worker reserve while moving or until the stationary walkable ring is ready; default `0`. The clipmap stays non-overlapping and existing load-before-unload / `TransitionHold` reconciliation remains authoritative; S5 adds no overlapping placeholder layer. Standalone defaults to legacy; use `--coarse-first-stream-order` to opt in. §8.10 + §8.26. |
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` / `MeasureHeadlessCollisionGateStressTerrain` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The normal spawn-height cvar offsets from the measured density surface; `voxel.TestCollisionGateStressDeepDrop=1` carves a process-local box shaft from above the top to just above the floor tile, extending along the post-landing route so the pawn does not run into the shaft sidewall, temporarily raises only the transient test DiffLayer brush-radius allowance for that fixture, removes only the local pawn's redundant streaming anchor for this isolated repro, proves every sample in two complete outside-ring level-0 tiles is air before seeding their known-empty status, prewarms the known-empty in-ring column, and asserts that the support walk reaches the in-layout floor outside the aligned level-0 render ring before the pawn drops. The log reports whether the fixed gate retains that otherwise out-of-ring target. Other streaming anchors remain active. The fixture rejects a target or floor outside the layout, a non-air empty-tile proof, or a floor inside the aligned level-0 ring. The `GetDensityAt` column at capsule feet defines the local air/solid surface; the deep-drop column crossing is cached after its first exact scan. Each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; deep-drop gate-on also requires horizontal travel during a hold. Result logs include the terminal density column, first crossing below spawn height, hold count/time, and gated horizontal progress. | | `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` / `MeasureHeadlessCollisionGateStressTerrain` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The normal spawn-height cvar offsets from the measured density surface; `voxel.TestCollisionGateStressDeepDrop=1` carves a process-local box shaft from above the top to just above the floor tile, extending along the post-landing route so the pawn does not run into the shaft sidewall, temporarily raises only the transient test DiffLayer brush-radius allowance for that fixture, removes only the local pawn's redundant streaming anchor for this isolated repro, proves every sample in two complete outside-ring level-0 tiles is air before seeding their known-empty status, prewarms the known-empty in-ring column, and asserts that the support walk reaches the in-layout floor outside the aligned level-0 render ring before the pawn drops. The log reports whether the fixed gate retains that otherwise out-of-ring target. Other streaming anchors remain active. The fixture rejects a target or floor outside the layout, a non-air empty-tile proof, or a floor inside the aligned level-0 ring. The `GetDensityAt` column at capsule feet defines the local air/solid surface; the deep-drop column crossing is cached after its first exact scan. Each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; deep-drop gate-on also requires horizontal travel during a hold. Result logs include the terminal density column, first crossing below spawn height, hold count/time, and gated horizontal progress. |
| `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells), then adds streaming anchors and the pending player's collision-only support column before the leaver sweep. It orders from the true local pawn position and builds `CriticalDesiredTiles` for the unresolved support target, occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants, including face/edge-touching replacements) for the reconcile pass. | | `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells), then adds streaming anchors and the pending player's collision-only support column before the leaver sweep. It orders from the true local pawn position and builds `CriticalDesiredTiles` for the unresolved support target, occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants, including face/edge-touching replacements) for the reconcile pass. |
| `CollisionGateDesiredTiles` / `RequestCollisionGateSupportTile` | 5062 | Retains an unresolved support target and up to three level-0 tiles below it across desired-set rebuilds. The additional keys are hidden collision-only tiles, queued ahead of ordinary streaming, and pruned once resolved or outside the active column. | | `CollisionGateDesiredTiles` / `RequestCollisionGateSupportTile` | 5062 | Retains an unresolved support target and up to three level-0 tiles below it across desired-set rebuilds. The additional keys are hidden collision-only tiles, queued ahead of ordinary streaming, and pruned once resolved or outside the active column. |
@@ -380,7 +383,8 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
| `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set and the independent collision set, so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. | | `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set and the independent collision set, so AI/remote players keep collision loaded around them; delta cull releases them on move/unregister. **§9.4:** a tile only a CollisionOnly anchor wants (clipmap didn't stamp it) → `CollisionOnlyTiles` → hidden at apply. Zero cost when no anchors. |
| `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. | | `ReconcileAnchorTileVisibility` | — | **§9.4:** after each rebuild, toggle `SetVisibility` on ALREADY-LOADED tiles that flipped render↔collision-only (diff `CollisionOnlyTiles` vs prev — bounded, no O(loaded) scan; unhide only if still desired). No-op without CollisionOnly anchors. |
| `RegisterStreamingAnchor` / `UnregisterStreamingAnchor` | — | **BlueprintCallable §9.3:** add/remove an actor as a streaming anchor (`EVoxelAnchorPolicy` CollisionOnly/FullVisual + XY/ZBelow/ZAbove box). Idempotent; forces a rebuild next Tick. §9.4: CollisionOnly tiles cook collision but are hidden (no draw/VSM) unless the player clipmap wants them too. | | `RegisterStreamingAnchor` / `UnregisterStreamingAnchor` | — | **BlueprintCallable §9.3:** add/remove an actor as a streaming anchor (`EVoxelAnchorPolicy` CollisionOnly/FullVisual + XY/ZBelow/ZAbove box). Idempotent; forces a rebuild next Tick. §9.4: CollisionOnly tiles cook collision but are hidden (no draw/VSM) unless the player clipmap wants them too. |
| `LoadTile` | 9033 | Budget check → `UE::Tasks::Launch` background gen+mesh; RAII task guard. Worker runs `Generator->ClassifyTile` first (T1.d): AllSolid/AllAir ⇒ skip `GenerateMesh`, tile stays empty (capture tiles always generate). It carries `GenerationEpoch`/desired epoch and checks `bShuttingDown`, generation pause, and its read-only cancellation token between expensive stages; obsolete work returns through `ProcessQueue`. STRATE CONTENT CUT: tiles ≥ `StrateContentCutMinLevel` pass the player-strate band (`MeshBandChunkLo/Hi` → voxels) to `GenerateMesh` + stamp it on `FChunkResult::BandChunkLo/Hi`; band change re-queues via `BandRemeshQueue` (see `UpdateChunksAroundPosition`). `voxel.TestFullResClipLevels` is a measurement-only runtime override; default -1 uses the authored setting. TOO-COARSE SKIP: if one cell is taller than the band (`Step > band height` — level ≥7 territory) the tile is enqueued EMPTY without launching a task (cell-granular cut could only render garbage). §8.10. | | `LoadTile` / `LoadCoreTile` | 9033 / 11633 | Budget check → background generation; RAII task guard. Both paths preserve `Epoch`, desired/modification epochs, cancellation state and MPSC result routing. Requests receive a monotonic request ordinal; workers assign a generation-start ordinal. `VoxelStartupTrace` joins those with priority, resident-at-request, queue wait, worker build, result wait, apply time and discard reason. Legacy `LoadTile` retains its classify/mesh and strate-band behavior; Core delegates to the resolved immutable snapshot. `EndPlay` still drains workers/results. §8.10. |
| `VoxelStartupTrace::RecordRequest` / `RecordTile` | — | Opt-in JSONL via `-voxel.StartupTraceFile=...`; `-voxel.StartupTraceUntilEndPlay=1` keeps tracing through movement, otherwise it stops at initial settle. Each request is keyed by epoch and ordinal; applied tile records add worker-start order, priority, queue/build/apply timings. Discard events identify stale epoch, obsolete, no-longer-desired and aborted results. |
| `UnloadTile` | — | Clears tile state; marks any matching generation request obsolete without removing its pending slot, so a late result cannot clear a replacement request; the component is PARKED in the pool (T2.c), not destroyed. | | `UnloadTile` | — | Clears tile state; marks any matching generation request obsolete without removing its pending slot, so a late result cannot clear a replacement request; the component is PARKED in the pool (T2.c), not destroyed. |
| `ApplyMeshToTile` | 9838 | Upload geometry. One component per tile (clipmap keeps count low; supersedes the old region batching); worker-built streams (T1.f) → `CreateSectionGroup(MoveTemp)`. Reuses the component's existing `URealtimeMesh` (no per-apply mesh alloc). **F17: two polygroups** (0 ground / 1 sky-cap, per-triangle class from the mesher) → RMC auto-section per non-empty group; slot 0 = override/default material, slot 1 = `CeilingMaterial` (fallback ground); config gated by `FChunkResult::bHasGroundTris/bHasCeilingTris`. Takes `FChunkResult&`; strate lookups clamp Z into `Result.BandChunkLo/Hi` (strate content cut) — ground at clamped bottom chunk, cap at clamped top (mid = gap fallback). Collision level-0 only (T1.c) both groups; shadow per SECTION: ground casts at level≤1, cap never. Ordinary same-tile re-cooks retain the installed body while it is valid. A carve intersecting the pawn's current support tile and capsule bounds suppresses that old body until the replacement cook is installed. `voxel.ProfileCollisionCookStages` reports generation/apply waits, combined RMC time, completion hops, triangle/skirt counts and collision settings. With `voxel.Step1PrequeueVisibleLOD0>0`, logs apply frame and waits for `GetLastRenderTime` to advance for first-visible frame; `voxel.Step1UploadDisableCollision=1` isolates render upload. Routes each terrain base through shared MIDs; disabled volume MIDs bind neutral `VolP*` and null `VolTex*`. **§9.4:** `SetVisibility(false)` when the tile is in `CollisionOnlyTiles` (anchor-only, hidden — collision still cooks). §8.10 + ARCHITECTURE SurfaceWorld row. | | `ApplyMeshToTile` | 9838 | Upload geometry. One component per tile (clipmap keeps count low; supersedes the old region batching); worker-built streams (T1.f) → `CreateSectionGroup(MoveTemp)`. Reuses the component's existing `URealtimeMesh` (no per-apply mesh alloc). **F17: two polygroups** (0 ground / 1 sky-cap, per-triangle class from the mesher) → RMC auto-section per non-empty group; slot 0 = override/default material, slot 1 = `CeilingMaterial` (fallback ground); config gated by `FChunkResult::bHasGroundTris/bHasCeilingTris`. Takes `FChunkResult&`; strate lookups clamp Z into `Result.BandChunkLo/Hi` (strate content cut) — ground at clamped bottom chunk, cap at clamped top (mid = gap fallback). Collision level-0 only (T1.c) both groups; shadow per SECTION: ground casts at level≤1, cap never. Ordinary same-tile re-cooks retain the installed body while it is valid. A carve intersecting the pawn's current support tile and capsule bounds suppresses that old body until the replacement cook is installed. `voxel.ProfileCollisionCookStages` reports generation/apply waits, combined RMC time, completion hops, triangle/skirt counts and collision settings. With `voxel.Step1PrequeueVisibleLOD0>0`, logs apply frame and waits for `GetLastRenderTime` to advance for first-visible frame; `voxel.Step1UploadDisableCollision=1` isolates render upload. Routes each terrain base through shared MIDs; disabled volume MIDs bind neutral `VolP*` and null `VolTex*`. **§9.4:** `SetVisibility(false)` when the tile is in `CollisionOnlyTiles` (anchor-only, hidden — collision still cooks). §8.10 + ARCHITECTURE SurfaceWorld row. |
| `HandleRealtimeMeshCollisionBodyUpdated` / `HandleTileCollisionCookComplete` / `PollStep1SupportSweepProbes` | 6964 / 7192 / — | Correlate RMC body-install event and collision-future completion. `voxel.ProfileCollisionCookStages=1` logs worker queue/gen, result wait, apply/upload/config submission, RMC submit→body update, body event→future, future→game-thread hop, submit frames, pending VoxelForge cooks, priority, collision flags, and triangle/skirt totals; it also reports mesh-generation-end→ready. `voxel.Step1CollisionProbe=1` sweeps the pawn capsule against the exact component pointer, logs both the first hit and whether it is the target body, and checks every body in the captured support cohort. RMC's end-frame dequeue, async worker start, geometry extraction and core cook remain inside the external plugin and are not individually timestampable from this module. | | `HandleRealtimeMeshCollisionBodyUpdated` / `HandleTileCollisionCookComplete` / `PollStep1SupportSweepProbes` | 6964 / 7192 / — | Correlate RMC body-install event and collision-future completion. `voxel.ProfileCollisionCookStages=1` logs worker queue/gen, result wait, apply/upload/config submission, RMC submit→body update, body event→future, future→game-thread hop, submit frames, pending VoxelForge cooks, priority, collision flags, and triangle/skirt totals; it also reports mesh-generation-end→ready. `voxel.Step1CollisionProbe=1` sweeps the pawn capsule against the exact component pointer, logs both the first hit and whether it is the target body, and checks every body in the captured support cohort. RMC's end-frame dequeue, async worker start, geometry extraction and core cook remain inside the external plugin and are not individually timestampable from this module. |
@@ -5,6 +5,8 @@
#include "Misc/CommandLine.h" #include "Misc/CommandLine.h"
#include "Misc/Parse.h" #include "Misc/Parse.h"
#include <atomic>
namespace VoxelForgeStartupTrace namespace VoxelForgeStartupTrace
{ {
namespace namespace
@@ -27,6 +29,7 @@ namespace
FCriticalSection GTraceLock; FCriticalSection GTraceLock;
TUniquePtr<FState> GState; TUniquePtr<FState> GState;
std::atomic<bool> GTraceActive{false};
FString JsonEscape(const FString& In) FString JsonEscape(const FString& In)
{ {
@@ -49,9 +52,8 @@ namespace
const FString WithNewline = Line + TEXT("\n"); const FString WithNewline = Line + TEXT("\n");
FTCHARToUTF8 Utf8(*WithNewline); FTCHARToUTF8 Utf8(*WithNewline);
State.Archive->Serialize(const_cast<ANSICHAR*>(Utf8.Get()), Utf8.Length()); State.Archive->Serialize(const_cast<ANSICHAR*>(Utf8.Get()), Utf8.Length());
// The trace is intended to remain readable if a harness terminates the editor after // The file archive buffers records; flushing every tile/request can stall the game thread.
// the steady-state record. This is bounded I/O (startup only), not the shared UE log. // L'archive tamponne les lignes ; vider apres chaque tuile/requete peut bloquer le thread de jeu.
State.Archive->Flush();
} }
void WriteEventLocked(FState& State, const TCHAR* Type, const FString& FieldsJson) void WriteEventLocked(FState& State, const TCHAR* Type, const FString& FieldsJson)
@@ -203,6 +205,7 @@ void BeginFromCommandLine()
NewState->StartSeconds = FPlatformTime::Seconds(); NewState->StartSeconds = FPlatformTime::Seconds();
NewState->bActive = true; NewState->bActive = true;
GState = MoveTemp(NewState); GState = MoveTemp(NewState);
GTraceActive.store(true, std::memory_order_release);
WriteEventLocked(*GState, TEXT("header"), FString::Printf( WriteEventLocked(*GState, TEXT("header"), FString::Printf(
TEXT("\"schema\":1,\"path\":\"%s\",\"tile_record_cap\":%d"), TEXT("\"schema\":1,\"path\":\"%s\",\"tile_record_cap\":%d"),
*JsonEscape(GState->Path), GState->TileRecordCap)); *JsonEscape(GState->Path), GState->TileRecordCap));
@@ -210,8 +213,7 @@ void BeginFromCommandLine()
bool IsActive() bool IsActive()
{ {
FScopeLock Lock(&GTraceLock); return GTraceActive.load(std::memory_order_acquire);
return GState.IsValid() && GState->bActive;
} }
void RecordEvent(const TCHAR* Type, const FString& FieldsJson) void RecordEvent(const TCHAR* Type, const FString& FieldsJson)
@@ -221,6 +223,18 @@ void RecordEvent(const TCHAR* Type, const FString& FieldsJson)
WriteEventLocked(*GState, Type, FieldsJson); WriteEventLocked(*GState, Type, FieldsJson);
} }
void RecordRequest(const FTileSample& Sample, uint32 Epoch)
{
FScopeLock Lock(&GTraceLock);
if (!GState.IsValid() || !GState->bActive) return;
WriteEventLocked(*GState, TEXT("request"), FString::Printf(
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
"\"request_ordinal\":%llu,\"epoch\":%u,\"resident_at_request\":%d"),
Sample.TileX, Sample.TileY, Sample.TileZ, Sample.Level, Sample.WorkPriority,
static_cast<unsigned long long>(Sample.RequestOrdinal), Epoch,
Sample.bResidentAtRequest ? 1 : 0));
}
void StageBegin(const TCHAR* Name) void StageBegin(const TCHAR* Name)
{ {
if (Name == nullptr) return; if (Name == nullptr) return;
@@ -266,11 +280,17 @@ void RecordTile(const FTileSample& Sample)
GState->Samples.Add(Sample); GState->Samples.Add(Sample);
++GState->TileRecordsWritten; ++GState->TileRecordsWritten;
WriteEventLocked(*GState, TEXT("tile"), FString::Printf( WriteEventLocked(*GState, TEXT("tile"), FString::Printf(
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu," TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
"\"request_ordinal\":%llu,\"generation_ordinal\":%llu,\"resident_at_request\":%d,"
"\"verdict\":%d,\"proof\":%d,\"empty\":%d,\"vertices\":%d,\"triangles\":%d,\"geometry_bytes\":%llu,"
"\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s," "\"request_to_apply_s\":%s,\"queue_wait_s\":%s,\"worker_queue_s\":%s,\"result_queue_s\":%s,"
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"), "\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
Sample.TileX, Sample.TileY, Sample.TileZ, Sample.TileX, Sample.TileY, Sample.TileZ,
Sample.Level, Sample.Verdict, Sample.bSealedSolidProof ? 1 : 0, Sample.Level, Sample.WorkPriority,
static_cast<unsigned long long>(Sample.RequestOrdinal),
static_cast<unsigned long long>(Sample.GenerationOrdinal),
Sample.bResidentAtRequest ? 1 : 0,
Sample.Verdict, Sample.bSealedSolidProof ? 1 : 0,
Sample.bEmpty ? 1 : 0, Sample.Vertices, Sample.Triangles, Sample.bEmpty ? 1 : 0, Sample.Vertices, Sample.Triangles,
static_cast<unsigned long long>(Sample.GeometryBytes), static_cast<unsigned long long>(Sample.GeometryBytes),
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds), *Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
@@ -283,6 +303,7 @@ void Finish(const TCHAR* Reason)
{ {
FScopeLock Lock(&GTraceLock); FScopeLock Lock(&GTraceLock);
if (!GState.IsValid() || !GState->bActive) return; if (!GState.IsValid() || !GState->bActive) return;
GTraceActive.store(false, std::memory_order_release);
// Close any stage left open by an abnormal early exit so the summary remains useful. // Close any stage left open by an abnormal early exit so the summary remains useful.
const double Now = FPlatformTime::Seconds(); const double Now = FPlatformTime::Seconds();
+358 -37
View File
@@ -179,6 +179,18 @@ namespace
GVoxelForgeCollisionGate, GVoxelForgeCollisionGate,
TEXT("Protect the pawn from falling through unresolved ground: 0 off, 1 on.")); TEXT("Protect the pawn from falling through unresolved ground: 0 off, 1 on."));
int32 GVoxelForgeStreamingCoarseFirst = 0;
FAutoConsoleVariableRef CVarVoxelForgeStreamingCoarseFirst(
TEXT("voxel.StreamingCoarseFirst"),
GVoxelForgeStreamingCoarseFirst,
TEXT("Prioritize current walkable tiles, then far coarse clipmap levels, then near refinement; 0 restores legacy LOD order."));
int32 GVoxelForgeStreamingDynamicNearReserve = 0;
FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve(
TEXT("voxel.StreamingDynamicNearReserve"),
GVoxelForgeStreamingDynamicNearReserve,
TEXT("Keep the authored near-worker reserve while the pawn moves or its walkable ring is incomplete; release it when stationary and ready."));
int32 GVoxelForgeProfileTileGeneration = 0; int32 GVoxelForgeProfileTileGeneration = 0;
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration( FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
TEXT("voxel.ProfileTileGeneration"), TEXT("voxel.ProfileTileGeneration"),
@@ -2988,6 +3000,12 @@ void AVoxelWorld::ConfigureHeadlessStreamingTest()
bStartupTraceThroughCrossing = TraceThroughCrossingValue != 0; bStartupTraceThroughCrossing = TraceThroughCrossingValue != 0;
} }
int32 TraceUntilEndPlayValue = 0;
if (FParse::Value(CommandLine, TEXT("voxel.StartupTraceUntilEndPlay="), TraceUntilEndPlayValue))
{
bStartupTraceUntilEndPlay = TraceUntilEndPlayValue != 0;
}
float CenterX = 0.0f; float CenterX = 0.0f;
float CenterY = 0.0f; float CenterY = 0.0f;
float CenterZ = 0.0f; float CenterZ = 0.0f;
@@ -6807,6 +6825,7 @@ void AVoxelWorld::Tick(float DeltaTime)
&& PendingTiles.Num() == 0 && PendingTiles.Num() == 0
&& bStartupTraceDesiredRecorded && bStartupTraceDesiredRecorded
&& PendingCollisionCooks.Num() == 0 && PendingCollisionCooks.Num() == 0
&& !bStartupTraceUntilEndPlay
&& (!bStartupTraceThroughCrossing && (!bStartupTraceThroughCrossing
|| !bHeadlessStrateCrossingTest || !bHeadlessStrateCrossingTest
|| bHeadlessStrateCrossingTestPassed || bHeadlessStrateCrossingTestPassed
@@ -9093,6 +9112,22 @@ void AVoxelWorld::ProcessPendingChunks()
if (DequeuedChunk.bAborted || bRequestObsolete if (DequeuedChunk.bAborted || bRequestObsolete
|| DequeuedChunk.Epoch != GenerationEpoch || !IsDesired(DequeuedChunk.Tile)) || DequeuedChunk.Epoch != GenerationEpoch || !IsDesired(DequeuedChunk.Tile))
{ {
if (VoxelForgeStartupTrace::IsActive())
{
VoxelForgeStartupTrace::RecordEvent(TEXT("tile_discard"), FString::Printf(
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
"\"request_ordinal\":%llu,\"epoch\":%u,\"current_epoch\":%u,"
"\"reason\":\"%s\""),
DequeuedChunk.Tile.Coord.X, DequeuedChunk.Tile.Coord.Y,
DequeuedChunk.Tile.Coord.Z, DequeuedChunk.Tile.Level,
static_cast<int32>(DequeuedChunk.WorkPriority),
static_cast<unsigned long long>(DequeuedChunk.RequestOrdinal),
DequeuedChunk.Epoch, GenerationEpoch,
DequeuedChunk.bAborted ? TEXT("aborted")
: (bRequestObsolete ? TEXT("obsolete")
: (DequeuedChunk.Epoch != GenerationEpoch ? TEXT("stale_epoch")
: TEXT("not_desired")))));
}
PendingTileCancellation.Remove(DequeuedChunk.Tile); PendingTileCancellation.Remove(DequeuedChunk.Tile);
PendingTiles.Remove(DequeuedChunk.Tile); PendingTiles.Remove(DequeuedChunk.Tile);
if (bRequestObsolete) if (bRequestObsolete)
@@ -9164,6 +9199,19 @@ void AVoxelWorld::ProcessPendingChunks()
// if the tile leaves and re-enters the desired set before the queue is drained. // if the tile leaves and re-enters the desired set before the queue is drained.
if (DequeuedChunk.bAborted || bRequestObsolete) if (DequeuedChunk.bAborted || bRequestObsolete)
{ {
if (VoxelForgeStartupTrace::IsActive())
{
VoxelForgeStartupTrace::RecordEvent(TEXT("tile_discard"), FString::Printf(
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
"\"request_ordinal\":%llu,\"epoch\":%u,\"current_epoch\":%u,"
"\"reason\":\"%s\""),
DequeuedChunk.Tile.Coord.X, DequeuedChunk.Tile.Coord.Y,
DequeuedChunk.Tile.Coord.Z, DequeuedChunk.Tile.Level,
static_cast<int32>(DequeuedChunk.WorkPriority),
static_cast<unsigned long long>(DequeuedChunk.RequestOrdinal),
DequeuedChunk.Epoch, GenerationEpoch,
DequeuedChunk.bAborted ? TEXT("aborted") : TEXT("obsolete")));
}
if (bRequestObsolete) if (bRequestObsolete)
{ {
++ObsoleteTileAbortCount; ++ObsoleteTileAbortCount;
@@ -9219,6 +9267,19 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
if (Result.bAborted || Result.Epoch != GenerationEpoch if (Result.bAborted || Result.Epoch != GenerationEpoch
|| !IsDesired(Result.Tile)) || !IsDesired(Result.Tile))
{ {
if (VoxelForgeStartupTrace::IsActive())
{
VoxelForgeStartupTrace::RecordEvent(TEXT("tile_discard"), FString::Printf(
TEXT("\"tile\":[%d,%d,%d],\"level\":%d,\"priority\":%d,"
"\"request_ordinal\":%llu,\"epoch\":%u,\"current_epoch\":%u,"
"\"reason\":\"%s\""),
Result.Tile.Coord.X, Result.Tile.Coord.Y, Result.Tile.Coord.Z,
Result.Tile.Level, static_cast<int32>(Result.WorkPriority),
static_cast<unsigned long long>(Result.RequestOrdinal),
Result.Epoch, GenerationEpoch,
Result.bAborted ? TEXT("aborted")
: (Result.Epoch != GenerationEpoch ? TEXT("stale_epoch") : TEXT("not_desired"))));
}
return false; return false;
} }
@@ -9352,6 +9413,10 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
Sample.TileY = Result.Tile.Coord.Y; Sample.TileY = Result.Tile.Coord.Y;
Sample.TileZ = Result.Tile.Coord.Z; Sample.TileZ = Result.Tile.Coord.Z;
Sample.Level = Result.Tile.Level; Sample.Level = Result.Tile.Level;
Sample.WorkPriority = static_cast<int32>(Result.WorkPriority);
Sample.RequestOrdinal = Result.RequestOrdinal;
Sample.GenerationOrdinal = Result.GenerationOrdinal;
Sample.bResidentAtRequest = Result.bResidentAtRequest;
Sample.Verdict = Result.ClassifyVerdict; Sample.Verdict = Result.ClassifyVerdict;
Sample.bSealedSolidProof = Result.bSealedSolidProof; Sample.bSealedSolidProof = Result.bSealedSolidProof;
Sample.bEmpty = bResultEmpty; Sample.bEmpty = bResultEmpty;
@@ -10041,6 +10106,14 @@ void AVoxelWorld::BuildCoreDesiredTiles(const FIntVector& Center, const FVector&
AddCriticalTile(NextTile); AddCriticalTile(NextTile);
} }
AddCriticalTile(PlayerTile); AddCriticalTile(PlayerTile);
// The local 3x3 XY collision ring is the walkable gate; admit it with the support tile.
// L'anneau de collision XY 3x3 forme le seuil praticable; il passe avec la tuile de support.
for (int32 Dy = -1; Dy <= 1; ++Dy)
for (int32 Dx = -1; Dx <= 1; ++Dx)
{
const FVoxelTileKey WalkableKey(PlayerTile.Coord + FIntVector(Dx, Dy, 0), 0);
if (CollisionDesiredTiles.Contains(WalkableKey)) AddCriticalTile(WalkableKey);
}
for (int32 Index = CriticalDesiredTiles.Num() - 1; Index >= 0; --Index) for (int32 Index = CriticalDesiredTiles.Num() - 1; Index >= 0; --Index)
{ {
const int32 ExistingIndex = DesiredSorted.Find(CriticalDesiredTiles[Index]); const int32 ExistingIndex = DesiredSorted.Find(CriticalDesiredTiles[Index]);
@@ -10398,6 +10471,14 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& Pla
AddCriticalTile(NextTile); AddCriticalTile(NextTile);
} }
AddCriticalTile(PlayerTile); AddCriticalTile(PlayerTile);
// Prioritize exactly the 3x3 walkable collision neighborhood around the pawn.
// Prioriser exactement le voisinage de collision praticable 3x3 autour du pion.
for (int32 Dy = -1; Dy <= 1; ++Dy)
for (int32 Dx = -1; Dx <= 1; ++Dx)
{
const FVoxelTileKey WalkableKey(PlayerTile.Coord + FIntVector(Dx, Dy, 0), 0);
if (CollisionDesiredTiles.Contains(WalkableKey)) AddCriticalTile(WalkableKey);
}
// Insert in reverse so CriticalDesiredTiles[0] remains the occupied floor, [1] the heading // Insert in reverse so CriticalDesiredTiles[0] remains the occupied floor, [1] the heading
// tile, and all ordinary distance-sorted work follows both. // tile, and all ordinary distance-sorted work follows both.
@@ -10717,6 +10798,12 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
const FVector& PlayerHeading) const FVector& PlayerHeading)
{ {
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
// Measure the scheduler update only in opt-in trace runs; keep the normal frame path unchanged.
// Mesurer l'ordonnanceur uniquement dans les traces opt-in ; garder le chemin normal sans surcout.
const bool bTraceStreamingScheduler = VoxelForgeStartupTrace::IsActive();
const double StreamingUpdateStartSeconds = bTraceStreamingScheduler
? FPlatformTime::Seconds() : 0.0;
double DesiredRebuildMilliseconds = 0.0;
if (bUseVoxelForgeCore) if (bUseVoxelForgeCore)
{ {
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState; const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
@@ -10749,7 +10836,54 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
} }
} }
const int32 MaxTasks = GetMaxConcurrentTasks(); const int32 MaxTasks = GetMaxConcurrentTasks();
const int32 NearTaskReserve = GetNearTaskReserve(MaxTasks); const int32 AuthoredNearTaskReserve = GetNearTaskReserve(MaxTasks);
const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition));
FVoxelTileKey WalkableSupportTile;
const FIntVector WalkableCenter = GetPlayerSupportTile(
PlayerPawn, CenterPosition, WalkableSupportTile)
? WalkableSupportTile.Coord : CenterChunk;
bool bWalkableRingReadyForReserve = true;
int32 WalkableRingReadyCount = 0;
for (int32 Dy = -1; Dy <= 1; ++Dy)
for (int32 Dx = -1; Dx <= 1; ++Dx)
{
const FVoxelTileKey WalkableKey(WalkableCenter + FIntVector(Dx, Dy, 0), 0);
if (CollisionReadyTiles.Contains(WalkableKey)
|| CollisionNotRequiredTiles.Contains(WalkableKey))
{
++WalkableRingReadyCount;
}
else
{
bWalkableRingReadyForReserve = false;
}
}
if (VoxelForgeStartupTrace::IsActive())
{
const bool bGateCenterChanged = !bHasLastWalkableGateSample
|| LastWalkableGateCenter != WalkableCenter;
const bool bGateStateChanged = !bHasLastWalkableGateSample
|| bLastWalkableGateReady != bWalkableRingReadyForReserve;
if (bGateCenterChanged || bGateStateChanged)
{
VoxelForgeStartupTrace::RecordEvent(TEXT("walkable_gate"), FString::Printf(
TEXT("\"support_center\":[%d,%d,%d],\"stream_center\":[%d,%d,%d],"
"\"ready\":%d,\"ready_tiles\":%d,\"required_tiles\":9,\"speed_cm_s\":%s"),
WalkableCenter.X, WalkableCenter.Y, WalkableCenter.Z,
CenterChunk.X, CenterChunk.Y, CenterChunk.Z,
bWalkableRingReadyForReserve ? 1 : 0, WalkableRingReadyCount,
*FString::SanitizeFloat(PlayerHeading.Size2D())));
bHasLastWalkableGateSample = true;
LastWalkableGateCenter = WalkableCenter;
bLastWalkableGateReady = bWalkableRingReadyForReserve;
}
}
const bool bPawnMoving = PlayerHeading.Size2D() >= 50.0f;
EffectiveNearTaskReserve = GVoxelForgeStreamingDynamicNearReserve == 0
? AuthoredNearTaskReserve
: ((bPawnMoving || !bWalkableRingReadyForReserve) ? AuthoredNearTaskReserve : 0);
const int32 NearTaskReserve = EffectiveNearTaskReserve;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve); const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve);
if (GVoxelForgeProfileTileGeneration != 0 if (GVoxelForgeProfileTileGeneration != 0
&& !GVoxelForgeStreamingBudgetReported) && !GVoxelForgeStreamingBudgetReported)
@@ -10763,7 +10897,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d " TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d " "authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
"step1_generation_cap=%d step1_max_mesh_applies=%d " "step1_generation_cap=%d step1_max_mesh_applies=%d "
"near_task_reserve=%d coarse_task_cap=%d " "near_task_reserve=%d dynamic_reserve=%d moving=%d walkable_ring_ready=%d "
"coarse_task_cap=%d coarse_first=%d "
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"), "max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
LogicalCores, LogicalCores,
AutomaticCap, AutomaticCap,
@@ -10773,14 +10908,17 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
GVoxelForgeStep1GenerationCap, GVoxelForgeStep1GenerationCap,
GVoxelForgeStep1MaxMeshApplies, GVoxelForgeStep1MaxMeshApplies,
NearTaskReserve, NearTaskReserve,
GVoxelForgeStreamingDynamicNearReserve,
bPawnMoving ? 1 : 0,
bWalkableRingReadyForReserve ? 1 : 0,
CoarseTaskCap, CoarseTaskCap,
GVoxelForgeStreamingCoarseFirst,
Settings ? Settings->MaxMeshAppliesPerFrame : 4, Settings ? Settings->MaxMeshAppliesPerFrame : 4,
Settings ? Settings->MaxMeshApplyMilliseconds : 2.0f, Settings ? Settings->MaxMeshApplyMilliseconds : 2.0f,
Settings ? Settings->MaxUnloadsPerFrame : 6); Settings ? Settings->MaxUnloadsPerFrame : 6);
GVoxelForgeStreamingBudgetReported = true; GVoxelForgeStreamingBudgetReported = true;
} }
const FIntVector CenterChunk = GetTileCoordAtLocalCm(WorldToLocalCm(CenterPosition));
CurrentCenterChunk = CenterChunk; CurrentCenterChunk = CenterChunk;
for (auto It = PreEditDensityCache.CreateIterator(); It; ++It) for (auto It = PreEditDensityCache.CreateIterator(); It; ++It)
{ {
@@ -10833,7 +10971,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
{ {
VoxelForgeStartupTrace::StageBegin(TEXT("BuildDesiredTiles.first")); VoxelForgeStartupTrace::StageBegin(TEXT("BuildDesiredTiles.first"));
} }
const double DesiredRebuildStartSeconds = bTraceStreamingScheduler
? FPlatformTime::Seconds() : 0.0;
BuildDesiredTiles(CenterChunk, CenterPosition, PlayerPawn, PlayerHeading, Leavers); BuildDesiredTiles(CenterChunk, CenterPosition, PlayerPawn, PlayerHeading, Leavers);
if (bTraceStreamingScheduler)
{
DesiredRebuildMilliseconds = (FPlatformTime::Seconds() - DesiredRebuildStartSeconds) * 1000.0;
}
if (bTraceFirstDesired) if (bTraceFirstDesired)
{ {
VoxelForgeStartupTrace::StageEnd(TEXT("BuildDesiredTiles.first")); VoxelForgeStartupTrace::StageEnd(TEXT("BuildDesiredTiles.first"));
@@ -11053,18 +11197,20 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
} }
//========================================================================= //=========================================================================
// Submit pending work (budgeted, collision-critical -> edits -> LOD0 -> ascending coarse // Submit pending work (collision-critical -> edits -> coarse shell -> fine detail).
// levels). Coarse work is capped below the full task budget so a later near request always // Soumettre le travail (collision critique -> modifications -> enveloppe grossiere -> detail fin).
// has reserved admission; far work still runs continuously in the remaining slots. // Coarse work is capped below the full task budget so later near requests keep admission.
// Le travail grossier reste sous le budget total pour reserver des admissions aux tuiles proches.
//========================================================================= //=========================================================================
if (!bAllChunksLoaded) if (!bAllChunksLoaded)
{ {
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles); TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles);
int32 Submitted = 0; int32 Submitted = 0;
// FLOOR RESPONSIVENESS — the occupied support tile and the next tile along the pawn heading // FLOOR RESPONSIVENESS — the support tile and collision-required 3x3 XY ring lead ordinary work.
// are an absolute prefix, ahead of dirty remeshes, band work, and ordinary clipmap work. // PRIORITE AU SOL — la tuile de support et l'anneau XY 3x3 requis pour la collision passent avant le reste.
// They run at BackgroundHigh so the pawn's floor gets the first available worker. // They run at BackgroundHigh so the pawn's floor gets the first available worker.
// Ils passent en BackgroundHigh afin que le sol du pion obtienne les premiers workers disponibles.
for (const FVoxelTileKey& T : CriticalDesiredTiles) for (const FVoxelTileKey& T : CriticalDesiredTiles)
{ {
if (GetActiveGenerationTaskCount() >= MaxTasks) break; if (GetActiveGenerationTaskCount() >= MaxTasks) break;
@@ -11086,22 +11232,6 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
++Submitted; ++Submitted;
} }
// NEAR TERRAIN — all ordinary level-0 work is promoted after the absolute support/edit
// prefixes. It uses the near queue and BackgroundHigh so both worker execution and result
// application stay ahead of a completed horizon backlog.
for (const FVoxelTileKey& T : DesiredSorted)
{
if (T.Level != 0) continue;
if (GetActiveGenerationTaskCount() >= MaxTasks) break;
if (PendingTiles.Contains(T)) continue; // in flight
if (LoadedTiles.Contains(T)) continue; // already loaded (level is in the key — no LOD remesh)
LoadTile(T, EVoxelTileWorkPriority::LOD0);
++Submitted;
}
// FAR FIELD — preserve the clipmap's level ordering explicitly. DesiredSorted is
// distance-sorted for the critical/near work, but a level pass prevents a level-5 horizon
// tile from being admitted ahead of an unfinished level-1/2 bridge tile.
const uint64 FarFieldMeasureStartCycles = bMeasureReadyTransitionVisibility const uint64 FarFieldMeasureStartCycles = bMeasureReadyTransitionVisibility
? FPlatformTime::Cycles64() : 0; ? FPlatformTime::Cycles64() : 0;
int32 MaxDesiredLevel = 0; int32 MaxDesiredLevel = 0;
@@ -11109,18 +11239,99 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
{ {
MaxDesiredLevel = FMath::Max(MaxDesiredLevel, T.Level); MaxDesiredLevel = FMath::Max(MaxDesiredLevel, T.Level);
} }
for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level)
auto SubmitLevel = [&](int32 Level, EVoxelTileWorkPriority Priority, int32 AdmissionCap,
bool bPreferTravelDirection)
{ {
if (GetActiveGenerationTaskCount() >= AdmissionCap)
{
return;
}
TArray<FVoxelTileKey> LevelTiles;
for (const FVoxelTileKey& T : DesiredSorted) for (const FVoxelTileKey& T : DesiredSorted)
{ {
if (T.Level != Level) continue; if (T.Level == Level && !PendingTiles.Contains(T) && !LoadedTiles.Contains(T))
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break; {
if (PendingTiles.Contains(T)) continue; // in flight LevelTiles.Add(T);
if (LoadedTiles.Contains(T)) continue; // already loaded }
LoadTile(T); }
// Sort only outstanding keys; re-sorting loaded or pending tiles on every tick inflated game-thread work.
// Trier uniquement les cles restantes ; re-trier chaque tick les tuiles chargees ou en attente alourdissait le thread de jeu.
if (bPreferTravelDirection && LevelTiles.Num() > 1)
{
FVector Heading = GetActorTransform().InverseTransformVectorNoScale(PlayerHeading);
if (Heading.IsNearlyZero() && IsValid(PlayerPawn))
{
Heading = GetActorTransform().InverseTransformVectorNoScale(
PlayerPawn->GetActorForwardVector());
}
Heading = Heading.GetSafeNormal();
const FVector PlayerLocalCm = WorldToLocalCm(CenterPosition);
const double TileSizeCm = static_cast<double>(CHUNK_SIZE << Level)
* GetActiveVoxelSizeCm();
LevelTiles.Sort([&](const FVoxelTileKey& A, const FVoxelTileKey& B)
{
const FVector CenterA(
(static_cast<double>(A.Coord.X) + 0.5) * TileSizeCm,
(static_cast<double>(A.Coord.Y) + 0.5) * TileSizeCm,
(static_cast<double>(A.Coord.Z) + 0.5) * TileSizeCm);
const FVector CenterB(
(static_cast<double>(B.Coord.X) + 0.5) * TileSizeCm,
(static_cast<double>(B.Coord.Y) + 0.5) * TileSizeCm,
(static_cast<double>(B.Coord.Z) + 0.5) * TileSizeCm);
const FVector OffsetA = CenterA - PlayerLocalCm;
const FVector OffsetB = CenterB - PlayerLocalCm;
const bool bAheadA = !Heading.IsNearlyZero() && FVector::DotProduct(OffsetA, Heading) > 0.0;
const bool bAheadB = !Heading.IsNearlyZero() && FVector::DotProduct(OffsetB, Heading) > 0.0;
if (bAheadA != bAheadB) return bAheadA;
const double DistanceA = OffsetA.SizeSquared();
const double DistanceB = OffsetB.SizeSquared();
if (DistanceA != DistanceB) return DistanceA < DistanceB;
if (A.Coord.X != B.Coord.X) return A.Coord.X < B.Coord.X;
if (A.Coord.Y != B.Coord.Y) return A.Coord.Y < B.Coord.Y;
return A.Coord.Z < B.Coord.Z;
});
}
for (const FVoxelTileKey& T : LevelTiles)
{
if (GetActiveGenerationTaskCount() >= AdmissionCap) break;
if (PendingTiles.Contains(T)) continue;
if (LoadedTiles.Contains(T)) continue;
LoadTile(T, Priority);
++Submitted; ++Submitted;
} }
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break; };
// Coarse-first gives the far shell the first non-critical admissions. Keep the current
// collision-ready 3x3 ring above it, then spend the remaining slots on full-resolution
// refinement. The opt-out reproduces the S4 LOD0-to-LOD8 submission order for a control.
// Le shell lointain passe apres l'anneau de collision, puis le raffinement fin. L'option
// inverse restaure l'ordre LOD0-vers-LOD8 de S4 comme controle negatif.
if (GVoxelForgeStreamingCoarseFirst != 0)
{
const int32 FirstCoarseLevel = FMath::Max(
2, Settings ? Settings->FullResClipLevels : 2);
for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level)
{
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
CoarseTaskCap, true);
}
for (int32 Level = 0; Level < FirstCoarseLevel && Level <= MaxDesiredLevel; ++Level)
{
const EVoxelTileWorkPriority Priority = Level == 0
? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon;
SubmitLevel(Level, Priority, MaxTasks, true);
}
}
else
{
SubmitLevel(0, EVoxelTileWorkPriority::LOD0, MaxTasks, false);
for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level)
{
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
CoarseTaskCap, false);
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break;
}
} }
if (bMeasureReadyTransitionVisibility) if (bMeasureReadyTransitionVisibility)
{ {
@@ -11175,6 +11386,60 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
bAllChunksLoaded = true; bAllChunksLoaded = true;
} }
} }
if (VoxelForgeStartupTrace::IsActive())
{
const double TraceNow = FPlatformTime::Seconds();
if (TraceNow - LastStreamingTraceSampleSeconds >= 0.25)
{
int32 DesiredByLevel[9] = {};
int32 ResidentByLevel[9] = {};
int32 PendingByLevel[9] = {};
for (const FVoxelTileKey& Tile : DesiredSorted)
{
if (Tile.Level < 0 || Tile.Level > 8) continue;
++DesiredByLevel[Tile.Level];
if (LoadedTiles.Contains(Tile)) ++ResidentByLevel[Tile.Level];
if (PendingTiles.Contains(Tile)) ++PendingByLevel[Tile.Level];
}
FString DesiredCounts = TEXT("[");
FString ResidentCounts = TEXT("[");
FString PendingCounts = TEXT("[");
for (int32 Level = 0; Level <= 8; ++Level)
{
if (Level > 0)
{
DesiredCounts += TEXT(",");
ResidentCounts += TEXT(",");
PendingCounts += TEXT(",");
}
DesiredCounts += FString::FromInt(DesiredByLevel[Level]);
ResidentCounts += FString::FromInt(ResidentByLevel[Level]);
PendingCounts += FString::FromInt(PendingByLevel[Level]);
}
DesiredCounts += TEXT("]");
ResidentCounts += TEXT("]");
PendingCounts += TEXT("]");
const FVector LocalPawn = WorldToLocalCm(CenterPosition);
const double StreamingUpdateMilliseconds = (TraceNow - StreamingUpdateStartSeconds) * 1000.0;
VoxelForgeStartupTrace::RecordEvent(TEXT("streaming_sample"), FString::Printf(
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d,"
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"pending_by_lod\":%s,"
"\"active_generation\":%d,\"task_cap\":%d,\"near_reserve\":%d,\"epoch\":%u,"
"\"update_chunks_ms\":%s,\"desired_rebuild_ms\":%s"),
CenterChunk.X, CenterChunk.Y, CenterChunk.Z,
*FString::SanitizeFloat(LocalPawn.X), *FString::SanitizeFloat(LocalPawn.Y),
*FString::SanitizeFloat(LocalPawn.Z),
*FString::SanitizeFloat(PlayerHeading.Size2D()),
bWalkableRingReadyForReserve ? 1 : 0,
*DesiredCounts, *ResidentCounts, *PendingCounts,
GetActiveGenerationTaskCount(), MaxTasks, NearTaskReserve, GenerationEpoch,
*FString::SanitizeFloat(StreamingUpdateMilliseconds),
*FString::SanitizeFloat(DesiredRebuildMilliseconds)));
LastStreamingTraceSampleSeconds = TraceNow;
}
}
} }
void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority) void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority WorkPriority)
@@ -11189,7 +11454,10 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
const int32 MaxTasks = GetMaxConcurrentTasks(); // T2.d — core-clamped const int32 MaxTasks = GetMaxConcurrentTasks(); // T2.d — core-clamped
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - GetNearTaskReserve(MaxTasks)); const int32 NearReserve = EffectiveNearTaskReserve >= 0
? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1))
: GetNearTaskReserve(MaxTasks);
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
? CoarseTaskCap : MaxTasks; ? CoarseTaskCap : MaxTasks;
if (GetActiveGenerationTaskCount() >= AdmissionCap) if (GetActiveGenerationTaskCount() >= AdmissionCap)
@@ -11200,12 +11468,26 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
UE_LOG(LogTemp, Display, UE_LOG(LogTemp, Display,
TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d admission=%d " TEXT("[VoxelForgeStreamingBudget] generation_cap_hit=1 pending=%d admission=%d "
"effective=%d near_reserve=%d tile_level=%d"), "effective=%d near_reserve=%d tile_level=%d"),
PendingTiles.Num(), AdmissionCap, MaxTasks, GetNearTaskReserve(MaxTasks), Tile.Level); PendingTiles.Num(), AdmissionCap, MaxTasks, NearReserve, Tile.Level);
GVoxelForgeGenerationCapHitReported = true; GVoxelForgeGenerationCapHitReported = true;
} }
return; // Budget full — wait for a task to finish. return; // Budget full — wait for a task to finish.
} }
PendingTiles.Add(Tile); PendingTiles.Add(Tile);
const uint64 RequestOrdinal = ++NextStreamingRequestOrdinal;
const bool bResidentAtRequest = LoadedTiles.Contains(Tile);
if (VoxelForgeStartupTrace::IsActive())
{
VoxelForgeStartupTrace::FTileSample RequestSample;
RequestSample.TileX = Tile.Coord.X;
RequestSample.TileY = Tile.Coord.Y;
RequestSample.TileZ = Tile.Coord.Z;
RequestSample.Level = Tile.Level;
RequestSample.WorkPriority = static_cast<int32>(WorkPriority);
RequestSample.RequestOrdinal = RequestOrdinal;
RequestSample.bResidentAtRequest = bResidentAtRequest;
VoxelForgeStartupTrace::RecordRequest(RequestSample, GenerationEpoch);
}
if (bStartupCollisionGateTimingCaptured if (bStartupCollisionGateTimingCaptured
&& StartupCollisionGateSupportTile.Coord == Tile.Coord && StartupCollisionGateSupportTile.Coord == Tile.Coord
&& StartupCollisionGateSupportTile.Level == Tile.Level && StartupCollisionGateSupportTile.Level == Tile.Level
@@ -11297,6 +11579,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
Empty.DesiredEpoch = TaskDesiredEpoch; Empty.DesiredEpoch = TaskDesiredEpoch;
Empty.ModificationEpoch = TaskModificationEpoch; Empty.ModificationEpoch = TaskModificationEpoch;
Empty.RequestStartCycles = RequestStartCycles; Empty.RequestStartCycles = RequestStartCycles;
Empty.RequestOrdinal = RequestOrdinal;
Empty.bResidentAtRequest = bResidentAtRequest;
Empty.BandChunkLo = BandChunkLo; Empty.BandChunkLo = BandChunkLo;
Empty.BandChunkHi = BandChunkHi; Empty.BandChunkHi = BandChunkHi;
Empty.WorkPriority = WorkPriority; Empty.WorkPriority = WorkPriority;
@@ -11332,7 +11616,8 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch,
bWantCapture, bWantPreEditDensityGrid, bWantSurfaceWitnesses, bWantCapture, bWantPreEditDensityGrid, bWantSurfaceWitnesses,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
RequestStartCycles, TaskDesiredEpoch, TaskModificationEpoch, Cancellation, RequestStartCycles, RequestOrdinal, bResidentAtRequest,
TaskDesiredEpoch, TaskModificationEpoch, Cancellation,
WorkPriority, bStep1SupportCohort]() WorkPriority, bStep1SupportCohort]()
{ {
// RAII: decrement the counter on every exit path. // RAII: decrement the counter on every exit path.
@@ -11342,6 +11627,11 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); } ~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
} Guard{ActiveTaskCount}; } Guard{ActiveTaskCount};
FChunkResult Result;
Result.RequestOrdinal = RequestOrdinal;
Result.bResidentAtRequest = bResidentAtRequest;
Result.WorkPriority = WorkPriority;
auto EnqueueObsoleteResult = [&]() auto EnqueueObsoleteResult = [&]()
{ {
if (bShuttingDown.load(std::memory_order_relaxed) if (bShuttingDown.load(std::memory_order_relaxed)
@@ -11355,6 +11645,11 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
Canceled.DesiredEpoch = TaskDesiredEpoch; Canceled.DesiredEpoch = TaskDesiredEpoch;
Canceled.ModificationEpoch = TaskModificationEpoch; Canceled.ModificationEpoch = TaskModificationEpoch;
Canceled.RequestStartCycles = RequestStartCycles; Canceled.RequestStartCycles = RequestStartCycles;
Canceled.RequestOrdinal = RequestOrdinal;
Canceled.GenerationOrdinal = Result.GenerationOrdinal;
Canceled.GenerationStartCycles = Result.GenerationStartCycles;
Canceled.GenerationEndCycles = FPlatformTime::Cycles64();
Canceled.bResidentAtRequest = bResidentAtRequest;
Canceled.WorkPriority = WorkPriority; Canceled.WorkPriority = WorkPriority;
Canceled.bStep1SupportCohort = bStep1SupportCohort; Canceled.bStep1SupportCohort = bStep1SupportCohort;
Canceled.bAborted = true; Canceled.bAborted = true;
@@ -11377,13 +11672,15 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
} }
FScopedVoxelStackRegistration StackRegistration(Tile.Level); FScopedVoxelStackRegistration StackRegistration(Tile.Level);
FChunkResult Result;
Result.DesiredEpoch = TaskDesiredEpoch; Result.DesiredEpoch = TaskDesiredEpoch;
Result.ModificationEpoch = TaskModificationEpoch; Result.ModificationEpoch = TaskModificationEpoch;
Result.WorkPriority = WorkPriority; Result.WorkPriority = WorkPriority;
Result.RequestOrdinal = RequestOrdinal;
Result.bResidentAtRequest = bResidentAtRequest;
Result.bStep1SupportCohort = bStep1SupportCohort; Result.bStep1SupportCohort = bStep1SupportCohort;
Result.bCaptureSurfaceWitnesses = bWantSurfaceWitnesses; Result.bCaptureSurfaceWitnesses = bWantSurfaceWitnesses;
const uint64 GenerationStartCycles = FPlatformTime::Cycles64(); const uint64 GenerationStartCycles = FPlatformTime::Cycles64();
Result.GenerationOrdinal = NextStreamingGenerationOrdinal.fetch_add(1, std::memory_order_relaxed) + 1;
Result.GenerationStartCycles = GenerationStartCycles; Result.GenerationStartCycles = GenerationStartCycles;
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture, GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi, BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
@@ -11447,7 +11744,10 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
const int32 MaxTasks = GetMaxConcurrentTasks(); const int32 MaxTasks = GetMaxConcurrentTasks();
const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon; const bool bNearPriority = WorkPriority != EVoxelTileWorkPriority::Horizon;
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - GetNearTaskReserve(MaxTasks)); const int32 NearReserve = EffectiveNearTaskReserve >= 0
? FMath::Clamp(EffectiveNearTaskReserve, 0, FMath::Max(0, MaxTasks - 1))
: GetNearTaskReserve(MaxTasks);
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
? CoarseTaskCap : MaxTasks; ? CoarseTaskCap : MaxTasks;
if (GetActiveGenerationTaskCount() >= AdmissionCap) return; if (GetActiveGenerationTaskCount() >= AdmissionCap) return;
@@ -11462,13 +11762,28 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
const uint32 TaskDesiredEpoch = DesiredEpoch; const uint32 TaskDesiredEpoch = DesiredEpoch;
const uint32 TaskModificationEpoch = ModificationEpoch; const uint32 TaskModificationEpoch = ModificationEpoch;
const uint64 RequestStartCycles = FPlatformTime::Cycles64(); const uint64 RequestStartCycles = FPlatformTime::Cycles64();
const uint64 RequestOrdinal = ++NextStreamingRequestOrdinal;
const bool bResidentAtRequest = LoadedTiles.Contains(Tile);
if (VoxelForgeStartupTrace::IsActive())
{
VoxelForgeStartupTrace::FTileSample RequestSample;
RequestSample.TileX = Tile.Coord.X;
RequestSample.TileY = Tile.Coord.Y;
RequestSample.TileZ = Tile.Coord.Z;
RequestSample.Level = Tile.Level;
RequestSample.WorkPriority = static_cast<int32>(WorkPriority);
RequestSample.RequestOrdinal = RequestOrdinal;
RequestSample.bResidentAtRequest = bResidentAtRequest;
VoxelForgeStartupTrace::RecordRequest(RequestSample, TaskEpoch);
}
const bool bCaptureHashes = bCaptureVoxelForgeCoreHashesForValidation; const bool bCaptureHashes = bCaptureVoxelForgeCoreHashesForValidation;
const UE::Tasks::ETaskPriority TaskPriority = bNearPriority const UE::Tasks::ETaskPriority TaskPriority = bNearPriority
? UE::Tasks::ETaskPriority::BackgroundHigh ? UE::Tasks::ETaskPriority::BackgroundHigh
: UE::Tasks::ETaskPriority::BackgroundNormal; : UE::Tasks::ETaskPriority::BackgroundNormal;
UE::Tasks::Launch(TEXT("VoxelForgeCoreTile"), UE::Tasks::Launch(TEXT("VoxelForgeCoreTile"),
[this, State, Cancellation, Tile, WorkPriority, TaskEpoch, TaskDesiredEpoch, [this, State, Cancellation, Tile, WorkPriority, TaskEpoch, TaskDesiredEpoch,
TaskModificationEpoch, RequestStartCycles, bCaptureHashes]() TaskModificationEpoch, RequestStartCycles, RequestOrdinal, bResidentAtRequest,
bCaptureHashes]()
{ {
struct FTaskGuard struct FTaskGuard
{ {
@@ -11500,8 +11815,11 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
Result.DesiredEpoch = TaskDesiredEpoch; Result.DesiredEpoch = TaskDesiredEpoch;
Result.ModificationEpoch = TaskModificationEpoch; Result.ModificationEpoch = TaskModificationEpoch;
Result.RequestStartCycles = RequestStartCycles; Result.RequestStartCycles = RequestStartCycles;
Result.RequestOrdinal = RequestOrdinal;
Result.bResidentAtRequest = bResidentAtRequest;
Result.WorkPriority = WorkPriority; Result.WorkPriority = WorkPriority;
Result.GenerationStartCycles = FPlatformTime::Cycles64(); Result.GenerationStartCycles = FPlatformTime::Cycles64();
Result.GenerationOrdinal = NextStreamingGenerationOrdinal.fetch_add(1, std::memory_order_relaxed) + 1;
auto EnqueueObsolete = [&]() auto EnqueueObsolete = [&]()
{ {
@@ -11511,6 +11829,9 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
Canceled.DesiredEpoch = TaskDesiredEpoch; Canceled.DesiredEpoch = TaskDesiredEpoch;
Canceled.ModificationEpoch = TaskModificationEpoch; Canceled.ModificationEpoch = TaskModificationEpoch;
Canceled.RequestStartCycles = RequestStartCycles; Canceled.RequestStartCycles = RequestStartCycles;
Canceled.RequestOrdinal = RequestOrdinal;
Canceled.GenerationOrdinal = Result.GenerationOrdinal;
Canceled.bResidentAtRequest = bResidentAtRequest;
Canceled.GenerationStartCycles = Result.GenerationStartCycles; Canceled.GenerationStartCycles = Result.GenerationStartCycles;
Canceled.GenerationEndCycles = FPlatformTime::Cycles64(); Canceled.GenerationEndCycles = FPlatformTime::Cycles64();
Canceled.WorkPriority = WorkPriority; Canceled.WorkPriority = WorkPriority;
@@ -13,7 +13,11 @@ namespace VoxelForgeStartupTrace
int32 TileY = 0; int32 TileY = 0;
int32 TileZ = 0; int32 TileZ = 0;
int32 Level = -1; int32 Level = -1;
int32 WorkPriority = 0;
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (early path) int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (early path)
uint64 RequestOrdinal = 0;
uint64 GenerationOrdinal = 0;
bool bResidentAtRequest = false;
bool bSealedSolidProof = false; bool bSealedSolidProof = false;
bool bEmpty = true; bool bEmpty = true;
int32 Vertices = 0; int32 Vertices = 0;
@@ -40,6 +44,8 @@ namespace VoxelForgeStartupTrace
/** FieldsJson is the contents after the type/timestamp object prefix, without braces. */ /** FieldsJson is the contents after the type/timestamp object prefix, without braces. */
VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString()); VOXELFORGE_API void RecordEvent(const TCHAR* Type, const FString& FieldsJson = FString());
VOXELFORGE_API void RecordRequest(const FTileSample& Sample, uint32 Epoch);
VOXELFORGE_API void StageBegin(const TCHAR* Name); VOXELFORGE_API void StageBegin(const TCHAR* Name);
VOXELFORGE_API void StageEnd(const TCHAR* Name); VOXELFORGE_API void StageEnd(const TCHAR* Name);
+13
View File
@@ -139,6 +139,11 @@ struct FChunkResult
uint64 GenerationEndCycles = 0; uint64 GenerationEndCycles = 0;
uint64 ApplyStartCycles = 0; uint64 ApplyStartCycles = 0;
EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon; EVoxelTileWorkPriority WorkPriority = EVoxelTileWorkPriority::Horizon;
// Ordinals make admitted order and actual worker start order auditable across MPSC results.
// Les numeros d'ordre rendent auditables l'admission et le demarrage des workers via MPSC.
uint64 RequestOrdinal = 0;
uint64 GenerationOrdinal = 0;
bool bResidentAtRequest = false;
// Step-1-only tag captured on the game thread for the pawn's bounded support cohort. // Step-1-only tag captured on the game thread for the pawn's bounded support cohort.
bool bStep1SupportCohort = false; bool bStep1SupportCohort = false;
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0; int32 NumSkirtTriangles = 0; int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0; int32 NumCeilingTriangles = 0; int32 NumSkirtTriangles = 0;
@@ -1179,6 +1184,9 @@ public:
uint32 DesiredEpoch = 0; // increments whenever DesiredStamped is rebuilt uint32 DesiredEpoch = 0; // increments whenever DesiredStamped is rebuilt
int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count
float PeakObservedPawnSpeedCmPerSecond = 0.0f; float PeakObservedPawnSpeedCmPerSecond = 0.0f;
int32 EffectiveNearTaskReserve = -1;
uint64 NextStreamingRequestOrdinal = 0;
std::atomic<uint64> NextStreamingGenerationOrdinal{0};
struct FReadyTransitionVisibilitySample struct FReadyTransitionVisibilitySample
{ {
@@ -1257,6 +1265,11 @@ public:
bool bHeadlessStreamingTestExitOnSteady = false; bool bHeadlessStreamingTestExitOnSteady = false;
bool bHeadlessStreamingTestExitRequested = false; bool bHeadlessStreamingTestExitRequested = false;
bool bStartupTraceThroughCrossing = false; bool bStartupTraceThroughCrossing = false;
bool bStartupTraceUntilEndPlay = false;
bool bHasLastWalkableGateSample = false;
bool bLastWalkableGateReady = false;
FIntVector LastWalkableGateCenter = FIntVector::ZeroValue;
double LastStreamingTraceSampleSeconds = 0.0;
bool bHeadlessStreamingTestCenterOverride = false; bool bHeadlessStreamingTestCenterOverride = false;
FVector HeadlessStreamingTestCenterVoxel = FVector::ZeroVector; FVector HeadlessStreamingTestCenterVoxel = FVector::ZeroVector;
int32 HeadlessStreamingTestMoveAttempts = 0; int32 HeadlessStreamingTestMoveAttempts = 0;
+29 -2
View File
@@ -2,14 +2,14 @@
param( param(
[string]$OutputName = 'VoxelForgeCoreStandalone.exe', [string]$OutputName = 'VoxelForgeCoreStandalone.exe',
[ValidateSet('All','Core')][string]$Target = 'All', [ValidateSet('All','Core')][string]$Target = 'All',
[ValidateRange(1,16)][int]$CompileWorkers = 16, [ValidateRange(1,12)][int]$CompileWorkers = 12,
[ValidateSet('Normal','BelowNormal')][string]$BuildPriority = 'Normal' [ValidateSet('Normal','BelowNormal')][string]$BuildPriority = 'Normal'
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$BuildHostProcess = Get-Process -Id $PID $BuildHostProcess = Get-Process -Id $PID
$BuildHostProcess.PriorityClass = $BuildPriority $BuildHostProcess.PriorityClass = $BuildPriority
$BuildHostProcess.ProcessorAffinity = [IntPtr]0xFFFF $BuildHostProcess.ProcessorAffinity = [IntPtr]0xFFF
$Root = [IO.Path]::GetFullPath($PSScriptRoot) $Root = [IO.Path]::GetFullPath($PSScriptRoot)
$BuildDir = Join-Path $Root 'Build\VoxelForgeCore' $BuildDir = Join-Path $Root 'Build\VoxelForgeCore'
$BinDir = Join-Path $Root 'bin' $BinDir = Join-Path $Root 'bin'
@@ -24,6 +24,33 @@ $CoreLibrary = Join-Path $BuildDir 'VoxelForgeCore.lib'
$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat' $VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat'
if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC environment script not found: $VcVars" } if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC environment script not found: $VcVars" }
if (-not ('VoxelForgeStandaloneBuildMemoryProbe' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class VoxelForgeStandaloneBuildMemoryProbe {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct MEMORYSTATUSEX {
public uint dwLength, dwMemoryLoad;
public ulong ullTotalPhys, ullAvailPhys, ullTotalPageFile, ullAvailPageFile;
public ulong ullTotalVirtual, ullAvailVirtual, ullAvailExtendedVirtual;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX status);
public static ulong AvailablePhysicalBytes() {
var status = new MEMORYSTATUSEX();
status.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
if (!GlobalMemoryStatusEx(ref status)) throw new InvalidOperationException("GlobalMemoryStatusEx failed.");
return status.ullAvailPhys;
}
}
'@
}
while ([VoxelForgeStandaloneBuildMemoryProbe]::AvailablePhysicalBytes() -lt 4GB) {
Write-Warning 'Standalone build is waiting for at least 4 GiB of free physical memory.'
Start-Sleep -Seconds 20
}
$SourceManifest = Join-Path $Root 'Core\core_sources.txt' $SourceManifest = Join-Path $Root 'Core\core_sources.txt'
if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) { throw "Core source manifest not found: $SourceManifest" } if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) { throw "Core source manifest not found: $SourceManifest" }
$CoreSourceNames = @(Get-Content -LiteralPath $SourceManifest | ForEach-Object { $_.Trim() } | $CoreSourceNames = @(Get-Content -LiteralPath $SourceManifest | ForEach-Object { $_.Trim() } |
@@ -1237,3 +1237,31 @@ occurred. I did not open the owner's editor, build his project or edit assets, a
push or stash. push or stash.
— Luna — Luna
## World assembler S5 / M3 Round 9 — 2026-10-19
I separated walkable collision, rendered enclosure, and per-LOD refinement rather than using the
moving full-clipmap resident count as a gate. The old standalone order queued and started LOD0 first;
the owner-camera trace shows LOD8 queue-wait p95 at 16.290 s and LOD8 build p95 at 365.8 ms. A
five-second 60 m/s capture crossed 37 centers and logged 5,006 new requests / 3,211 queued cancels.
This establishes queue churn, but not a visual-gap duration or proof that the Unreal actor starves.
The source candidate gives the shared actor scheduler a collision-ring → coarse-shell → LOD0/1
refinement order, retains the authored near reserve while moving, and adds request/start ordinals and
discard tracing. Standalone mirrors the order and has a legacy-order control. I did not add an LOD8
placeholder because initial overlapping parents do not have an all-descendants swap gate. The loaded
full-view capture still shows a large sky region; do not call enclosure passed from tile counts.
The S4-to-current standalone timing mismatch is unresolved: both report 2,553 tiles, but the current
cold CPU run is 16.685 s versus Round 8's 3.107 s. The default-camera first run built only 2,226
tiles and was rerun with the owner camera. Spawn samples through 2.0 s and the 60 m/s mid-flight
sample are black. No candidate BuildHost, after captures, negative-control run, or S5 hash matrix is
available yet.
I stopped before compiling because plugin-root `CLAUDE.md` §#1 explicitly says never build/compile
and to hand off at “ready to build.” The report lists the exact outstanding checks, compile spots,
and every failed or corrected run in [WorldAssemblerM3/Round9/REPORT.md](../../WorldAssemblerM3/Round9/REPORT.md).
No owner editor/project or asset was touched; no commit, push or stash was made.
— Luna
@@ -0,0 +1,175 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Label,
[Parameter(Mandatory = $true)][string[]]$ViewerArguments,
[ValidateSet('Spawn', 'Flight')][string]$CaptureMode = 'Spawn',
[switch]$WaitForFlightDrain
)
# Capture through the viewer's D3D back buffer; no desktop or window capture is used.
# Capturer le tampon D3D du viewer; aucune capture du bureau ou de la fenetre n'est utilisee.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../../..'))
$ViewerExe = Join-Path $PluginRoot 'Standalone/bin/VoxelForgeCoreStandalone.exe'
$CaptureRoot = Join-Path $PSScriptRoot 'captures'
$LogRoot = Join-Path $PSScriptRoot 'viewer_logs'
[IO.Directory]::CreateDirectory($CaptureRoot) | Out-Null
[IO.Directory]::CreateDirectory($LogRoot) | Out-Null
if (-not ('VoxelForgeR9MemoryProbe' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class VoxelForgeR9MemoryProbe {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct MEMORYSTATUSEX {
public uint dwLength, dwMemoryLoad;
public ulong ullTotalPhys, ullAvailPhys, ullTotalPageFile, ullAvailPageFile;
public ulong ullTotalVirtual, ullAvailVirtual, ullAvailExtendedVirtual;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX status);
public static ulong AvailablePhysicalBytes() {
var status = new MEMORYSTATUSEX();
status.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
if (!GlobalMemoryStatusEx(ref status)) throw new InvalidOperationException("GlobalMemoryStatusEx failed.");
return status.ullAvailPhys;
}
}
'@
}
if ([VoxelForgeR9MemoryProbe]::AvailablePhysicalBytes() -lt 4GB) {
throw 'Viewer launch skipped: less than 4 GiB of free physical memory.'
}
if (Get-Process UnrealEditor* -ErrorAction SilentlyContinue) {
throw 'Viewer launch skipped: an UnrealEditor process is still running.'
}
if (-not (Test-Path -LiteralPath $ViewerExe -PathType Leaf)) {
throw "Standalone viewer is missing: $ViewerExe"
}
Add-Type -AssemblyName System.Drawing
$SafeLabel = $Label -replace '[^A-Za-z0-9_-]', '_'
$SequenceSuffix = if ($CaptureMode -eq 'Spawn') { 'spawn' } else { 'mid_sprint' }
$CapturePrefix = Join-Path $CaptureRoot "${SafeLabel}_${SequenceSuffix}"
$RunArguments = [Collections.Generic.List[string]]::new()
foreach ($Argument in $ViewerArguments) { $RunArguments.Add($Argument) }
if ($CaptureMode -eq 'Spawn') {
$RunArguments.Add("--capture-spawn-sequence-prefix=$CapturePrefix")
$RunArguments.Add('--quit-after-spawn-captures')
$ExpectedIndices = @(0, 1, 2)
$TimeLimitSeconds = 60
} else {
$RunArguments.Add("--capture-flight-sequence-prefix=$CapturePrefix")
if (-not $WaitForFlightDrain) { $RunArguments.Add('--flight-capture-close-index=1') }
$ExpectedIndices = @(1)
$TimeLimitSeconds = if ($WaitForFlightDrain) { 900 } else { 45 }
}
$StdoutPath = Join-Path $LogRoot "$SafeLabel`_$SequenceSuffix.stdout.log"
$StderrPath = Join-Path $LogRoot "$SafeLabel`_$SequenceSuffix.stderr.log"
$QuotedArguments = @($RunArguments | ForEach-Object { '"' + $_.Replace('"', '\"') + '"' })
$ArgumentLine = $QuotedArguments -join ' '
$Process = Start-Process -FilePath $ViewerExe -ArgumentList $ArgumentLine `
-WorkingDirectory (Split-Path -Parent $ViewerExe) -PassThru -WindowStyle Normal `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
$Started = [Diagnostics.Stopwatch]::StartNew()
try {
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
$Process.ProcessorAffinity = [IntPtr]0xFFF
while ($true) {
$Process.Refresh()
if ($Process.HasExited) { break }
if ($Process.PrivateMemorySize64 -gt 8GB) {
throw "Owned viewer PID $($Process.Id) exceeded the 8 GiB private-memory limit."
}
if ($Started.Elapsed.TotalSeconds -gt $TimeLimitSeconds) {
throw "Owned viewer PID $($Process.Id) exceeded the $TimeLimitSeconds-second capture limit."
}
Start-Sleep -Milliseconds 200
}
$Process.WaitForExit()
$Process.Refresh()
if ($Process.ExitCode -ne 0) {
throw "Owned viewer PID $($Process.Id) exited with code $($Process.ExitCode)."
}
} catch {
$Process.Refresh()
if (-not $Process.HasExited) {
[void]$Process.CloseMainWindow()
if (-not $Process.WaitForExit(3000)) {
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
$Process.WaitForExit()
}
}
throw
}
function Save-DownscaledJpeg([string]$Source, [string]$Destination, [long]$Quality) {
$Original = [Drawing.Image]::FromFile($Source)
try {
$Width = [Math]::Min(960, $Original.Width)
$Height = [int][Math]::Round($Original.Height * $Width / $Original.Width)
$Small = New-Object Drawing.Bitmap($Width, $Height)
$Graphics = [Drawing.Graphics]::FromImage($Small)
try {
$Graphics.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$Graphics.PixelOffsetMode = [Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$Graphics.DrawImage($Original, 0, 0, $Width, $Height)
$Codec = [Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
Where-Object { $_.MimeType -eq 'image/jpeg' } | Select-Object -First 1
$Parameters = New-Object Drawing.Imaging.EncoderParameters(1)
try {
$Parameters.Param[0] = New-Object Drawing.Imaging.EncoderParameter(
[Drawing.Imaging.Encoder]::Quality, $Quality)
$Small.Save($Destination, $Codec, $Parameters)
} finally {
$Parameters.Dispose()
}
} finally {
$Graphics.Dispose()
$Small.Dispose()
}
} finally {
$Original.Dispose()
}
}
$ViewerLog = Get-Content -LiteralPath $StdoutPath -Raw
foreach ($Index in $ExpectedIndices) {
$PngPath = '{0}_{1:D2}.png' -f $CapturePrefix, $Index
if (-not (Test-Path -LiteralPath $PngPath -PathType Leaf)) {
throw "Viewer exited normally but did not write required back-buffer capture index $Index."
}
if ($CaptureMode -eq 'Spawn') {
$CaptureTime = @('0.5s', '1.0s', '2.0s')[$Index]
$JpegPath = Join-Path $CaptureRoot "${SafeLabel}_spawn_$CaptureTime.jpg"
} else {
$JpegPath = Join-Path $CaptureRoot "${SafeLabel}_mid_sprint.jpg"
}
Save-DownscaledJpeg $PngPath $JpegPath 72
if ((Get-Item -LiteralPath $JpegPath).Length -ge 300KB) {
Save-DownscaledJpeg $PngPath $JpegPath 48
}
$Jpeg = Get-Item -LiteralPath $JpegPath
if ($Jpeg.Length -ge 300KB) { throw "Downscaled capture exceeds 300 KB: $($Jpeg.Name)" }
Remove-Item -LiteralPath $PngPath -Force
$Timing = [regex]::Match($ViewerLog, "\[(Spawn|Flight)SequenceCapture\] index=$Index elapsed_s=([0-9.]+)")
$Elapsed = if ($Timing.Success) { $Timing.Groups[2].Value } else { 'missing' }
Write-Host ('capture={0} elapsed_s={1} size={2} bytes' -f $Jpeg.Name, $Elapsed, $Jpeg.Length)
}
if ($CaptureMode -eq 'Flight') {
# Keep only the reviewed mid-flight JPG; remove this run's unused raw D3D frames.
# Garder uniquement le JPG de vol examine ; supprimer les tampons D3D bruts inutilises de ce run.
foreach ($Index in 0..4) {
$PngPath = '{0}_{1:D2}.png' -f $CapturePrefix, $Index
if (Test-Path -LiteralPath $PngPath -PathType Leaf) {
Remove-Item -LiteralPath $PngPath -Force
}
}
}
Write-Host ('viewer_closed=1 pid={0} exit_code={1} elapsed_s={2:0.000} priority=Normal affinity=0xFFF' -f
$Process.Id, $Process.ExitCode, $Started.Elapsed.TotalSeconds)
@@ -0,0 +1,147 @@
# World Assembler S5 / M3 Round 9 — measured result
**Status:** Standalone and BuildHost compile. Regression checks pass. The S5 scheduler is retained as an opt-in experiment; legacy order is the default again. S5 did not demonstrate an enclosure or walkability win, and the viewer could not confirm what was presented to the screen. The 60 m/s result also had invalid center deltas.
## Count, binaries and timing protocol
The owner clipmap is 2,553 tiles: 960 LOD0/1 and 1,593 coarse tiles. The final Standalone executable is Standalone/bin/VoxelForgeCoreStandalone.exe.
The matched CPU protocol used a fresh process, the staged owner_world.json, 12 workers, normal priority, affinity 0xFFF, radius 3, max LOD 8, camera 0,-35,-487.94,0,-22, world-assembler-prototype, force-generate, stream-benchmark and stream-hash. Same mode and settings for before/after:
| Run | 2,553 tiles | Hashes |
|---|---:|---|
| Legacy order | 3.641 s | Same as candidate |
| Coarse-first | 3.620 s | Same as legacy |
| Final coarse-first repeat | 3.579 s | Same as legacy |
| Final rebuilt default (legacy) smoke | 3.592 s | Same as legacy |
This is a 0.6–1.7% difference, within run noise. The 27-tile CPU mesh-ready measurement was 55 ms legacy / 22 ms coarse-first; Standalone has no collision cooking, so this is not a walkability result.
Round 7/8 report 2.975/3.107 s. The saved reproduction log, s4_protocol_reproduce_ffff.stdout.log, reproduces the 3.1 s result at 3.146 s with the prototype route, 2,553 tiles, 12 workers, hash on, normal priority and affinity 0xFFFF. The Round 7/8 reports did not preserve the exact executable hash or full launch command, so the historical binary identity cannot be established. Their Round 8 p95 run also inherited 0xFFFF, while the main S5 before/after protocol pins 0xFFF.
The reported 16.685 s run has no raw command log. A later run without the prototype route took 18.668 s, produced a different density hash, and emitted no LOD8 cap triangles; the correct prototype route produces density hash c10697a8f48380c7 and the 385-tile cap. This makes a different generation route the likely cause of the 16.685 s result, but the missing original log prevents proving its exact flags. It was not a matched comparison based on tile count alone. CPU-idle state was not controlled.
## Capture and cap instruments
The old black sequence logged three Presents with zero tile geometry and zero draw batches before its screenshots. That points to clear-only startup frames, not a capture-before-first-present race. Its logger did not tie each image to a frame index, so the exact capture frame cannot be proven. The matched S5 captures record frame index, prior presents, resident meshes, draw batches and triangle counts at capture:
| Sample | Legacy frame / tiles / batches / triangles | Coarse-first frame / tiles / batches / triangles |
|---|---|---|
| Spawn 0.5 s | 4 / 451 / 6 / 151,020 | 5 / 76 / 4 / 834,954 |
| Spawn 1.0 s | 13 / 1,801 / 41 / 446,090 | 12 / 133 / 4 / 1,804,858 |
| Spawn 2.0 s | 25 / 2,308 / 70 / 904,353 | 27 / 559 / 44 / 3,240,081 |
The capture reads the D3D back buffer pre-present after earlier frames have presented. The candidate Presents returned DXGI_STATUS_OCCLUDED even though the window was visible, so the images prove rendered back-buffer geometry but cannot establish player-visible pixels. The coarse-first images are visibly much sparser at spawn; treat that as a possible early visual regression, not a confirmed screen result.
Matched JPEGs are 9.7–21.8 KB, all below 300 KB. The viewer was capped at 60 fps with VSync and closed after capture. Coarse-first is now opt-in with --coarse-first-stream-order; legacy is the default:
| Capture | Legacy | Coarse-first |
|---|---|---|
| Spawn 0.5 s | [JPG](captures/s5_before_legacy_spawn_0.5s.jpg) | [JPG](captures/s5_after_coarse_spawn_0.5s.jpg) |
| Spawn 1.0 s | [JPG](captures/s5_before_legacy_spawn_1.0s.jpg) | [JPG](captures/s5_after_coarse_spawn_1.0s.jpg) |
| Spawn 2.0 s | [JPG](captures/s5_before_legacy_spawn_2.0s.jpg) | [JPG](captures/s5_after_coarse_spawn_2.0s.jpg) |
| 10 m/s mid-sprint | [JPG](captures/s5_before_legacy_10ms_mid_sprint.jpg) | [JPG](captures/s5_after_coarse_10ms_mid_sprint.jpg) |
| 60 m/s mid-sprint | [JPG](captures/s5_before_legacy_mid_sprint.jpg) | [JPG](captures/s5_after_coarse_mid_sprint.jpg) |
The S4 low-angle ray audit used the same owner camera at voxel origin (0,-140,-1951.8): all 72 upward rays at that view hit the roof, and all 2,520/2,520 audit rays hit. The dark “sky” region is the cap underside, not uncovered sky. S4 reports 385 refined LOD8 cap tiles; the final band census requested 413 cap bands, emitted 411, and classified the remaining two as zero-area boundary touches, with no unexplained empty bands. Far/near agreement was 0.0504 cm against a 2 cm tolerance.
## Gates and sprint results
The BuildHost actor trace reports collision readiness for the support tile and required 3×3 ring. Both orderings reached 9/9 ready at cold spawn:
| Gate interval | Legacy | Coarse-first |
|---|---:|---:|
| Cold spawn | 0.583→0.624 s (41.2 ms) | 0.577→0.618 s (41.6 ms) |
| Teleport to center (-5,0,-26) | 6.427→6.642 s (215 ms) | 6.389→6.598 s (208 ms) |
| Teleport to center (-2,8,-47) | 15.066→15.349 s (283 ms) | 14.943→15.504 s (561 ms) |
The actor test teleported between centers and logged speed 0; it is not a 10/60 m/s actor walk. Two separate enforced-collision-gate runs, one per order, reached the 900 s limit while support requests kept recentering. Thus the readiness counter passed at cold spawn, but physical gate enforcement during movement remains unverified.
Standalone cannot measure collision readiness. Its logged full LOD0/1 set completed at 0.889 s in the legacy spawn run. Coarse-first had not completed that set by the 2.053 s capture; its 10 m/s run logged completion at 3.450 s. The 1.57 s refinement target therefore failed for coarse-first in this viewer workload.
BuildHost full-set residency samples were spaced about 250 ms apart. These are per-LOD total residency times from trace start, not per-tile time-in-range:
| LOD | Legacy seconds | Coarse-first seconds |
|---:|---:|---:|
| 8 | 3.352 | 3.338 |
| 7 | 3.603 | 3.589 |
| 6 | 3.854 | 3.841 |
| 5 | 4.105 | 4.092 |
| 4 | 4.858 | 4.847 |
| 3 | 5.108 | 5.099 |
| 2 | 5.360 | 5.351 |
| 0 | 5.865 | 5.853 |
| 1 | 6.367 | 6.354 |
The largest difference is 13 ms, below the sample interval. The full desired LOD sets arrive after the 1.57 s goal in both orders. Enclosure gap frames/time were not measured: DXGI reported occlusion, and no reliable screen-visible gap counter exists. The static S4 ray audit proves roof coverage after generation, not startup or moving enclosure.
The S5 full-view tile p95 probe used the same 12 workers, normal priority, inherited affinity 0xFFFF, 60 fps/VSync and no timeline trace for both orderings. Each viewer was closed immediately after InitialClipmapReady:
| LOD | Legacy p95 ms | Coarse-first p95 ms |
|---:|---:|---:|
| 0 | 16.398 | 17.456 |
| 1 | 15.665 | 15.804 |
| 2 | 4.393 | 4.292 |
| 3 | 17.263 | 18.055 |
| 4 | 59.038 | 58.867 |
| 5 | 17.649 | 17.915 |
| 6 | 51.086 | 57.610 |
| 7 | 29.419 | 32.116 |
| 8 | 82.782 | 85.041 |
The coarse-first full view took 3.309 s versus 3.187 s legacy. LOD6/7 p95 rose 12.8%/9.2% in this one pair; the result cannot establish “within noise.” S4’s p95 check was a separate eight-key staged test (LOD7 19.801/19.807 ms, LOD8 58.346/58.411 ms for S3/S4) and was not replayed on those exact keys in S5. Do not claim an apples-to-apples S3/S4 p95 pass.
Standalone sprint benchmark results:
| Speed | Order | Requests incl. 2,553 spawn | Queued cancellations | Discarded stale results | Nearest-ready p95 / max | Max lag p95 / max | Status |
|---:|---|---:|---:|---:|---|---|---|
| 10 m/s | Legacy | 33,730 | 3 | 10 | 126.628 / 171.452 ms | 1.27 / 1.71 m | FAIL |
| 10 m/s | Coarse-first | 33,730 | 0 | 0 | 129.812 / 148.705 ms | 1.30 / 1.49 m | FAIL |
| 60 m/s | Legacy | 39,632 | 29,809 | 138 | 93.101 / 114.475 ms | 5.59 / 6.87 m | FAIL, invalid deltas |
| 60 m/s | Coarse-first | 38,679 | 22,662 | 120 | 68.748 / 182.143 ms | 4.12 / 10.93 m | FAIL, invalid deltas |
The 10 m/s route had valid unit-+Y center deltas: coarse-first slightly worsened p95 lag but improved worst lag. At 60 m/s the center deltas were invalid, so the apparent p95/crossing-wall improvement is not a valid matched gate win; its worst nearest-ready lag was worse by 67.7 ms. Request/cancellation counts cover the initial cold request set and crossings 1–250; stale discards include initial work and crossings 1–250.
The actor trace’s UpdateChunks sample is not an isolated scheduler timer. Legacy mean/p95/max was 0.549/0.683/5.480 ms; optimized coarse-first was 0.116/0.269/4.860 ms. The pre-optimization candidate had 79.073 and 35.510 ms spikes from sorting each level repeatedly while at capacity. The final candidate skips full queues and only sorts missing, nonpending tiles. No blocking wait was added to Tick.
## Determinism, regression checks and async safety
Standalone final 2,553-tile hashes match S4: density c10697a8f48380c7, mesh 430d2be2bdd8b988, payload b7b33e14092225c9, topology c5c46d66606a819e. LOD0–3 hashes also match S4 exactly:
| LOD | Density | Mesh | Payload | Topology |
|---:|---|---|---|---|
| 0 | 769f2a706d585a72 | 33eeeb39fa443816 | 645621a1d968983f | c2457ed905036c9d |
| 1 | 69cc5a5c0a2c60af | b48335f5410c57d5 | d15c5480162ab8ba | feca1bd025823604 |
| 2 | 85deed5683a77202 | 53469895987aeea2 | e55f8ae7e1532033 | d5feb12a0e85c09b |
| 3 | 6518caefb3be7e7f | ec513c90b853a614 | f62fa1137ff17f82 | ccf1dcf681965816 |
BuildHost SurfaceWorld at 25 cm passed W1/W8/W16 with density 0cd2a1a4152ee4aa, mesh 80a5b054b904b1a2, payload bf2f1b40dff38acc, topology 56074198682d5e18. FloatingIslands at 25 cm passed W1/W8/W16, collision, edits and switch-off. Core-off legacy passed at 12.5 cm; M2 V1 and V2 each passed five focused fixtures plus border. S4 roof coverage/agreement and S3 ground agreement passed; final ground delta was 0.000 cm and final far-horizon agreement max was 0.0427 cm.
Async safety audit: all four result queues remain EQueueMode::Mpsc; EndPlay sets bShuttingDown, waits ActiveTaskCount to zero, then drains queues. Generation workers check shutdown state. Both LoadTile and LoadCoreTile carry GenerationEpoch in the result and reject stale results; ModsLock behavior is unchanged. No async path or generated header was removed or rewritten.
## Builds, run ledger and changed files
Final Standalone Build.ps1 -Target All and BuildVoxelForgeCoreHost.ps1 builds passed. BuildHost link safety checked 96,244 dumpbin lines against 421 copied type names, verified five staged asset hashes, and linked/audited 18 Core shims. Recent launches had over 17 GiB free RAM; recorded runtime peaks stayed below 4 GiB. No run returned 0xC0000142. No new UECC-* folder appeared; the only crash folder is dated 2026-09-17.
Failed, retried or capped attempts:
- The first baseline used the default camera and generated 2,226 tiles in 6.521 s; the owner-camera run restored 2,553.
- One standalone hash launch omitted stream-benchmark and entered the viewer; it was stopped after 146 s and rerun correctly. A later clip-radius 6 request hit the standalone safety bound.
- A generic horizon test targeted invalid /Game/Untitled and failed; it was not an owner-world result. Initial headless/GDI captures did not read the D3D frame; the viewer back-buffer path replaced them.
- Get-CimInstance memory preflight was denied; subsequent checks used GlobalMemoryStatusEx. A fresh-shell helper/preflight attempt failed before process launch and was corrected. Two tile-profile command strings also had PowerShell parse errors before launch; the corrected pairs produced complete metrics.
- Both enforced-collision-gate actor runs timed out at 900 s while support requests recentered. All four tile-profile viewers reached InitialClipmapReady, were closed deliberately, and returned exit -1 on WM_CLOSE; their timing output was complete.
**Habits:** Counted 2,553 tiles and checked the executable/mode; evaluated the staged owner plan and walk; used fresh matched before/after settings; explained the Round 7/8 timing discrepancy and affinity difference; kept the result report bounded to measured outcomes.
## Source files and compile spots
No new C++ source file was added. Modified C++:
- Source/VoxelForge/Private/VoxelWorld.cpp and Source/VoxelForge/Public/VoxelWorld.h — VoxelForge runtime module; BuildHost UBT.
- Source/VoxelForge/Private/VoxelStartupTrace.cpp and Source/VoxelForge/Public/VoxelStartupTrace.h — VoxelForge runtime module; BuildHost UBT.
- Standalone/Source/StandaloneViewer.cpp and Standalone/Source/StreamingWorld.h — Standalone/Build.ps1 -Target All.
Modified supporting scripts/docs: Standalone/Build.ps1, Tools/VoxelForgeTest.ps1, Tools/TestVoxelForgeCoreWorldHost.ps1, Standalone/Research/WorldAssemblerM3/Round9/CaptureViewerSequence.ps1, CODEMAP.md §3, ARCHITECTURE.md §8.26. The capture helper is the only new helper source.
### Dialogue
**User:** Check the instrument before judging the scheduler.
**Luna:** The 3.1 s prototype result reproduced; the matched gates did not justify a default-on order, so coarse-first remains opt-in.
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+117 -4
View File
@@ -1635,6 +1635,9 @@ namespace
uint64 GpuFarProtectedThinSamples=0,GpuFarProtectedSolidSamples=0; uint64 GpuFarProtectedThinSamples=0,GpuFarProtectedSolidSamples=0;
uint64 GpuFarProtectedSealSamples=0,GpuFarProtectedOverlaySamples=0; uint64 GpuFarProtectedSealSamples=0,GpuFarProtectedOverlaySamples=0;
uint64 PresentCallCount=0; uint64 PresentCallCount=0;
uint64 LastCapturedFrameOrdinal=0,LastCapturedPresentOrdinal=0;
int32 LastCapturedSequenceIndex=-1;
bool bLastCapturedSequenceIsFlight=false;
uint64 StartupNonBlockingPresentCalls=0,StartupNonBlockingPresentSkips=0; uint64 StartupNonBlockingPresentCalls=0,StartupNonBlockingPresentSkips=0;
double GpuFarGpuDispatchMs=0.0; double GpuFarGpuDispatchMs=0.0;
double GpuFarGpuDispatchMaxMs=0.0; double GpuFarGpuDispatchMaxMs=0.0;
@@ -1692,6 +1695,10 @@ namespace
float FlightSpeedMps=16.0f; float FlightSpeedMps=16.0f;
std::string FlightCaptureSequencePrefix; std::string FlightCaptureSequencePrefix;
int32 FlightCaptureSequenceIndex=0; int32 FlightCaptureSequenceIndex=0;
int32 FlightCaptureCloseIndex=-1;
std::string SpawnCaptureSequencePrefix;
int32 SpawnCaptureSequenceIndex=0;
bool bQuitAfterSpawnCaptures=false,bSpawnCaptureClosePosted=false;
bool bFlightStarted=false; bool bFlightStarted=false;
bool bFlightFinished=false; bool bFlightFinished=false;
bool bFlightClosePosted=false; bool bFlightClosePosted=false;
@@ -5509,6 +5516,71 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
void CaptureRenderedFrame() void CaptureRenderedFrame()
{ {
auto LogCaptureFrameAudit=[&](const char* Mode,int32 Index,double ElapsedSeconds)
{
std::size_t ReadyTiles=0;
for(const FVoxelTileKey& Key:DesiredTiles)
if(IsRenderedTileReady(Key))++ReadyTiles;
uint64 DrawIndices=0;
uint64 DrawBatches=0;
for(const auto& Pair:StreamingBatches)
{
const FStreamingGpuBatch& Batch=Pair.second;
if(!bShowLevel[Pair.first.Level]
||(CaptureOnlyLod>=0&&Pair.first.Level!=CaptureOnlyLod)
||!Batch.VertexBuffer||!Batch.IndexBuffer||!Batch.IndexCount
||(bDrawOnlyTile&&Batch.DrawOnlyIndexCount==0))continue;
DrawIndices+=bDrawOnlyTile?Batch.DrawOnlyIndexCount:Batch.IndexCount;
++DrawBatches;
}
std::printf("[CaptureFrameAudit] mode=%s index=%d elapsed_s=%.3f capture_phase=pre_present frame=%llu presents_completed=%llu next_present=%llu resident_tiles=%zu/%zu resident_meshes=%zu draw_batches=%llu draw_triangles=%llu queued_upload_batches=%zu gpu_far_tiles=%zu\n",
Mode,Index,ElapsedSeconds,
static_cast<unsigned long long>(ColdTimelineFrame()),
static_cast<unsigned long long>(PresentCallCount),
static_cast<unsigned long long>(PresentCallCount+1),
ReadyTiles,DesiredTiles.size(),ResidentTiles.size(),
static_cast<unsigned long long>(DrawBatches),
static_cast<unsigned long long>(DrawIndices/3),DirtyBatchSet.size(),GpuFarTiles.size());
std::fflush(stdout);
};
// Capturer apres le rendu, avant le present imminent du meme frame.
// Capture after rendering, before this same frame's imminent present.
if(!SpawnCaptureSequencePrefix.empty()&&SpawnCaptureSequenceIndex<3
&&GenerationStart.time_since_epoch().count()!=0)
{
static constexpr std::array<double,3> CaptureSeconds={0.5,1.0,2.0};
const double SpawnElapsed=std::chrono::duration<double>(
std::chrono::steady_clock::now()-GenerationStart).count();
if(SpawnElapsed>=CaptureSeconds[SpawnCaptureSequenceIndex])
{
LogCaptureFrameAudit("spawn",SpawnCaptureSequenceIndex,SpawnElapsed);
char Suffix[16]{};
std::snprintf(Suffix,sizeof(Suffix),"_%02d.png",SpawnCaptureSequenceIndex);
const std::string Path=SpawnCaptureSequencePrefix+Suffix;
if(SaveBackBufferPng(Path))
{
LastCapturedFrameOrdinal=ColdTimelineFrame();
LastCapturedPresentOrdinal=PresentCallCount+1;
LastCapturedSequenceIndex=SpawnCaptureSequenceIndex;
bLastCapturedSequenceIsFlight=false;
std::printf("[SpawnSequenceCapture] index=%d elapsed_s=%.3f path=%s\n",
SpawnCaptureSequenceIndex,SpawnElapsed,Path.c_str());
}
else
std::fprintf(stderr,"[SpawnSequenceCapture] index=%d elapsed_s=%.3f failed path=%s\n",
SpawnCaptureSequenceIndex,SpawnElapsed,Path.c_str());
++SpawnCaptureSequenceIndex;
std::fflush(stdout);
if(bQuitAfterSpawnCaptures&&SpawnCaptureSequenceIndex>=3
&&!bSpawnCaptureClosePosted)
{
bSpawnCaptureClosePosted=true;
PostMessageA(Window,WM_CLOSE,0,0);
}
}
}
// Capturer le vol dans la boucle de rendu conserve les transitions LOD. // Capturer le vol dans la boucle de rendu conserve les transitions LOD.
// Capture the flight inside the render loop to retain LOD transition frames. // Capture the flight inside the render loop to retain LOD transition frames.
if(!FlightCaptureSequencePrefix.empty()&&bFlightStarted) if(!FlightCaptureSequencePrefix.empty()&&bFlightStarted)
@@ -5519,17 +5591,30 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
if(FlightCaptureSequenceIndex<static_cast<int32>(CaptureSeconds.size()) if(FlightCaptureSequenceIndex<static_cast<int32>(CaptureSeconds.size())
&&FlightElapsed>=CaptureSeconds[FlightCaptureSequenceIndex]) &&FlightElapsed>=CaptureSeconds[FlightCaptureSequenceIndex])
{ {
LogCaptureFrameAudit("flight",FlightCaptureSequenceIndex,FlightElapsed);
char Suffix[16]{}; char Suffix[16]{};
std::snprintf(Suffix,sizeof(Suffix),"_%02d.png",FlightCaptureSequenceIndex); std::snprintf(Suffix,sizeof(Suffix),"_%02d.png",FlightCaptureSequenceIndex);
const std::string Path=FlightCaptureSequencePrefix+Suffix; const std::string Path=FlightCaptureSequencePrefix+Suffix;
const int32 CapturedIndex=FlightCaptureSequenceIndex;
if(SaveBackBufferPng(Path)) if(SaveBackBufferPng(Path))
{
LastCapturedFrameOrdinal=ColdTimelineFrame();
LastCapturedPresentOrdinal=PresentCallCount+1;
LastCapturedSequenceIndex=CapturedIndex;
bLastCapturedSequenceIsFlight=true;
std::printf("[FlightSequenceCapture] index=%d elapsed_s=%.3f path=%s\n", std::printf("[FlightSequenceCapture] index=%d elapsed_s=%.3f path=%s\n",
FlightCaptureSequenceIndex,FlightElapsed,Path.c_str()); CapturedIndex,FlightElapsed,Path.c_str());
}
else else
std::fprintf(stderr,"[FlightSequenceCapture] index=%d failed path=%s\n", std::fprintf(stderr,"[FlightSequenceCapture] index=%d failed path=%s\n",
FlightCaptureSequenceIndex,Path.c_str()); CapturedIndex,Path.c_str());
++FlightCaptureSequenceIndex; ++FlightCaptureSequenceIndex;
std::fflush(stdout); std::fflush(stdout);
if(FlightCaptureCloseIndex==CapturedIndex&&!bFlightClosePosted)
{
bFlightClosePosted=true;
PostMessageA(Window,WM_CLOSE,0,0);
}
} }
} }
const bool bReadyToCapture=bCaptureAfterClipmap?bInitialClipmapReported:IsFullDetailUploaded(); const bool bReadyToCapture=bCaptureAfterClipmap?bInitialClipmapReported:IsFullDetailUploaded();
@@ -5816,7 +5901,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
bGpuSkirtDrawAuditReported=true; bGpuSkirtDrawAuditReported=true;
std::fflush(stdout); std::fflush(stdout);
} }
CaptureRenderedFrame();
const bool bHoldFrontBufferForGpuWarmup=bHoldCpuFrameDuringGpuWarmup const bool bHoldFrontBufferForGpuWarmup=bHoldCpuFrameDuringGpuWarmup
&&bCpuReadyFramePresentedBeforeGpu&&bGpuFarEnabled &&bCpuReadyFramePresentedBeforeGpu&&bGpuFarEnabled
&&GpuFarTilesSubmitted>0&&!bInitialClipmapReported; &&GpuFarTilesSubmitted>0&&!bInitialClipmapReported;
@@ -5864,6 +5948,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::chrono::duration<double,std::milli>(GpuPumpEnd-CpuPumpEnd).count()); std::chrono::duration<double,std::milli>(GpuPumpEnd-CpuPumpEnd).count());
return; return;
} }
const bool bDidPresent=!bGpuFarNoPresent||bDeferGpuUntilCpuFrame;
if(bDidPresent)CaptureRenderedFrame();
const auto BeforePresent=std::chrono::steady_clock::now(); const auto BeforePresent=std::chrono::steady_clock::now();
const bool bNonBlockingStartupPresent=bGpuFarEnabled&&!bInitialClipmapReported const bool bNonBlockingStartupPresent=bGpuFarEnabled&&!bInitialClipmapReported
&&!bForceBlockingStartupPresent; &&!bForceBlockingStartupPresent;
@@ -5872,10 +5958,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
const auto PresentCallStart=std::chrono::steady_clock::now(); const auto PresentCallStart=std::chrono::steady_clock::now();
// The CPU-ready frame is the ownership handoff gate for this route. // The CPU-ready frame is the ownership handoff gate for this route.
// Keep that one presentation, then suppress presents while GPU work runs. // Keep that one presentation, then suppress presents while GPU work runs.
const bool bDidPresent=!bGpuFarNoPresent||bDeferGpuUntilCpuFrame;
const HRESULT PresentResult=bDidPresent const HRESULT PresentResult=bDidPresent
?SwapChain->Present(PresentSyncInterval,PresentFlags):S_FALSE; ?SwapChain->Present(PresentSyncInterval,PresentFlags):S_FALSE;
const auto PresentCallEnd=std::chrono::steady_clock::now(); const auto PresentCallEnd=std::chrono::steady_clock::now();
if(LastCapturedFrameOrdinal==FrameOrdinal)
{
std::printf("[CaptureFramePresentation] mode=%s index=%d frame=%llu present_ordinal=%llu hr=0x%08lx present_called=%d status_succeeded=%d occluded=%d window_visible=%d\n",
bLastCapturedSequenceIsFlight?"flight":"spawn",LastCapturedSequenceIndex,
static_cast<unsigned long long>(LastCapturedFrameOrdinal),
static_cast<unsigned long long>(LastCapturedPresentOrdinal),
static_cast<unsigned long>(PresentResult),
bDidPresent?1:0,SUCCEEDED(PresentResult)?1:0,
PresentResult==DXGI_STATUS_OCCLUDED?1:0,IsWindowVisible(Window)?1:0);
std::fflush(stdout);
LastCapturedFrameOrdinal=0;
LastCapturedPresentOrdinal=0;
LastCapturedSequenceIndex=-1;
}
if(!bDidPresent&&!bGpuFarNoPresentReported) if(!bDidPresent&&!bGpuFarNoPresentReported)
{ {
bGpuFarNoPresentReported=true; bGpuFarNoPresentReported=true;
@@ -9399,6 +9498,7 @@ int main(int Argc,char** Argv)
std::string WorldAssemblyManifestOutputPath; std::string WorldAssemblyManifestOutputPath;
bool bStreamBenchmark=false; bool bStreamBenchmark=false;
bool bStreamPipeline=false; bool bStreamPipeline=false;
bool bCoarseFirstStreamOrder=false;
bool bProfileDetailBand=false; bool bProfileDetailBand=false;
bool bPassageDetailAuditOnly=false; bool bPassageDetailAuditOnly=false;
bool bGridStairAudit=false; bool bGridStairAudit=false;
@@ -9423,6 +9523,13 @@ int main(int Argc,char** Argv)
else if(Arg.starts_with("--capture-flight-sequence-prefix=")) else if(Arg.starts_with("--capture-flight-sequence-prefix="))
App.FlightCaptureSequencePrefix=std::string( App.FlightCaptureSequencePrefix=std::string(
Arg.substr(std::string_view("--capture-flight-sequence-prefix=").size())); Arg.substr(std::string_view("--capture-flight-sequence-prefix=").size()));
else if(Arg.starts_with("--flight-capture-close-index="))
App.FlightCaptureCloseIndex=std::atoi(
std::string(Arg.substr(std::string_view("--flight-capture-close-index=").size())).c_str());
else if(Arg.starts_with("--capture-spawn-sequence-prefix="))
App.SpawnCaptureSequencePrefix=std::string(
Arg.substr(std::string_view("--capture-spawn-sequence-prefix=").size()));
else if(Arg=="--quit-after-spawn-captures")App.bQuitAfterSpawnCaptures=true;
else if(Arg=="--no-vsync")App.bDisableVSync=true; else if(Arg=="--no-vsync")App.bDisableVSync=true;
else if(Arg.starts_with("--frame-limit=")) else if(Arg.starts_with("--frame-limit="))
{FrameLimitHz=std::clamp(std::atoi(std::string(Arg.substr(14)).c_str()),1,60);bFrameLimitSpecified=true;} {FrameLimitHz=std::clamp(std::atoi(std::string(Arg.substr(14)).c_str()),1,60);bFrameLimitSpecified=true;}
@@ -9521,6 +9628,8 @@ int main(int Argc,char** Argv)
else if(Arg=="--round-tunnels-legacy") else if(Arg=="--round-tunnels-legacy")
{App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;} {App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;}
else if(Arg=="--stream-benchmark")bStreamBenchmark=true; else if(Arg=="--stream-benchmark")bStreamBenchmark=true;
else if(Arg=="--legacy-stream-order")bCoarseFirstStreamOrder=false;
else if(Arg=="--coarse-first-stream-order")bCoarseFirstStreamOrder=true;
else if(Arg=="--stream-pipeline") else if(Arg=="--stream-pipeline")
{bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;} {bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;}
else if(Arg=="--edit-benchmark") else if(Arg=="--edit-benchmark")
@@ -9613,6 +9722,10 @@ int main(int Argc,char** Argv)
} }
else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg); else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg);
} }
FStandaloneStreamingGenerator::SetCoarseFirstSchedulingEnabled(bCoarseFirstStreamOrder);
std::printf("[StandaloneStreamOrder] mode=%s negative_control=%s\n",
bCoarseFirstStreamOrder?"walkable_coarse_refine":"legacy_distance_lod",
bCoarseFirstStreamOrder?"--legacy-stream-order":"none");
std::printf("[FrameLimiter] target_fps=%d maximum=true vsync=%s source=%s\n",FrameLimitHz, std::printf("[FrameLimiter] target_fps=%d maximum=true vsync=%s source=%s\n",FrameLimitHz,
App.bDisableVSync?"off":"on",bFrameLimitSpecified?"argument":"default"); App.bDisableVSync?"off":"on",bFrameLimitSpecified?"argument":"default");
if(App.bCaptureOnlyTile&&App.bGpuFarEnabled) if(App.bCaptureOnlyTile&&App.bGpuFarEnabled)
+79 -7
View File
@@ -9,6 +9,7 @@
#include <atomic> #include <atomic>
#include <bit> #include <bit>
#include <chrono> #include <chrono>
#include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <deque> #include <deque>
@@ -409,6 +410,11 @@ namespace StandaloneWorldMesher
class FStandaloneStreamingGenerator class FStandaloneStreamingGenerator
{ {
public: public:
static void SetCoarseFirstSchedulingEnabled(bool bEnabled)
{bCoarseFirstSchedulingEnabled.store(bEnabled,std::memory_order_relaxed);}
static bool IsCoarseFirstSchedulingEnabled()
{return bCoarseFirstSchedulingEnabled.load(std::memory_order_relaxed);}
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=8, explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=8,
bool bInHashDensity=false, bool bInHashDensity=false,
const FStandaloneMeshCache* InMeshCache=nullptr, const FStandaloneMeshCache* InMeshCache=nullptr,
@@ -448,6 +454,7 @@ public:
bStop=false; bStop=false;
} }
for(int32 I=0;I<WorkerCount;++I)Workers.emplace_back([this](){WorkerLoop();}); for(int32 I=0;I<WorkerCount;++I)Workers.emplace_back([this](){WorkerLoop();});
bHasLastCameraPosition=false;TravelDirection=FVector::ZeroVector;
return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex); return UpdateDesired(CameraPositionVoxels,ResidentKeys,CrossingIndex);
} }
@@ -461,6 +468,7 @@ public:
bStop=false; bStop=false;
} }
for(int32 I=0;I<WorkerCount;++I)Workers.emplace_back([this](){WorkerLoop();}); for(int32 I=0;I<WorkerCount;++I)Workers.emplace_back([this](){WorkerLoop();});
bHasLastCameraPosition=false;TravelDirection=FVector::ZeroVector;
return UpdateDesiredSet(InitialDesired,ResidentKeys,CrossingIndex); return UpdateDesiredSet(InitialDesired,ResidentKeys,CrossingIndex);
} }
@@ -507,6 +515,23 @@ public:
const FVector& CameraPositionVoxels,const std::vector<FVoxelTileKey>& ResidentKeys, const FVector& CameraPositionVoxels,const std::vector<FVoxelTileKey>& ResidentKeys,
uint64 CrossingIndex=0) uint64 CrossingIndex=0)
{ {
if(bHasLastCameraPosition)
{
const FVector Delta=CameraPositionVoxels-LastCameraPositionVoxels;
const double LengthSquared=static_cast<double>(Delta.X)*Delta.X
+static_cast<double>(Delta.Y)*Delta.Y;
if(LengthSquared>1.0e-12)
{
const double InverseLength=1.0/std::sqrt(LengthSquared);
TravelDirection=FVector(static_cast<float>(Delta.X*InverseLength),
static_cast<float>(Delta.Y*InverseLength),0.0f);
}
else TravelDirection=FVector::ZeroVector;
}
else
TravelDirection=FVector::ZeroVector;
LastCameraPositionVoxels=CameraPositionVoxels;
bHasLastCameraPosition=true;
FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels); FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels);
if(CoreWorld) if(CoreWorld)
{ {
@@ -652,17 +677,60 @@ private:
Stats.CancelledByCrossing.reserve(CancelledByCrossing.size()); Stats.CancelledByCrossing.reserve(CancelledByCrossing.size());
for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair); for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair);
std::vector<VoxelForgeCore::TileKey> Unordered;Unordered.reserve(NewDesired.Tiles.size()); std::vector<FVoxelTileKey> Prioritized=NewDesired.Tiles;
for(const FVoxelTileKey& Key:NewDesired.Tiles) if(IsCoarseFirstSchedulingEnabled())
Unordered.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level}); {
const auto Prioritized=VoxelForgeCore::World::prioritizeTiles(Unordered, // The local walkable ring leads, the coarse enclosure follows, then target-detail work.
{NewDesired.CenterChunk.X,NewDesired.CenterChunk.Y,NewDesired.CenterChunk.Z}); // L'anneau praticable local passe d'abord, puis l'enveloppe grossiere, puis le detail cible.
const int32 FirstCoarseLevel=FMath::Max(2,World->FullResClipLevels);
auto Tier=[&](const FVoxelTileKey& Key)
{
if(Key.Level==0&&std::abs(Key.Coord.X-NewDesired.CenterChunk.X)<=1
&&std::abs(Key.Coord.Y-NewDesired.CenterChunk.Y)<=1
&&std::abs(Key.Coord.Z-NewDesired.CenterChunk.Z)<=1)return 0;
return Key.Level>=FirstCoarseLevel?1:2;
};
auto IsAhead=[&](const FVoxelTileKey& Key)
{
if(TravelDirection.X*TravelDirection.X+TravelDirection.Y*TravelDirection.Y<=1.0e-12f)
return false;
const double Scale=static_cast<double>(1<<Key.Level);
const double OffsetX=(static_cast<double>(Key.Coord.X)+0.5)*Scale-NewDesired.CenterChunk.X;
const double OffsetY=(static_cast<double>(Key.Coord.Y)+0.5)*Scale-NewDesired.CenterChunk.Y;
return OffsetX*TravelDirection.X+OffsetY*TravelDirection.Y>0.0;
};
std::sort(Prioritized.begin(),Prioritized.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
{
const int32 TierA=Tier(A),TierB=Tier(B);
if(TierA!=TierB)return TierA<TierB;
if(TierA==1&&A.Level!=B.Level)return A.Level>B.Level;
const bool bAheadA=IsAhead(A),bAheadB=IsAhead(B);
if(bAheadA!=bAheadB)return bAheadA;
const double DA=StandaloneWorldMesher::TileDistanceSquaredChunks(A,NewDesired.CenterChunk);
const double DB=StandaloneWorldMesher::TileDistanceSquaredChunks(B,NewDesired.CenterChunk);
if(DA!=DB)return DA<DB;
if(A.Level!=B.Level)return A.Level<B.Level;
if(A.Coord.Z!=B.Coord.Z)return A.Coord.Z<B.Coord.Z;
if(A.Coord.Y!=B.Coord.Y)return A.Coord.Y<B.Coord.Y;
return A.Coord.X<B.Coord.X;
});
}
else
{
std::vector<VoxelForgeCore::TileKey> Unordered;Unordered.reserve(NewDesired.Tiles.size());
for(const FVoxelTileKey& Key:NewDesired.Tiles)
Unordered.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level});
const auto CorePrioritized=VoxelForgeCore::World::prioritizeTiles(Unordered,
{NewDesired.CenterChunk.X,NewDesired.CenterChunk.Y,NewDesired.CenterChunk.Z});
Prioritized.clear();Prioritized.reserve(CorePrioritized.size());
for(const auto& Key:CorePrioritized)
Prioritized.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
}
std::unordered_map<FVoxelTileKey,size_t,FStandaloneTileKeyHash> PriorityRanks; std::unordered_map<FVoxelTileKey,size_t,FStandaloneTileKeyHash> PriorityRanks;
PriorityRanks.reserve(Prioritized.size()*2+1); PriorityRanks.reserve(Prioritized.size()*2+1);
for(size_t Rank=0;Rank<Prioritized.size();++Rank) for(size_t Rank=0;Rank<Prioritized.size();++Rank)
{ {
const auto& Key=Prioritized[Rank]; PriorityRanks.emplace(Prioritized[Rank],Rank);
PriorityRanks.emplace(FVoxelTileKey(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod),Rank);
} }
for(FStandaloneTileJob& Job:Queue)Job.PriorityRank=PriorityRanks.at(Job.Key); for(FStandaloneTileJob& Job:Queue)Job.PriorityRank=PriorityRanks.at(Job.Key);
@@ -884,6 +952,10 @@ private:
TimelineMilliseconds(Started),TimelineMilliseconds(Finished),WorkerMs); TimelineMilliseconds(Started),TimelineMilliseconds(Finished),WorkerMs);
} }
FStandaloneOwnerWorld* World=nullptr; FStandaloneOwnerWorld* World=nullptr;
inline static std::atomic<bool> bCoarseFirstSchedulingEnabled{true};
FVector LastCameraPositionVoxels=FVector::ZeroVector;
FVector TravelDirection=FVector::ZeroVector;
bool bHasLastCameraPosition=false;
int32 WorkerCount=8; int32 WorkerCount=8;
bool bHashDensity=false; bool bHashDensity=false;
const FStandaloneMeshCache* MeshCache=nullptr; const FStandaloneMeshCache* MeshCache=nullptr;
+17 -1
View File
@@ -2,6 +2,9 @@
param( param(
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900, [ValidateRange(60, 900)][int]$TimeoutSeconds = 900,
[ValidateRange(12.5, 25.0)][double]$CoreVoxelSizeCm = 25.0, [ValidateRange(12.5, 25.0)][double]$CoreVoxelSizeCm = 25.0,
[ValidateSet('CoarseFirst', 'Legacy')][string]$StreamingOrder = 'CoarseFirst',
[switch]$EnforceCollisionGate,
[switch]$StartupTrace,
[string]$WorldAssemblyManifestPath = '', [string]$WorldAssemblyManifestPath = '',
[ValidateSet('MarchingCubesV1', 'WorldAssemblerHybridV1', 'WorldAssemblerHybridV2')][string]$CoreMesherRecipe = 'MarchingCubesV1', [ValidateSet('MarchingCubesV1', 'WorldAssemblerHybridV1', 'WorldAssemblerHybridV2')][string]$CoreMesherRecipe = 'MarchingCubesV1',
[switch]$EnableHybridFixturePlacement, [switch]$EnableHybridFixturePlacement,
@@ -133,6 +136,9 @@ if ($SharedFeatureAuditOnly) {
if ($SelectedModeCount -ne 0) { throw '-SharedFeatureAuditOnly uses the SurfaceWorld owner route.' } if ($SelectedModeCount -ne 0) { throw '-SharedFeatureAuditOnly uses the SurfaceWorld owner route.' }
$RunName = 'shared_feature_audit' $RunName = 'shared_feature_audit'
} }
$RunName += if ($StreamingOrder -eq 'Legacy') { '_legacy_order' } else { '_coarse_first' }
if ($EnforceCollisionGate) { $RunName += '_collision_gate' }
if ($StartupTrace) { $RunName += '_trace' }
$foreignEditors = @(Get-Process -ErrorAction SilentlyContinue | $foreignEditors = @(Get-Process -ErrorAction SilentlyContinue |
Where-Object { $_.ProcessName -like 'UnrealEditor*' }) Where-Object { $_.ProcessName -like 'UnrealEditor*' })
@@ -247,6 +253,15 @@ $BaseArguments = @(
("-corehybridfixturesonly=$HybridFixturesOnlyFlag"), ("-corehybridfixturesonly=$HybridFixturesOnlyFlag"),
('-out="{0}"' -f $OutputDirectory) ('-out="{0}"' -f $OutputDirectory)
) )
$CoarseFirstValue = if ($StreamingOrder -eq 'CoarseFirst') { 1 } else { 0 }
$DynamicReserveValue = if ($StreamingOrder -eq 'CoarseFirst') { 1 } else { 0 }
$BaseArguments += "-voxel.StreamingCoarseFirst=$CoarseFirstValue"
$BaseArguments += "-voxel.StreamingDynamicNearReserve=$DynamicReserveValue"
if ($StartupTrace) {
$TracePath = Join-Path $LogDirectory "CoreWorldValidation-$RunName.trace.jsonl"
$BaseArguments += ('-voxel.StartupTraceFile="{0}"' -f $TracePath)
$BaseArguments += '-voxel.StartupTraceUntilEndPlay=1'
}
if ($SharedFeatureAuditOnly) { $BaseArguments += '-corefeatureauditonly' } if ($SharedFeatureAuditOnly) { $BaseArguments += '-corefeatureauditonly' }
if ($HasManifestOverride) { if ($HasManifestOverride) {
$BaseArguments += ('-coreworldassembly="{0}"' -f $StagedWorldAssembly) $BaseArguments += ('-coreworldassembly="{0}"' -f $StagedWorldAssembly)
@@ -254,7 +269,8 @@ if ($HasManifestOverride) {
if ($CoreTestSeed -ne [int]::MinValue) { if ($CoreTestSeed -ne [int]::MinValue) {
$BaseArguments += "-coretestseed=$CoreTestSeed" $BaseArguments += "-coretestseed=$CoreTestSeed"
} }
if ($FloatingIslandsOnly -or (-not $CoreFallbackTest -and -not $CoreOffTest -and -not $CorePlanOnly)) { if (-not $EnforceCollisionGate -and
($FloatingIslandsOnly -or (-not $CoreFallbackTest -and -not $CoreOffTest -and -not $CorePlanOnly))) {
# This commandlet keeps the travel pawn stationary; surface's initial collision cooks are still audited, # This commandlet keeps the travel pawn stationary; surface's initial collision cooks are still audited,
# and FloatingIslandsOnly separately probes cooked island/span capsule contacts. # and FloatingIslandsOnly separately probes cooked island/span capsule contacts.
# Le pawn de voyage du commandlet reste immobile ; les collisions de surface initiales sont auditees, # Le pawn de voyage du commandlet reste immobile ; les collisions de surface initiales sont auditees,
+4 -4
View File
@@ -488,10 +488,10 @@ function Invoke-UnrealLaunch([string]$Kind, [string[]]$Arguments, [string]$LogPa
$process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot ` $process = Start-Process -FilePath $EditorExe -ArgumentList $Arguments -WorkingDirectory $HostRoot `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle $windowStyle ` -RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath -WindowStyle $windowStyle `
-PassThru -PassThru
try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal } try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal }
catch { catch {
try { Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop } catch { } try { Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop } catch { }
throw "Could not lower owned Unreal PID $($process.Id) to BelowNormal: $($_.Exception.Message)" throw "Could not set owned Unreal PID $($process.Id) to Normal priority: $($_.Exception.Message)"
} }
$ledger.pid = [int]$process.Id $ledger.pid = [int]$process.Id
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds -EnforceMemoryLimit $wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds -EnforceMemoryLimit
@@ -547,10 +547,10 @@ function Invoke-BuildProcess([string[]]$Arguments, [string]$LogPath, [string]$Er
$process = Start-Process -FilePath $dotnet -ArgumentList $Arguments -WorkingDirectory $PluginRoot ` $process = Start-Process -FilePath $dotnet -ArgumentList $Arguments -WorkingDirectory $PluginRoot `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $ErrorPath -WindowStyle Hidden ` -RedirectStandardOutput $StdoutPath -RedirectStandardError $ErrorPath -WindowStyle Hidden `
-PassThru -PassThru
try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::BelowNormal } try { $process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal }
catch { catch {
try { Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop } catch { } try { Stop-Process -Id ([int]$process.Id) -Force -ErrorAction Stop } catch { }
throw "Could not lower owned build PID $($process.Id) to BelowNormal: $($_.Exception.Message)" throw "Could not set owned build PID $($process.Id) to Normal priority: $($_.Exception.Message)"
} }
$wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds -EnforceMemoryLimit $wait = Wait-OwnedProcess $process $LaunchTimeoutSeconds -EnforceMemoryLimit
} finally { } finally {