perf(streaming): item 3 bought nothing, item 5 removed dead weight -- both worth knowing
Two WORK-NEXT items, one null result and one confirmed removal. Item 4 untouched.
ITEM 3 -- CANCEL OBSOLETE WORK AND PRIORITISE THE FLOOR: NULL RESULT
Clean -nullrhi, diagnostics off, 800 cm/s, 200 m of travel:
before after
request->ready p95 0.369117 s 0.367612 s
queue wait p95 0.066949 s 0.064483 s
generation p95 0.287926 s 0.283642 s
p95 travel distance 2.952938 m 2.940894 m
obsolete aborts 0 12
Flat. Aborting obsolete tiles, sorting from the true pawn position and prioritising the
occupied/next tile changed request-to-ready by 0.4%.
⭐ AND THE NULL RESULT IS ITSELF THE FINDING. Look at the split: queue wait is 65 ms of a
368 ms p95; generation is 284 ms. **The queue was never the problem, so reordering it
cannot help.** Only 12 tiles in a 200 m run were obsolete enough to abort. The floor is
late because generation is slow, not because work is badly ordered -- item 3 establishes
that by exclusion, which is worth more than the 0.4%.
The machinery is kept: it is correct, it costs nothing, and it will matter once
generation is fast enough for ordering to be the binding constraint.
ITEM 5 -- VERDICT CACHE REMOVED. It was pure overhead.
Clean moving-session audit:
outer calls/probes/hits 2,109 / 2,109 / 0
nested calls/probes/hits 19,904 / 19,904 / 0
enclosing hits 0
stores 15,877, evictions 15,365
⇒ It stored 15,877 proofs, evicted 15,365 of them, and hit ZERO times. A moving player
visits unique regions; the cache was filling and thrashing a 512-entry table under a
global lock for no benefit. Same-binary A/B: request p95 0.367645 vs 0.365978,
generation p95 0.285559 vs 0.285992 -- neutral within noise, so its cost was the lock and
the linear scans rather than time we can see.
Removed: the global cache, its lock, the entry table, the invalidation path, the
cache-only profiler fields and the synthetic probe. The proof classifier stays.
⚠️ I asked for this cache two days ago on the strength of a commandlet measurement that
re-visited the same region repeatedly. That access pattern is nothing like a moving
player, and I should have said so when specifying it.
VERIFIED BY ME: build 0 errors; export 0.563 / 0.565 / 0.566 s (35de59b was 0.545-0.563,
flat as expected); geometry SHA-256 b3e5f4c3..9b377 UNCHANGED; capability 20,830 /
10,909, connected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aheo2crrZmHuYtPpdY2Cb
This commit is contained in:
1 parent
35de59bac5
commit
cf0319d480
12 files changed
+660
-514
No files matched your search
+24
-12
@@ -557,10 +557,16 @@ ApplyModification` (BlueprintCallable) + `EditorCarveSphere/EditorFillSphere` (C
|
|||||||
driven by `EditorBrush*` props.
|
driven by `EditorBrush*` props.
|
||||||
|
|
||||||
### 8.10 Performance invariants (DON'T regress)
|
### 8.10 Performance invariants (DON'T regress)
|
||||||
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set ONLY when the
|
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set only when the player
|
||||||
player crosses a chunk boundary (`LastUpdateCenter`); use the `DesiredSet` TSet for the cull;
|
crosses a level-0 chunk boundary, an anchor moves, or a rebuild is forced; use the stamped
|
||||||
idle via `bAllChunksLoaded`. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.)
|
desired map for the cull and idle via `bAllChunksLoaded`. On every rebuild, pending requests whose
|
||||||
- **LOD** changes HOT-SWAP (`LoadChunk` only, never unload-first) → no holes. LOD
|
keys left the desired set are marked obsolete through a per-request atomic token. Workers only
|
||||||
|
read that token (and `bShuttingDown`/generation pause), abort between expensive stages, and send
|
||||||
|
the result through the normal MPSC queue; the game thread reads the token again before applying.
|
||||||
|
`FChunkResult::Epoch` still guards the generation epoch. Desired work is sorted from the true pawn
|
||||||
|
position, with the occupied/support LOD0 tile and the next tile along the heading in an absolute
|
||||||
|
high-priority prefix. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.)
|
||||||
|
- **LOD** changes HOT-SWAP (`LoadTile` only, never unload-first) → no holes. LOD
|
||||||
reconciliation lives in the PERSISTENT per-frame submit loop (same loop as new-chunk loads),
|
reconciliation lives in the PERSISTENT per-frame submit loop (same loop as new-chunk loads),
|
||||||
NOT as a one-shot on the boundary-cross frame — a one-shot drops every chunk past the task
|
NOT as a one-shot on the boundary-cross frame — a one-shot drops every chunk past the task
|
||||||
budget and strands it at a stale LOD. Idle (`bAllChunksLoaded`) only when a full scan finds
|
budget and strands it at a stale LOD. Idle (`bAllChunksLoaded`) only when a full scan finds
|
||||||
@@ -688,9 +694,13 @@ driven by `EditorBrush*` props.
|
|||||||
tile is replaced by several finer tiles, so the old center-owner
|
tile is replaced by several finer tiles, so the old center-owner
|
||||||
check (`ReplacementLoaded`) dropped it as soon as the ONE tile over its centre loaded → the not-yet-
|
check (`ReplacementLoaded`) dropped it as soon as the ONE tile over its centre loaded → the not-yet-
|
||||||
ready edges flashed a hole; the full-coverage check keeps the old tile at its current resolution until
|
ready edges flashed a hole; the full-coverage check keeps the old tile at its current resolution until
|
||||||
the better mesh is wholly in, then swaps. In-flight (pending) tiles are NEVER cancelled on a rebuild —
|
the better mesh is wholly in, then swaps. In-flight requests are cancelled when their key leaves the
|
||||||
they finish, apply, and are culled later if no longer desired. Collision level-0 only; water level-0
|
desired set: the game thread flips the request token, the worker returns an aborted result, and the
|
||||||
only; shadows off for level≥2. **Decorations are NO LONGER tied to tiles** — they stream on a fixed world
|
queue consumer drops it even if cancellation raced the final enqueue. A result must also match the
|
||||||
|
current generation epoch and desired membership before it can apply. `TransitionHold` therefore
|
||||||
|
protects only already-applied geometry during a LOD transition; obsolete work never becomes loaded
|
||||||
|
geometry. Collision level-0 only; water level-0 only; shadows off for level≥2. **Decorations are NO
|
||||||
|
LONGER tied to tiles** — they stream on a fixed world
|
||||||
grid by distance (§8.5), so they don't pop on LOD swaps. Settings: `VoxelSettings::ClipRadius` (full-res near radius, tiles/level),
|
grid by distance (§8.5), so they don't pop on LOD swaps. Settings: `VoxelSettings::ClipRadius` (full-res near radius, tiles/level),
|
||||||
`MaxClipLevel` (far reach). **NEAR-FIELD GEN COST levers** (`LoadTile`): levels `< FullResClipLevels`
|
`MaxClipLevel` (far reach). **NEAR-FIELD GEN COST levers** (`LoadTile`): levels `< FullResClipLevels`
|
||||||
mesh at full `CHUNK_SIZE` cells (≈35³ `GetDensityAt` incl. margin ring), coarser levels at
|
mesh at full `CHUNK_SIZE` cells (≈35³ `GetDensityAt` incl. margin ring), coarser levels at
|
||||||
@@ -756,11 +766,13 @@ driven by `EditorBrush*` props.
|
|||||||
(stale stamp — removed + returned). The cull then considers ONLY leavers + `TransitionHold`
|
(stale stamp — removed + returned). The cull then considers ONLY leavers + `TransitionHold`
|
||||||
(tiles kept by load-before-unload from earlier crossings) instead of re-scanning EVERY loaded
|
(tiles kept by load-before-unload from earlier crossings) instead of re-scanning EVERY loaded
|
||||||
tile per crossing — that scan was the measured ~1.6 ms/crossing `CullTiles` spike (2026-07-05
|
tile per crossing — that scan was the measured ~1.6 ms/crossing `CullTiles` spike (2026-07-05
|
||||||
trace, plus `BuildDesiredTiles` 0.66 ms on the same frames). In-flight tiles that finish while
|
trace, plus `BuildDesiredTiles` 0.66 ms on the same frames). Pending requests that become leavers
|
||||||
no longer desired are caught at apply time (`ProcessPendingChunks` adds them to the hold);
|
are marked obsolete before replacement submission; `ProcessPendingChunks` reads the token, drops
|
||||||
`UnloadTile` drops hold entries; the settled cull (everything loaded) keeps its full scan as
|
the aborted result, and does not put it in the hold. `ApplyTileResult` independently rejects a
|
||||||
the safety net. `DesiredPending` for the overlap test is now built LAZILY (only when an
|
wrong generation epoch or undesired key. `UnloadTile` drops hold entries; the settled cull
|
||||||
in-range transition candidate exists). **The hold is re-evaluated on a ROTATING BUDGET**
|
(everything loaded) keeps its full scan as the safety net. `DesiredPending` for the overlap test is
|
||||||
|
now built LAZILY (only when an in-range transition candidate exists). **The hold is re-evaluated on
|
||||||
|
a ROTATING BUDGET**
|
||||||
(`TransitionHoldQueue` + cursor, ~256 tiles/crossing): re-scanning the whole hold each
|
(`TransitionHoldQueue` + cursor, ~256 tiles/crossing): re-scanning the whole hold each
|
||||||
crossing degenerates back to the O(loaded) scan whenever streaming never settles (measured
|
crossing degenerates back to the O(loaded) scan whenever streaming never settles (measured
|
||||||
2.47 ms/crossing in the first packaged capture) — keeping a tile a few crossings longer is
|
2.47 ms/crossing in the first packaged capture) — keeping a tile a few crossings longer is
|
||||||
|
|||||||
+13
-11
@@ -47,7 +47,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
|||||||
Tick → UpdateChunksAroundPosition
|
Tick → UpdateChunksAroundPosition
|
||||||
│ (load/unload around player, by distance + LOD)
|
│ (load/unload around player, by distance + LOD)
|
||||||
▼
|
▼
|
||||||
LoadChunk → UE::Tasks::Launch ──────────► background thread
|
LoadTile → UE::Tasks::Launch ──────────► background thread
|
||||||
│
|
│
|
||||||
UVoxelMarchingCubesMesher::GenerateMesh(chunk, step) ◄────────────────┘
|
UVoxelMarchingCubesMesher::GenerateMesh(chunk, step) ◄────────────────┘
|
||||||
│ samples density per cell corner
|
│ samples density per cell corner
|
||||||
@@ -265,12 +265,14 @@ redesign; tile identity lives in `FVoxelTileKey` (VoxelWorld.h).
|
|||||||
|
|
||||||
### 3.5 World orchestrator — `Public/VoxelWorld.h` + `Private/VoxelWorld.cpp`
|
### 3.5 World orchestrator — `Public/VoxelWorld.h` + `Private/VoxelWorld.cpp`
|
||||||
`AVoxelWorld : AActor` — owns everything, drives streaming. Also `FChunkResult` struct
|
`AVoxelWorld : AActor` — owns everything, drives streaming. Also `FChunkResult` struct
|
||||||
(VoxelWorld.h:35) = async task payload (coord, chunk, meshdata, LOD, **Epoch**).
|
(VoxelWorld.h:35) = async task payload (coord, chunk, meshdata, LOD, **Epoch**, request timing,
|
||||||
|
desired-set epoch and obsolete/cancellation state).
|
||||||
|
|
||||||
**Owned objects (UPROPERTY):** `Settings`, `Generator`, `Mesher`, `StrateManager`,
|
**Owned objects (UPROPERTY):** `Settings`, `Generator`, `Mesher`, `StrateManager`,
|
||||||
`DiffLayer` (VoxelWorld.h:55-78). **Storage:** `Chunks` map, `ChunkMeshes` map,
|
`DiffLayer` (VoxelWorld.h:55-78). **Storage:** `Chunks` map, `ChunkMeshes` map,
|
||||||
`ChunkLODs`, `ProcessQueue` (TQueue), `PendingChunkCoord` (TSet) (VoxelWorld.h:85-312).
|
`ChunkLODs`, `ProcessQueue` (TQueue), `PendingChunkCoord` (TSet) (VoxelWorld.h:85-312).
|
||||||
**Async state:** `bShuttingDown`, `ActiveTaskCount` (atomics), `GenerationEpoch`.
|
**Async state:** `bShuttingDown`, `ActiveTaskCount` (atomics), `GenerationEpoch`, and the
|
||||||
|
game-thread `PendingTileCancellation` map whose per-request atomic flags are read-only on workers.
|
||||||
|
|
||||||
**⭐ 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` /
|
||||||
@@ -295,19 +297,19 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
|||||||
| `OnObjectModifiedInEditor` | 58 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
|
| `OnObjectModifiedInEditor` | 58 | Regenerates when a strate asset is edited (if `bLiveEditStrates`). |
|
||||||
| `EndPlay` | 140 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
|
| `EndPlay` | 140 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
|
||||||
| `BeginPlay` | 177 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
|
| `BeginPlay` | 177 | Constructs Generator/Mesher/StrateManager/DiffLayer, wires services, seeds. |
|
||||||
| `Tick` | 220 | `UpdateChunksAroundPosition(player)` + `ProcessPendingChunks()`. |
|
| `Tick` | 220 | `UpdateChunksAroundPosition(player, heading)` + `ProcessPendingChunks()`; the clean LOD0 latency summary is flushed at `EndPlay`. |
|
||||||
| `GetPlayerPosition` | 231 | Pawn position or zero. (`GetLODForChunk`/`LODToStep`/`IsChunkInRange` removed — dead since the clipmap.) |
|
| `GetPlayerPosition` | 231 | Pawn position or zero. (`GetLODForChunk`/`LODToStep`/`IsChunkInRange` removed — dead since the clipmap.) |
|
||||||
| `ProcessPendingChunks` | 301 | Drains ProcessQueue under per-frame budget; applies each via `ApplyTileResult`. |
|
| `ProcessPendingChunks` | 301 | Drains ProcessQueue under 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`. |
|
||||||
| `ApplyTileResult` | — | **Shared game-thread apply** for one `FChunkResult` (async drain + sync carve): discards stale epochs, 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). |
|
||||||
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): ClassifyTile (T1.d) → `GenerateMesh`/`GenerateSheetMesh` → `BuildTileStreamSet`. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
|
| `GenerateTileResult` | — | **Shared worker-side gen** for one tile (async `LoadTile` task + sync `SyncRemeshTile`): ClassifyTile (T1.d) → `GenerateMesh`/`GenerateSheetMesh` → `BuildTileStreamSet`. Reads Generator/Mesher only → safe on a worker or the game thread; fills `FChunkResult`, no enqueue. |
|
||||||
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. |
|
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread (`GenerateTileResult` + `ApplyTileResult` inline, Cells=CHUNK_SIZE/Step=1 + strate band, no capture). Used for the tile under the brush centre so a carve is visible THIS frame; one full-res gen on the game thread. |
|
||||||
| `UpdateChunksAroundPosition` | 362 | Builds desired set, sorts by distance, loads/unloads, 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, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `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` | 362 | 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, or (F18 `bFarSheetRing`) as a SHEET ring at level MaxClipLevel+span (covered-check vs the MaxClipLevel box; `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. |
|
||||||
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep. |
|
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep; it orders from the true local pawn position and builds `CriticalDesiredTiles` for the occupied/support and heading tiles. |
|
||||||
| `AddAnchorDesiredTiles` | — | **§9.3 multi-anchor:** folds each `FVoxelStreamingAnchor`'s thin level-0 box (`XYRadiusChunks`/`ZBelowChunks`/`ZAboveChunks`) into the SAME desired set (deduped by stamp) 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 (deduped by stamp) 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. |
|
||||||
| `LoadChunk` | 445 | 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). 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`). 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). F18: `Tile.Level > MaxClipLevel` = SHEET tile → routed to `GenerateSheetMesh` (band mid-chunk = strate ref; band unarmed ⇒ empty; MC-ring sampling density, cells capped 128/axis; carries the XY hole `SheetHole*Vox` — hole moves ⇒ overlapping sheets re-queue via `BandRemeshQueue`, see `UpdateChunksAroundPosition`). §8.10. |
|
| `LoadTile` | 445 | 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`). 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). F18: `Tile.Level > MaxClipLevel` = SHEET tile → routed to `GenerateSheetMesh` (band mid-chunk = strate ref; band unarmed ⇒ empty; MC-ring sampling density, cells capped 128/axis; carries the XY hole `SheetHole*Vox` — hole moves ⇒ overlapping sheets re-queue via `BandRemeshQueue`, see `UpdateChunksAroundPosition`). §8.10. |
|
||||||
| `UnloadTile` | — | Clears tile state; 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` | — | 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. **§9.4:** `SetVisibility(false)` when the tile is in `CollisionOnlyTiles` (anchor-only, hidden — collision still cooks). §8.10 + ARCHITECTURE SurfaceWorld row. |
|
| `ApplyMeshToTile` | — | 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. **§9.4:** `SetVisibility(false)` when the tile is in `CollisionOnlyTiles` (anchor-only, hidden — collision still cooks). §8.10 + ARCHITECTURE SurfaceWorld row. |
|
||||||
| `AcquireTileComponent` / `ReleaseTileComponent` | — | **T2.c component pool** (`TileComponentPool`, bounded): park on unload (geometry+collision stripped, hidden, stays registered), pop on apply — no `NewObject`/`RegisterComponent`/GC churn during travel & regen. §8.10. |
|
| `AcquireTileComponent` / `ReleaseTileComponent` | — | **T2.c component pool** (`TileComponentPool`, bounded): park on unload (geometry+collision stripped, hidden, stays registered), pop on apply — no `NewObject`/`RegisterComponent`/GC churn during travel & regen. §8.10. |
|
||||||
| `GetStrateAtPosition` | 965 | Gameplay query → strate index. |
|
| `GetStrateAtPosition` | 965 | Gameplay query → strate index. |
|
||||||
@@ -768,7 +770,7 @@ Stage order (negative=solid throughout). Each stage's anchor:
|
|||||||
| View distance / task budget / LOD distances | `VoxelSettings.h` (no recompile of logic — data asset). |
|
| View distance / task budget / LOD distances | `VoxelSettings.h` (no recompile of logic — data asset). |
|
||||||
| LOD step mapping | `AVoxelWorld::LODToStep` VoxelWorld.cpp:268; `GetLODForChunk` :242. |
|
| LOD step mapping | `AVoxelWorld::LODToStep` VoxelWorld.cpp:268; `GetLODForChunk` :242. |
|
||||||
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:362. |
|
| How chunks stream in/out | `UpdateChunksAroundPosition` VoxelWorld.cpp:362. |
|
||||||
| Async threading / stale-result handling | `LoadChunk` :445, `ProcessPendingChunks` :301, Epoch logic. |
|
| Async threading / stale-result handling | `LoadTile` :445, `ProcessPendingChunks` :301, generation Epoch + per-request cancellation token. |
|
||||||
| Add a new cave feature / terrain op | Add enum in `VoxelTerrainOpDefinition.h:36`, params there, `ApplyTo` (.cpp:6), transport fields in `FStrateGenerationParams`, consume it in a new Step inside `GetDensityWithParams`. |
|
| Add a new cave feature / terrain op | Add enum in `VoxelTerrainOpDefinition.h:36`, params there, `ApplyTo` (.cpp:6), transport fields in `FStrateGenerationParams`, consume it in a new Step inside `GetDensityWithParams`. |
|
||||||
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :47 / `EvaluateSDFCached` :757. |
|
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :47 / `EvaluateSDFCached` :757. |
|
||||||
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:1241. |
|
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:1241. |
|
||||||
|
|||||||
@@ -480,9 +480,6 @@ namespace VoxelDensityProfile
|
|||||||
case ECounter::TileClassifyAllSolid: return TEXT("TileClassifyAllSolid");
|
case ECounter::TileClassifyAllSolid: return TEXT("TileClassifyAllSolid");
|
||||||
case ECounter::TileClassifyAllAir: return TEXT("TileClassifyAllAir");
|
case ECounter::TileClassifyAllAir: return TEXT("TileClassifyAllAir");
|
||||||
case ECounter::TileClassifyMixed: return TEXT("TileClassifyMixed");
|
case ECounter::TileClassifyMixed: return TEXT("TileClassifyMixed");
|
||||||
case ECounter::TileVerdictCacheHits: return TEXT("TileVerdictCacheHits");
|
|
||||||
case ECounter::TileVerdictRegionHits: return TEXT("TileVerdictRegionHits");
|
|
||||||
case ECounter::TileVerdictCacheStores: return TEXT("TileVerdictCacheStores");
|
|
||||||
case ECounter::FusedTunnelSamples: return TEXT("FusedTunnelSamples");
|
case ECounter::FusedTunnelSamples: return TEXT("FusedTunnelSamples");
|
||||||
case ECounter::FusedTunnelDetailSamples: return TEXT("FusedTunnelDetailSamples");
|
case ECounter::FusedTunnelDetailSamples: return TEXT("FusedTunnelDetailSamples");
|
||||||
case ECounter::FusedTunnelDetailSkipped: return TEXT("FusedTunnelDetailSkipped");
|
case ECounter::FusedTunnelDetailSkipped: return TEXT("FusedTunnelDetailSkipped");
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
#include "HAL/PlatformTime.h"
|
#include "HAL/PlatformTime.h"
|
||||||
#include "HAL/IConsoleManager.h"
|
#include "HAL/IConsoleManager.h"
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include "Misc/ScopeLock.h"
|
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
#include "VoxelStrateComposer.h"
|
#include "VoxelStrateComposer.h"
|
||||||
@@ -56,6 +55,7 @@ namespace
|
|||||||
GVoxelForgeUseFusedEvaluator = CommandLineValue;
|
GVoxelForgeUseFusedEvaluator = CommandLineValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//=============================================================================
|
//=============================================================================
|
||||||
@@ -1532,169 +1532,6 @@ UVoxelGenerator::UVoxelGenerator()
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelGenerator::InvalidateTileVerdictCache() const
|
|
||||||
{
|
|
||||||
FScopeLock Lock(&TileVerdictCacheLock);
|
|
||||||
TileVerdictCache.Reset();
|
|
||||||
TileVerdictCacheClock = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool UVoxelGenerator::FindTileVerdict(
|
|
||||||
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
|
|
||||||
int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion,
|
|
||||||
uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites,
|
|
||||||
EVoxelTileClass& OutVerdict, bool& bOutRegionHit) const
|
|
||||||
{
|
|
||||||
bOutRegionHit = false;
|
|
||||||
FScopeLock Lock(&TileVerdictCacheLock);
|
|
||||||
++TileVerdictCacheClock;
|
|
||||||
|
|
||||||
const int64 RequestMaxX = static_cast<int64>(OriginVoxels.X)
|
|
||||||
+ static_cast<int64>(CellsPerAxis) * Step;
|
|
||||||
const int64 RequestMaxY = static_cast<int64>(OriginVoxels.Y)
|
|
||||||
+ static_cast<int64>(CellsPerAxis) * Step;
|
|
||||||
const int64 RequestMaxZ = static_cast<int64>(OriginVoxels.Z)
|
|
||||||
+ static_cast<int64>(CellsPerAxis) * Step;
|
|
||||||
|
|
||||||
int32 BestIndex = INDEX_NONE;
|
|
||||||
bool bBestExact = false;
|
|
||||||
for (int32 Index = 0; Index < TileVerdictCache.Num(); ++Index)
|
|
||||||
{
|
|
||||||
FTileVerdictCacheEntry& Entry = TileVerdictCache[Index];
|
|
||||||
if (Entry.GeneratorSeed != Seed
|
|
||||||
|| Entry.ManagerWorldSeed != ManagerWorldSeed
|
|
||||||
|| Entry.OriginSpineRadius != OriginSpineRadius
|
|
||||||
|| Entry.WorldRadiusVoxels != WorldRadiusVoxels
|
|
||||||
|| Entry.EdgeSealThickness != EdgeSealThickness
|
|
||||||
|| Entry.ManagerLifetimeId != (StrateManager ? StrateManager->GetCacheLifetimeId() : 0)
|
|
||||||
|| Entry.LayoutVersion != LayoutVersion
|
|
||||||
|| Entry.DiffVersion != DiffVersion
|
|
||||||
|| Entry.ParamsFingerprint != ParamsFingerprint
|
|
||||||
|| Entry.PassageSupportFloorWrites != PassageSupportFloorWrites
|
|
||||||
|| Entry.Step != Step
|
|
||||||
|| Entry.CellsPerAxis < CellsPerAxis)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int64 EntryMaxX = static_cast<int64>(Entry.Origin.X)
|
|
||||||
+ static_cast<int64>(Entry.CellsPerAxis) * Step;
|
|
||||||
const int64 EntryMaxY = static_cast<int64>(Entry.Origin.Y)
|
|
||||||
+ static_cast<int64>(Entry.CellsPerAxis) * Step;
|
|
||||||
const int64 EntryMaxZ = static_cast<int64>(Entry.Origin.Z)
|
|
||||||
+ static_cast<int64>(Entry.CellsPerAxis) * Step;
|
|
||||||
const int64 DeltaX = static_cast<int64>(OriginVoxels.X) - Entry.Origin.X;
|
|
||||||
const int64 DeltaY = static_cast<int64>(OriginVoxels.Y) - Entry.Origin.Y;
|
|
||||||
const int64 DeltaZ = static_cast<int64>(OriginVoxels.Z) - Entry.Origin.Z;
|
|
||||||
if (DeltaX < 0 || DeltaY < 0 || DeltaZ < 0
|
|
||||||
|| (DeltaX % Step) != 0 || (DeltaY % Step) != 0 || (DeltaZ % Step) != 0
|
|
||||||
|| RequestMaxX > EntryMaxX || RequestMaxY > EntryMaxY || RequestMaxZ > EntryMaxZ)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool bExact = Entry.Origin == OriginVoxels
|
|
||||||
&& Entry.CellsPerAxis == CellsPerAxis;
|
|
||||||
if (BestIndex == INDEX_NONE || (bExact && !bBestExact)
|
|
||||||
|| (bExact == bBestExact
|
|
||||||
&& Entry.CellsPerAxis < TileVerdictCache[BestIndex].CellsPerAxis))
|
|
||||||
{
|
|
||||||
BestIndex = Index;
|
|
||||||
bBestExact = bExact;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BestIndex == INDEX_NONE)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
FTileVerdictCacheEntry& Entry = TileVerdictCache[BestIndex];
|
|
||||||
Entry.LastUse = TileVerdictCacheClock;
|
|
||||||
OutVerdict = Entry.Verdict;
|
|
||||||
bOutRegionHit = !(Entry.Origin == OriginVoxels
|
|
||||||
&& Entry.CellsPerAxis == CellsPerAxis);
|
|
||||||
return OutVerdict != EVoxelTileClass::Mixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UVoxelGenerator::StoreTileVerdict(
|
|
||||||
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
|
|
||||||
int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion,
|
|
||||||
uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites,
|
|
||||||
EVoxelTileClass Verdict) const
|
|
||||||
{
|
|
||||||
if (Verdict == EVoxelTileClass::Mixed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FScopeLock Lock(&TileVerdictCacheLock);
|
|
||||||
++TileVerdictCacheClock;
|
|
||||||
|
|
||||||
int32 SlotIndex = INDEX_NONE;
|
|
||||||
int32 VictimIndex = INDEX_NONE;
|
|
||||||
uint32 OldestUse = MAX_uint32;
|
|
||||||
for (int32 Index = 0; Index < TileVerdictCache.Num(); ++Index)
|
|
||||||
{
|
|
||||||
FTileVerdictCacheEntry& Entry = TileVerdictCache[Index];
|
|
||||||
if (Entry.GeneratorSeed == Seed
|
|
||||||
&& Entry.ManagerWorldSeed == ManagerWorldSeed
|
|
||||||
&& Entry.OriginSpineRadius == OriginSpineRadius
|
|
||||||
&& Entry.WorldRadiusVoxels == WorldRadiusVoxels
|
|
||||||
&& Entry.EdgeSealThickness == EdgeSealThickness
|
|
||||||
&& Entry.ManagerLifetimeId == (StrateManager ? StrateManager->GetCacheLifetimeId() : 0)
|
|
||||||
&& Entry.LayoutVersion == LayoutVersion
|
|
||||||
&& Entry.DiffVersion == DiffVersion
|
|
||||||
&& Entry.ParamsFingerprint == ParamsFingerprint
|
|
||||||
&& Entry.PassageSupportFloorWrites == PassageSupportFloorWrites
|
|
||||||
&& Entry.Origin == OriginVoxels
|
|
||||||
&& Entry.Step == Step
|
|
||||||
&& Entry.CellsPerAxis == CellsPerAxis)
|
|
||||||
{
|
|
||||||
SlotIndex = Index;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (Entry.LastUse < OldestUse)
|
|
||||||
{
|
|
||||||
OldestUse = Entry.LastUse;
|
|
||||||
VictimIndex = Index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SlotIndex == INDEX_NONE)
|
|
||||||
{
|
|
||||||
if (TileVerdictCache.Num() < TileVerdictCacheCapacity)
|
|
||||||
{
|
|
||||||
SlotIndex = TileVerdictCache.AddDefaulted();
|
|
||||||
}
|
|
||||||
else if (VictimIndex != INDEX_NONE)
|
|
||||||
{
|
|
||||||
SlotIndex = VictimIndex;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FTileVerdictCacheEntry& Entry = TileVerdictCache[SlotIndex];
|
|
||||||
Entry.Origin = OriginVoxels;
|
|
||||||
Entry.Step = Step;
|
|
||||||
Entry.CellsPerAxis = CellsPerAxis;
|
|
||||||
Entry.GeneratorSeed = Seed;
|
|
||||||
Entry.ManagerWorldSeed = ManagerWorldSeed;
|
|
||||||
Entry.OriginSpineRadius = OriginSpineRadius;
|
|
||||||
Entry.WorldRadiusVoxels = WorldRadiusVoxels;
|
|
||||||
Entry.EdgeSealThickness = EdgeSealThickness;
|
|
||||||
Entry.ManagerLifetimeId = StrateManager ? StrateManager->GetCacheLifetimeId() : 0;
|
|
||||||
Entry.LayoutVersion = LayoutVersion;
|
|
||||||
Entry.DiffVersion = DiffVersion;
|
|
||||||
Entry.ParamsFingerprint = ParamsFingerprint;
|
|
||||||
Entry.PassageSupportFloorWrites = PassageSupportFloorWrites;
|
|
||||||
Entry.Verdict = Verdict;
|
|
||||||
Entry.LastUse = TileVerdictCacheClock;
|
|
||||||
}
|
|
||||||
|
|
||||||
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
||||||
{
|
{
|
||||||
// Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas
|
// Les paramètres globaux sont copiés ici une seule fois : le chemin voxel ne doit pas
|
||||||
@@ -1703,7 +1540,6 @@ void UVoxelGenerator::InitializeSettings(const UVoxelSettings* Settings)
|
|||||||
OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 14.0f;
|
OriginSpineRadius = Settings ? Settings->GetEffectiveOriginSpineRadius() : 14.0f;
|
||||||
WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f;
|
WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f;
|
||||||
EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 64.0f;
|
EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 64.0f;
|
||||||
InvalidateTileVerdictCache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UVoxelGenerator::BeginDensityBlock(FIntVector OriginVoxels, int32 Step,
|
void UVoxelGenerator::BeginDensityBlock(FIntVector OriginVoxels, int32 Step,
|
||||||
@@ -5115,48 +4951,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
|||||||
VoxelDensityProfile::AddCounter(
|
VoxelDensityProfile::AddCounter(
|
||||||
VoxelDensityProfile::ECounter::TileClassifyCalls);
|
VoxelDensityProfile::ECounter::TileClassifyCalls);
|
||||||
|
|
||||||
const uint64 ParamsFingerprint = StrateManager
|
|
||||||
? StrateManager->GetGenerationParamsFingerprint() : 0;
|
|
||||||
const bool bCacheable = ParamsFingerprint != 0;
|
|
||||||
const uint32 LayoutVersion = StrateManager ? StrateManager->GetLayoutVersion() : 0;
|
|
||||||
const uint32 DiffVersion = DiffLayer ? DiffLayer->GetModsVersion() : 0;
|
|
||||||
const int32 ManagerWorldSeed = StrateManager ? StrateManager->GetWorldSeed() : 0;
|
|
||||||
const uint8 PassageSupportFloorWrites = StrateManager
|
|
||||||
&& StrateManager->ArePassageSupportFloorWritesEnabledForDiagnostics() ? 1u : 0u;
|
|
||||||
|
|
||||||
EVoxelTileClass CachedVerdict = EVoxelTileClass::Mixed;
|
|
||||||
bool bRegionHit = false;
|
|
||||||
if (bCacheable
|
|
||||||
&& FindTileVerdict(
|
|
||||||
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis,
|
|
||||||
ManagerWorldSeed, LayoutVersion, DiffVersion, ParamsFingerprint,
|
|
||||||
PassageSupportFloorWrites, CachedVerdict, bRegionHit))
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TileVerdictCacheHits);
|
|
||||||
if (bRegionHit)
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TileVerdictRegionHits);
|
|
||||||
}
|
|
||||||
if (CachedVerdict == EVoxelTileClass::AllSolid)
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TileClassifyAllSolid);
|
|
||||||
}
|
|
||||||
else if (CachedVerdict == EVoxelTileClass::AllAir)
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TileClassifyAllAir);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TileClassifyMixed);
|
|
||||||
}
|
|
||||||
return CachedVerdict;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EVoxelTileClass Verdict = ClassifyTileUncached(
|
const EVoxelTileClass Verdict = ClassifyTileUncached(
|
||||||
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, OutStats);
|
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, OutStats);
|
||||||
if (Verdict == EVoxelTileClass::AllSolid)
|
if (Verdict == EVoxelTileClass::AllSolid)
|
||||||
@@ -5175,15 +4969,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
|||||||
VoxelDensityProfile::ECounter::TileClassifyMixed);
|
VoxelDensityProfile::ECounter::TileClassifyMixed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bCacheable && Verdict != EVoxelTileClass::Mixed)
|
|
||||||
{
|
|
||||||
StoreTileVerdict(
|
|
||||||
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis,
|
|
||||||
ManagerWorldSeed, LayoutVersion, DiffVersion, ParamsFingerprint,
|
|
||||||
PassageSupportFloorWrites, Verdict);
|
|
||||||
VoxelDensityProfile::AddCounter(
|
|
||||||
VoxelDensityProfile::ECounter::TileVerdictCacheStores);
|
|
||||||
}
|
|
||||||
return Verdict;
|
return Verdict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,8 +98,6 @@ namespace
|
|||||||
int32 AllAir = 0;
|
int32 AllAir = 0;
|
||||||
int32 AllSolid = 0;
|
int32 AllSolid = 0;
|
||||||
int32 Mixed = 0;
|
int32 Mixed = 0;
|
||||||
int32 CacheHits = 0;
|
|
||||||
int32 RegionHits = 0;
|
|
||||||
int64 Triangles = 0;
|
int64 Triangles = 0;
|
||||||
|
|
||||||
for (const FTileSample& Sample : Samples)
|
for (const FTileSample& Sample : Samples)
|
||||||
@@ -110,8 +108,6 @@ namespace
|
|||||||
if (Sample.Verdict == 0) ++Mixed;
|
if (Sample.Verdict == 0) ++Mixed;
|
||||||
else if (Sample.Verdict == 1) ++AllSolid;
|
else if (Sample.Verdict == 1) ++AllSolid;
|
||||||
else if (Sample.Verdict == 2) ++AllAir;
|
else if (Sample.Verdict == 2) ++AllAir;
|
||||||
if (Sample.bCacheHit) ++CacheHits;
|
|
||||||
if (Sample.bRegionHit) ++RegionHits;
|
|
||||||
Triangles += Sample.Triangles;
|
Triangles += Sample.Triangles;
|
||||||
Request.Add(Sample.RequestToApplySeconds);
|
Request.Add(Sample.RequestToApplySeconds);
|
||||||
Queue.Add(Sample.QueueWaitSeconds);
|
Queue.Add(Sample.QueueWaitSeconds);
|
||||||
@@ -122,14 +118,14 @@ namespace
|
|||||||
}
|
}
|
||||||
|
|
||||||
return FString::Printf(
|
return FString::Printf(
|
||||||
TEXT("{\"count\":%d,\"empty\":%d,\"all_air\":%d,\"all_solid\":%d,\"mixed\":%d,\"cache_hits\":%d,\"region_hits\":%d,\"triangles\":%lld,"
|
TEXT("{\"count\":%d,\"empty\":%d,\"all_air\":%d,\"all_solid\":%d,\"mixed\":%d,\"triangles\":%lld,"
|
||||||
"\"request_apply_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
"\"request_apply_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||||
"\"queue_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
"\"queue_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||||
"\"generation_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
"\"generation_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||||
"\"classify_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
"\"classify_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||||
"\"mesh_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
"\"mesh_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||||
"\"apply_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s}}"),
|
"\"apply_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s}}"),
|
||||||
Count, Empty, AllAir, AllSolid, Mixed, CacheHits, RegionHits, static_cast<long long>(Triangles),
|
Count, Empty, AllAir, AllSolid, Mixed, static_cast<long long>(Triangles),
|
||||||
*Number(Percentile(Request, 0.50)), *Number(Percentile(Request, 0.95)), *Number(Maximum(Request)),
|
*Number(Percentile(Request, 0.50)), *Number(Percentile(Request, 0.95)), *Number(Maximum(Request)),
|
||||||
*Number(Percentile(Queue, 0.50)), *Number(Percentile(Queue, 0.95)), *Number(Maximum(Queue)),
|
*Number(Percentile(Queue, 0.50)), *Number(Percentile(Queue, 0.95)), *Number(Maximum(Queue)),
|
||||||
*Number(Percentile(Generation, 0.50)), *Number(Percentile(Generation, 0.95)), *Number(Maximum(Generation)),
|
*Number(Percentile(Generation, 0.50)), *Number(Percentile(Generation, 0.95)), *Number(Maximum(Generation)),
|
||||||
@@ -264,11 +260,10 @@ 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("\"level\":%d,\"verdict\":%d,\"empty\":%d,\"cache_hit\":%d,\"region_hit\":%d,\"triangles\":%d,"
|
TEXT("\"level\":%d,\"verdict\":%d,\"empty\":%d,\"triangles\":%d,"
|
||||||
"\"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.Level, Sample.Verdict, Sample.bEmpty ? 1 : 0,
|
Sample.Level, Sample.Verdict, Sample.bEmpty ? 1 : 0, Sample.Triangles,
|
||||||
Sample.bCacheHit ? 1 : 0, Sample.bRegionHit ? 1 : 0, Sample.Triangles,
|
|
||||||
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
||||||
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
||||||
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
#include "HAL/IConsoleManager.h"
|
#include "HAL/IConsoleManager.h"
|
||||||
|
#include "HAL/PlatformMisc.h"
|
||||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
@@ -79,33 +80,6 @@ namespace
|
|||||||
bool GVoxelForgeStreamingBudgetReported = false;
|
bool GVoxelForgeStreamingBudgetReported = false;
|
||||||
bool GVoxelForgeGenerationCapHitReported = false;
|
bool GVoxelForgeGenerationCapHitReported = false;
|
||||||
int32 GVoxelForgeMaxPendingTilesObserved = 0;
|
int32 GVoxelForgeMaxPendingTilesObserved = 0;
|
||||||
bool GVoxelForgeTestStreamingCenterParsed = false;
|
|
||||||
bool GVoxelForgeUseTestStreamingCenter = false;
|
|
||||||
FIntVector GVoxelForgeTestStreamingCenter = FIntVector::ZeroValue;
|
|
||||||
|
|
||||||
void VF_ParseTestStreamingCenter()
|
|
||||||
{
|
|
||||||
if (GVoxelForgeTestStreamingCenterParsed) return;
|
|
||||||
GVoxelForgeTestStreamingCenterParsed = true;
|
|
||||||
|
|
||||||
int32 X = 0, Y = 0, Z = 0;
|
|
||||||
const TCHAR* CommandLine = FCommandLine::Get();
|
|
||||||
const bool bHaveX = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterX="), X);
|
|
||||||
const bool bHaveY = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterY="), Y);
|
|
||||||
const bool bHaveZ = FParse::Value(CommandLine, TEXT("voxel.StreamingTestCenterZ="), Z);
|
|
||||||
if (!bHaveX || !bHaveY || !bHaveZ)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
GVoxelForgeTestStreamingCenter = FIntVector(X, Y, Z);
|
|
||||||
GVoxelForgeUseTestStreamingCenter = true;
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForge] diagnostic fixed streaming center: (%d,%d,%d)"),
|
|
||||||
GVoxelForgeTestStreamingCenter.X,
|
|
||||||
GVoxelForgeTestStreamingCenter.Y,
|
|
||||||
GVoxelForgeTestStreamingCenter.Z);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AVoxelWorld::AVoxelWorld()
|
AVoxelWorld::AVoxelWorld()
|
||||||
@@ -473,8 +447,22 @@ void AVoxelWorld::RegenerateAllChunks()
|
|||||||
// Density volume: bump epoch (drop in-flight fills) + drop data → full refill next Tick.
|
// Density volume: bump epoch (drop in-flight fills) + drop data → full refill next Tick.
|
||||||
if (DensityVolume) { DensityVolume->Reset(); }
|
if (DensityVolume) { DensityVolume->Reset(); }
|
||||||
|
|
||||||
// Clear pending set — stale tasks will be discarded by the epoch check.
|
// Mark any still-running request obsolete. Keep its PendingTiles slot and token map until the
|
||||||
PendingTiles.Empty();
|
// worker/result drains; otherwise a late old result could remove the slot belonging to a new
|
||||||
|
// request for the same key. Live-edit callers normally arrive with generation paused (and no
|
||||||
|
// active tasks), in which case the stale queue can be discarded immediately.
|
||||||
|
for (TPair<FVoxelTileKey, TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>>& Pair :
|
||||||
|
PendingTileCancellation)
|
||||||
|
{
|
||||||
|
if (Pair.Value) { Pair.Value->bObsolete.store(true, std::memory_order_release); }
|
||||||
|
}
|
||||||
|
if (ActiveTaskCount.load(std::memory_order_relaxed) == 0)
|
||||||
|
{
|
||||||
|
FChunkResult StaleResult;
|
||||||
|
while (ProcessQueue.Dequeue(StaleResult)) {}
|
||||||
|
PendingTiles.Empty();
|
||||||
|
PendingTileCancellation.Empty();
|
||||||
|
}
|
||||||
// Tiles are already destroyed above — drop any deferred-teardown keys so the drain doesn't
|
// Tiles are already destroyed above — drop any deferred-teardown keys so the drain doesn't
|
||||||
// try to UnloadTile coords that no longer exist.
|
// try to UnloadTile coords that no longer exist.
|
||||||
PendingUnload.Empty();
|
PendingUnload.Empty();
|
||||||
@@ -489,7 +477,9 @@ void AVoxelWorld::RegenerateAllChunks()
|
|||||||
LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
bAllChunksLoaded = false;
|
bAllChunksLoaded = false;
|
||||||
DesiredSorted.Reset();
|
DesiredSorted.Reset();
|
||||||
|
CriticalDesiredTiles.Reset();
|
||||||
DesiredStamped.Reset();
|
DesiredStamped.Reset();
|
||||||
|
++DesiredEpoch;
|
||||||
TransitionHold.Reset();
|
TransitionHold.Reset();
|
||||||
TransitionHoldQueue.Reset();
|
TransitionHoldQueue.Reset();
|
||||||
TransitionHoldCursor = 0;
|
TransitionHoldCursor = 0;
|
||||||
@@ -970,13 +960,17 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|||||||
}
|
}
|
||||||
FPlatformProcess::Yield(); // Give CPU to other threads
|
FPlatformProcess::Yield(); // Give CPU to other threads
|
||||||
}
|
}
|
||||||
UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] EndPlay: workers drained in %.6fs"),
|
|
||||||
FPlatformTime::Seconds() - EndPlayStartSeconds);
|
|
||||||
|
|
||||||
// Drain any queued results
|
// Drain any queued results
|
||||||
FChunkResult Discard;
|
FChunkResult Discard;
|
||||||
while (ProcessQueue.Dequeue(Discard)) {}
|
while (ProcessQueue.Dequeue(Discard))
|
||||||
|
{
|
||||||
|
if (Discard.bObsolete) { ++ObsoleteTileAbortCount; }
|
||||||
|
}
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("[VoxelWorld] EndPlay: workers drained in %.6fs; obsolete_tile_aborts=%d"),
|
||||||
|
FPlatformTime::Seconds() - EndPlayStartSeconds, ObsoleteTileAbortCount);
|
||||||
|
LogStreamingLatencySummary();
|
||||||
PendingTiles.Empty();
|
PendingTiles.Empty();
|
||||||
|
PendingTileCancellation.Empty();
|
||||||
PendingUnload.Empty();
|
PendingUnload.Empty();
|
||||||
DirtyRemeshQueue.Empty();
|
DirtyRemeshQueue.Empty();
|
||||||
BandRemeshQueue.Empty();
|
BandRemeshQueue.Empty();
|
||||||
@@ -1034,6 +1028,10 @@ void AVoxelWorld::BeginPlay()
|
|||||||
VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay"));
|
VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay"));
|
||||||
Super::BeginPlay();
|
Super::BeginPlay();
|
||||||
bShuttingDown.store(false, std::memory_order_relaxed);
|
bShuttingDown.store(false, std::memory_order_relaxed);
|
||||||
|
LOD0ReadySamples.Reset();
|
||||||
|
ObsoleteTileAbortCount = 0;
|
||||||
|
PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
||||||
|
ConfigureHeadlessStreamingTest();
|
||||||
|
|
||||||
// Some headless harnesses load module CVars after the engine's startup-CVar pass. Read the
|
// Some headless harnesses load module CVars after the engine's startup-CVar pass. Read the
|
||||||
// profiling switches explicitly as well, so a reported game profile cannot silently run with
|
// profiling switches explicitly as well, so a reported game profile cannot silently run with
|
||||||
@@ -1216,6 +1214,155 @@ void AVoxelWorld::BeginPlay()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::ConfigureHeadlessStreamingTest()
|
||||||
|
{
|
||||||
|
bHeadlessStreamingTestMovement = false;
|
||||||
|
bHeadlessStreamingTestExitRequested = false;
|
||||||
|
HeadlessStreamingTestMoveAttempts = 0;
|
||||||
|
HeadlessStreamingTestDistanceCm = 0.0;
|
||||||
|
HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
|
||||||
|
HeadlessStreamingTestStartSeconds = 0.0f;
|
||||||
|
HeadlessStreamingTestExitSeconds = 0.0f;
|
||||||
|
HeadlessStreamingTestDirection = FVector::XAxisVector;
|
||||||
|
HeadlessStreamingTestLastActualPosition = FVector::ZeroVector;
|
||||||
|
HeadlessStreamingTestBeginSeconds = 0.0;
|
||||||
|
HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
||||||
|
|
||||||
|
const TCHAR* CommandLine = FCommandLine::Get();
|
||||||
|
int32 MoveValue = 0;
|
||||||
|
const bool bMoveRequested =
|
||||||
|
(FParse::Value(CommandLine, TEXT("voxel.TestMove="), MoveValue) && MoveValue != 0)
|
||||||
|
|| FParse::Param(CommandLine, TEXT("voxel.TestMove"));
|
||||||
|
float Speed = 800.0f;
|
||||||
|
float StartSeconds = 20.0f;
|
||||||
|
float ExitSeconds = 0.0f;
|
||||||
|
FParse::Value(CommandLine, TEXT("voxel.TestMoveSpeedCmPerSecond="), Speed);
|
||||||
|
FParse::Value(CommandLine, TEXT("voxel.TestMoveStartSeconds="), StartSeconds);
|
||||||
|
FParse::Value(CommandLine, TEXT("voxel.TestExitSeconds="), ExitSeconds);
|
||||||
|
|
||||||
|
if (bMoveRequested)
|
||||||
|
{
|
||||||
|
FString DirectionText;
|
||||||
|
if (FParse::Value(CommandLine, TEXT("voxel.TestMoveDirection="), DirectionText))
|
||||||
|
{
|
||||||
|
TArray<FString> Components;
|
||||||
|
DirectionText.ParseIntoArray(Components, TEXT(","), true);
|
||||||
|
if (Components.Num() == 3)
|
||||||
|
{
|
||||||
|
HeadlessStreamingTestDirection = FVector(
|
||||||
|
FCString::Atof(*Components[0]),
|
||||||
|
FCString::Atof(*Components[1]),
|
||||||
|
FCString::Atof(*Components[2]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HeadlessStreamingTestDirection = HeadlessStreamingTestDirection.GetSafeNormal();
|
||||||
|
if (HeadlessStreamingTestDirection.IsNearlyZero())
|
||||||
|
{
|
||||||
|
HeadlessStreamingTestDirection = FVector::XAxisVector;
|
||||||
|
}
|
||||||
|
HeadlessStreamingTestSpeedCmPerSecond = FMath::Max(Speed, 0.0f);
|
||||||
|
HeadlessStreamingTestStartSeconds = FMath::Max(StartSeconds, 0.0f);
|
||||||
|
bHeadlessStreamingTestMovement = HeadlessStreamingTestSpeedCmPerSecond > 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeadlessStreamingTestExitSeconds = FMath::Max(ExitSeconds, 0.0f);
|
||||||
|
if (bHeadlessStreamingTestMovement || HeadlessStreamingTestExitSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
HeadlessStreamingTestBeginSeconds = FPlatformTime::Seconds();
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeStreamingTest] clean=1 movement=%d speed_cm_s=%.3f start_s=%.3f "
|
||||||
|
"exit_s=%.3f direction=(%.3f,%.3f,%.3f)"),
|
||||||
|
bHeadlessStreamingTestMovement ? 1 : 0,
|
||||||
|
HeadlessStreamingTestSpeedCmPerSecond,
|
||||||
|
HeadlessStreamingTestStartSeconds,
|
||||||
|
HeadlessStreamingTestExitSeconds,
|
||||||
|
HeadlessStreamingTestDirection.X,
|
||||||
|
HeadlessStreamingTestDirection.Y,
|
||||||
|
HeadlessStreamingTestDirection.Z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::AdvanceHeadlessStreamingTest(
|
||||||
|
FVector& InOutPlayerPosition, FVector& InOutPlayerHeading, APawn* PlayerPawn)
|
||||||
|
{
|
||||||
|
if (!bHeadlessStreamingTestMovement || !PlayerPawn || HeadlessStreamingTestBeginSeconds <= 0.0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double ElapsedSeconds = FPlatformTime::Seconds() - HeadlessStreamingTestBeginSeconds;
|
||||||
|
if (ElapsedSeconds < static_cast<double>(HeadlessStreamingTestStartSeconds))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The headless game runs uncapped, so simulation DeltaTime is not a stable movement clock. Use
|
||||||
|
// wall time for this explicit harness path; both A/B runs then cover exactly the same distance
|
||||||
|
// regardless of how much work the scheduler is doing per frame.
|
||||||
|
const double PreviousElapsedSeconds = FMath::Max(
|
||||||
|
HeadlessStreamingTestLastElapsedSeconds,
|
||||||
|
static_cast<double>(HeadlessStreamingTestStartSeconds));
|
||||||
|
const double MoveSeconds = FMath::Max(0.0, ElapsedSeconds - PreviousElapsedSeconds);
|
||||||
|
HeadlessStreamingTestLastElapsedSeconds = ElapsedSeconds;
|
||||||
|
if (MoveSeconds <= 0.0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector NewPosition = InOutPlayerPosition
|
||||||
|
+ HeadlessStreamingTestDirection
|
||||||
|
* HeadlessStreamingTestSpeedCmPerSecond
|
||||||
|
* static_cast<float>(MoveSeconds);
|
||||||
|
const bool bMoved = PlayerPawn->SetActorLocation(
|
||||||
|
NewPosition, false, nullptr, ETeleportType::TeleportPhysics);
|
||||||
|
++HeadlessStreamingTestMoveAttempts;
|
||||||
|
if (bMoved)
|
||||||
|
{
|
||||||
|
const FVector ActualPosition = PlayerPawn->GetActorLocation();
|
||||||
|
HeadlessStreamingTestDistanceCm += FVector::Dist(InOutPlayerPosition, ActualPosition);
|
||||||
|
HeadlessStreamingTestLastActualPosition = ActualPosition;
|
||||||
|
InOutPlayerPosition = ActualPosition;
|
||||||
|
InOutPlayerHeading = HeadlessStreamingTestDirection * HeadlessStreamingTestSpeedCmPerSecond;
|
||||||
|
|
||||||
|
// The explicit -nullrhi harness moves the pawn from this world's pre-physics tick. Keep
|
||||||
|
// the template character movement component from applying its own zero-input/gravity step
|
||||||
|
// afterward and undoing the synthetic teleport before the next scheduler observation.
|
||||||
|
if (ACharacter* Character = Cast<ACharacter>(PlayerPawn))
|
||||||
|
{
|
||||||
|
if (UCharacterMovementComponent* CharacterMovement = Character->GetCharacterMovement())
|
||||||
|
{
|
||||||
|
CharacterMovement->StopMovementImmediately();
|
||||||
|
CharacterMovement->SetMovementMode(MOVE_None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::MaybeFinishHeadlessStreamingTest()
|
||||||
|
{
|
||||||
|
if (bHeadlessStreamingTestExitRequested || HeadlessStreamingTestExitSeconds <= 0.0f
|
||||||
|
|| HeadlessStreamingTestBeginSeconds <= 0.0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FPlatformTime::Seconds() - HeadlessStreamingTestBeginSeconds
|
||||||
|
< static_cast<double>(HeadlessStreamingTestExitSeconds))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bHeadlessStreamingTestExitRequested = true;
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeStreamingTest] complete; moves=%d distance_m=%.6f last_position=(%.1f,%.1f,%.1f); requesting graceful exit after clean session"),
|
||||||
|
HeadlessStreamingTestMoveAttempts,
|
||||||
|
HeadlessStreamingTestDistanceCm / 100.0,
|
||||||
|
HeadlessStreamingTestLastActualPosition.X,
|
||||||
|
HeadlessStreamingTestLastActualPosition.Y,
|
||||||
|
HeadlessStreamingTestLastActualPosition.Z);
|
||||||
|
FPlatformMisc::RequestExit(false, TEXT("VoxelForge headless streaming test complete"));
|
||||||
|
}
|
||||||
|
|
||||||
void AVoxelWorld::Tick(float DeltaTime)
|
void AVoxelWorld::Tick(float DeltaTime)
|
||||||
{
|
{
|
||||||
Super::Tick(DeltaTime);
|
Super::Tick(DeltaTime);
|
||||||
@@ -1225,8 +1372,15 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
const bool bHasPlayer = TryGetPlayerPosition(PlayerLastPos, &PlayerPawn);
|
const bool bHasPlayer = TryGetPlayerPosition(PlayerLastPos, &PlayerPawn);
|
||||||
|
|
||||||
if (bHasPlayer) {
|
if (bHasPlayer) {
|
||||||
|
// Capture velocity before the collision gate can deliberately zero it while the support
|
||||||
|
// tile is cooking. The scheduler uses this as the movement heading for its next-tile
|
||||||
|
// critical prefix; a stationary pawn falls back to actor forward in BuildDesiredTiles.
|
||||||
|
FVector PlayerHeading = PlayerPawn ? PlayerPawn->GetVelocity() : FVector::ZeroVector;
|
||||||
|
AdvanceHeadlessStreamingTest(PlayerLastPos, PlayerHeading, PlayerPawn);
|
||||||
|
PeakObservedPawnSpeedCmPerSecond = FMath::Max(
|
||||||
|
PeakObservedPawnSpeedCmPerSecond, PlayerHeading.Size2D());
|
||||||
UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime);
|
UpdatePawnCollisionGate(PlayerPawn, PlayerLastPos, DeltaTime);
|
||||||
UpdateChunksAroundPosition(PlayerLastPos);
|
UpdateChunksAroundPosition(PlayerLastPos, PlayerPawn, PlayerHeading);
|
||||||
if (AtmosphereManager)
|
if (AtmosphereManager)
|
||||||
{
|
{
|
||||||
AtmosphereManager->UpdateForPlayer(PlayerLastPos);
|
AtmosphereManager->UpdateForPlayer(PlayerLastPos);
|
||||||
@@ -1320,13 +1474,15 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
Max.Z = FMath::Max(Max.Z, Key.Coord.Z);
|
Max.Z = FMath::Max(Max.Z, Key.Coord.Z);
|
||||||
}
|
}
|
||||||
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_state"), FString::Printf(
|
VoxelForgeStartupTrace::RecordEvent(TEXT("steady_state"), FString::Printf(
|
||||||
TEXT("\"desired_tiles\":%d,\"loaded_tiles\":%d,\"collision_ready_tiles\":%d,"
|
TEXT("\"desired_tiles\":%d,\"loaded_tiles\":%d,\"collision_ready_tiles\":%d,"
|
||||||
"\"collision_not_required_tiles\":%d,\"pending_collision_cooks\":%d,"
|
"\"collision_not_required_tiles\":%d,\"pending_collision_cooks\":%d,"
|
||||||
"\"pawn_collision_gate_engaged\":%d,"
|
"\"pawn_collision_gate_engaged\":%d,"
|
||||||
|
"\"obsolete_tile_aborts\":%d,\"desired_epoch\":%u,"
|
||||||
"\"pending_tiles\":%d,\"pending_unload\":%d,\"generation_epoch\":%u"),
|
"\"pending_tiles\":%d,\"pending_unload\":%d,\"generation_epoch\":%u"),
|
||||||
DesiredSorted.Num(), LoadedTiles.Num(), CollisionReadyTiles.Num(),
|
DesiredSorted.Num(), LoadedTiles.Num(), CollisionReadyTiles.Num(),
|
||||||
CollisionNotRequiredTiles.Num(), PendingCollisionCooks.Num(),
|
CollisionNotRequiredTiles.Num(), PendingCollisionCooks.Num(),
|
||||||
bPawnCollisionGateEngaged ? 1 : 0,
|
bPawnCollisionGateEngaged ? 1 : 0,
|
||||||
|
ObsoleteTileAbortCount, DesiredEpoch,
|
||||||
PendingTiles.Num(), PendingUnload.Num(), GenerationEpoch));
|
PendingTiles.Num(), PendingUnload.Num(), GenerationEpoch));
|
||||||
TArray<int32> SteadyLevels;
|
TArray<int32> SteadyLevels;
|
||||||
SteadyLevelCounts.GetKeys(SteadyLevels);
|
SteadyLevelCounts.GetKeys(SteadyLevels);
|
||||||
@@ -1376,6 +1532,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
|||||||
GVoxelForgePendingModificationStrength);
|
GVoxelForgePendingModificationStrength);
|
||||||
}
|
}
|
||||||
ProcessUnloadQueue();
|
ProcessUnloadQueue();
|
||||||
|
MaybeFinishHeadlessStreamingTest();
|
||||||
|
|
||||||
#if ENABLE_DRAW_DEBUG
|
#if ENABLE_DRAW_DEBUG
|
||||||
// Density-volume overlay (step 1a): cyan boxes for solid level-0 cells near the player.
|
// Density-volume overlay (step 1a): cyan boxes for solid level-0 cells near the player.
|
||||||
@@ -1856,6 +2013,95 @@ void AVoxelWorld::HandleRealtimeMeshCollisionBodyUpdated(URealtimeMesh* Mesh, UB
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::RecordLOD0ReadySample(
|
||||||
|
uint64 RequestStartCycles, uint64 GenerationStartCycles, uint64 GenerationEndCycles,
|
||||||
|
uint64 ApplyStartCycles, uint64 ReadyCycles, uint64 CollisionSubmittedCycles)
|
||||||
|
{
|
||||||
|
if (RequestStartCycles == 0 || ReadyCycles < RequestStartCycles)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto SecondsBetween = [](uint64 Later, uint64 Earlier) -> double
|
||||||
|
{
|
||||||
|
return Later >= Earlier ? FPlatformTime::ToSeconds64(Later - Earlier) : 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
FStreamingLatencySample Sample;
|
||||||
|
Sample.RequestToReadySeconds = SecondsBetween(ReadyCycles, RequestStartCycles);
|
||||||
|
Sample.GenerationSeconds = (GenerationStartCycles != 0 && GenerationEndCycles >= GenerationStartCycles)
|
||||||
|
? SecondsBetween(GenerationEndCycles, GenerationStartCycles) : 0.0;
|
||||||
|
|
||||||
|
// Queue wait is deliberately only the time before generation plus the result wait before the
|
||||||
|
// game-thread apply. Mesh upload and the RMC collision cook stay in request-to-ready but are not
|
||||||
|
// mislabeled as scheduler time.
|
||||||
|
const double WorkerQueueSeconds = GenerationStartCycles != 0
|
||||||
|
? SecondsBetween(GenerationStartCycles, RequestStartCycles) : 0.0;
|
||||||
|
const double ResultQueueSeconds = GenerationEndCycles != 0 && ApplyStartCycles != 0
|
||||||
|
? SecondsBetween(ApplyStartCycles, GenerationEndCycles)
|
||||||
|
: (GenerationStartCycles == 0 ? SecondsBetween(ApplyStartCycles, RequestStartCycles) : 0.0);
|
||||||
|
Sample.QueueWaitSeconds = WorkerQueueSeconds + ResultQueueSeconds;
|
||||||
|
Sample.CollisionCookSeconds = CollisionSubmittedCycles != 0
|
||||||
|
? SecondsBetween(ReadyCycles, CollisionSubmittedCycles) : 0.0;
|
||||||
|
LOD0ReadySamples.Add(Sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::LogStreamingLatencySummary() const
|
||||||
|
{
|
||||||
|
TArray<double> RequestToReady;
|
||||||
|
TArray<double> QueueWait;
|
||||||
|
TArray<double> Generation;
|
||||||
|
TArray<double> CollisionCook;
|
||||||
|
RequestToReady.Reserve(LOD0ReadySamples.Num());
|
||||||
|
QueueWait.Reserve(LOD0ReadySamples.Num());
|
||||||
|
Generation.Reserve(LOD0ReadySamples.Num());
|
||||||
|
CollisionCook.Reserve(LOD0ReadySamples.Num());
|
||||||
|
for (const FStreamingLatencySample& Sample : LOD0ReadySamples)
|
||||||
|
{
|
||||||
|
RequestToReady.Add(Sample.RequestToReadySeconds);
|
||||||
|
QueueWait.Add(Sample.QueueWaitSeconds);
|
||||||
|
Generation.Add(Sample.GenerationSeconds);
|
||||||
|
CollisionCook.Add(Sample.CollisionCookSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto Percentile = [](TArray<double> Values, double Fraction) -> double
|
||||||
|
{
|
||||||
|
if (Values.Num() == 0) return 0.0;
|
||||||
|
Values.Sort();
|
||||||
|
const int32 Index = FMath::Clamp(
|
||||||
|
FMath::CeilToInt(Fraction * static_cast<double>(Values.Num() - 1)),
|
||||||
|
0, Values.Num() - 1);
|
||||||
|
return Values[Index];
|
||||||
|
};
|
||||||
|
const auto Maximum = [](const TArray<double>& Values) -> double
|
||||||
|
{
|
||||||
|
double Result = 0.0;
|
||||||
|
for (const double Value : Values) { Result = FMath::Max(Result, Value); }
|
||||||
|
return Result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const double RequestP95 = Percentile(RequestToReady, 0.95);
|
||||||
|
const double TravelP95Metres = RequestP95
|
||||||
|
* static_cast<double>(PeakObservedPawnSpeedCmPerSecond) / 100.0;
|
||||||
|
const bool bDiagnosticsEnabled = GVoxelForgeProfileTileGeneration != 0
|
||||||
|
|| VoxelForgeStartupTrace::IsActive();
|
||||||
|
UE_LOG(LogTemp, Display,
|
||||||
|
TEXT("[VoxelForgeStreamingLatency] mode=%s diagnostics=%s lod0_ready_samples=%d "
|
||||||
|
"request_to_ready_s[p50=%.6f p95=%.6f max=%.6f] "
|
||||||
|
"queue_wait_s[p50=%.6f p95=%.6f max=%.6f] "
|
||||||
|
"generation_s[p50=%.6f p95=%.6f max=%.6f] "
|
||||||
|
"collision_cook_s[p50=%.6f p95=%.6f max=%.6f] "
|
||||||
|
"peak_speed_cm_s=%.3f p95_travel_m=%.6f obsolete_tile_aborts=%d"),
|
||||||
|
bDiagnosticsEnabled ? TEXT("profiled") : TEXT("clean"),
|
||||||
|
bDiagnosticsEnabled ? TEXT("on") : TEXT("off"),
|
||||||
|
LOD0ReadySamples.Num(),
|
||||||
|
Percentile(RequestToReady, 0.50), RequestP95, Maximum(RequestToReady),
|
||||||
|
Percentile(QueueWait, 0.50), Percentile(QueueWait, 0.95), Maximum(QueueWait),
|
||||||
|
Percentile(Generation, 0.50), Percentile(Generation, 0.95), Maximum(Generation),
|
||||||
|
Percentile(CollisionCook, 0.50), Percentile(CollisionCook, 0.95), Maximum(CollisionCook),
|
||||||
|
PeakObservedPawnSpeedCmPerSecond, TravelP95Metres, ObsoleteTileAbortCount);
|
||||||
|
}
|
||||||
|
|
||||||
void AVoxelWorld::HandleTileCollisionCookComplete(
|
void AVoxelWorld::HandleTileCollisionCookComplete(
|
||||||
const FVoxelTileKey& Tile, uint64 SubmissionId, URealtimeMesh* Mesh, uint8 Result)
|
const FVoxelTileKey& Tile, uint64 SubmissionId, URealtimeMesh* Mesh, uint8 Result)
|
||||||
{
|
{
|
||||||
@@ -1893,6 +2139,11 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
|
|||||||
const uint64 MeasuredCollisionSubmittedCycles = Pending->CollisionSubmittedCycles;
|
const uint64 MeasuredCollisionSubmittedCycles = Pending->CollisionSubmittedCycles;
|
||||||
const bool bBodyUpdatedEventSeen = Pending->bBodyUpdatedEventSeen;
|
const bool bBodyUpdatedEventSeen = Pending->bBodyUpdatedEventSeen;
|
||||||
const uint64 BodyUpdatedEventCycles = Pending->BodyUpdatedEventCycles;
|
const uint64 BodyUpdatedEventCycles = Pending->BodyUpdatedEventCycles;
|
||||||
|
const uint64 MeasuredRequestStartCycles = Pending->RequestStartCycles;
|
||||||
|
const uint64 MeasuredGenerationStartCycles = Pending->GenerationStartCycles;
|
||||||
|
const uint64 MeasuredGenerationEndCycles = Pending->GenerationEndCycles;
|
||||||
|
const uint64 MeasuredApplyStartCycles = Pending->ApplyStartCycles;
|
||||||
|
const bool bRecordStreamingLatency = Pending->bRecordStreamingLatency;
|
||||||
PendingCollisionCooks.Remove(Tile);
|
PendingCollisionCooks.Remove(Tile);
|
||||||
|
|
||||||
const ERealtimeMeshCollisionUpdateResult CollisionResult =
|
const ERealtimeMeshCollisionUpdateResult CollisionResult =
|
||||||
@@ -1913,6 +2164,12 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uint64 ReadyCycles = FPlatformTime::Cycles64();
|
const uint64 ReadyCycles = FPlatformTime::Cycles64();
|
||||||
|
if (bBodyInstalled && Tile.Level == 0 && bRecordStreamingLatency)
|
||||||
|
{
|
||||||
|
RecordLOD0ReadySample(MeasuredRequestStartCycles, MeasuredGenerationStartCycles,
|
||||||
|
MeasuredGenerationEndCycles, MeasuredApplyStartCycles,
|
||||||
|
ReadyCycles, MeasuredCollisionSubmittedCycles);
|
||||||
|
}
|
||||||
const double MeshToCookSeconds = FPlatformTime::ToSeconds64(
|
const double MeshToCookSeconds = FPlatformTime::ToSeconds64(
|
||||||
ReadyCycles - MeasuredMeshSubmittedCycles);
|
ReadyCycles - MeasuredMeshSubmittedCycles);
|
||||||
const double CollisionRequestToCookSeconds = FPlatformTime::ToSeconds64(
|
const double CollisionRequestToCookSeconds = FPlatformTime::ToSeconds64(
|
||||||
@@ -2008,12 +2265,25 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
while (ProcessQueue.Dequeue(DequeuedChunk))
|
while (ProcessQueue.Dequeue(DequeuedChunk))
|
||||||
{
|
{
|
||||||
++ResultsDrained;
|
++ResultsDrained;
|
||||||
|
const TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe> Cancellation =
|
||||||
|
PendingTileCancellation.FindRef(DequeuedChunk.Tile);
|
||||||
|
const bool bRequestObsolete = DequeuedChunk.bObsolete
|
||||||
|
|| (Cancellation.IsValid()
|
||||||
|
&& Cancellation->bObsolete.load(std::memory_order_acquire));
|
||||||
|
PendingTileCancellation.Remove(DequeuedChunk.Tile);
|
||||||
PendingTiles.Remove(DequeuedChunk.Tile);
|
PendingTiles.Remove(DequeuedChunk.Tile);
|
||||||
|
|
||||||
// A worker can observe shutdown after entering the mesher. Do not turn that partial
|
// A worker can observe shutdown after entering the mesher. Do not turn that partial
|
||||||
// result into an all-air tile: it must remain eligible for a future generation epoch.
|
// result into an all-air tile: it must remain eligible for a future generation epoch.
|
||||||
if (DequeuedChunk.bAborted)
|
// Read the token here as well as on the worker: cancellation can race the final worker
|
||||||
|
// check after it has already enqueued a complete result. This also drops a stale result
|
||||||
|
// if the tile leaves and re-enters the desired set before the queue is drained.
|
||||||
|
if (DequeuedChunk.bAborted || bRequestObsolete)
|
||||||
{
|
{
|
||||||
|
if (bRequestObsolete)
|
||||||
|
{
|
||||||
|
++ObsoleteTileAbortCount;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2059,8 +2329,11 @@ void AVoxelWorld::ProcessPendingChunks()
|
|||||||
// SyncRemeshTile (synchronous carve). Returns true iff a visible mesh was uploaded (budget).
|
// SyncRemeshTile (synchronous carve). Returns true iff a visible mesh was uploaded (budget).
|
||||||
bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||||
{
|
{
|
||||||
// Discard results from a previous generation epoch (stale).
|
// Discard results from a previous generation epoch (stale), or for a tile that has since left
|
||||||
if (Result.bAborted || Result.Epoch != GenerationEpoch)
|
// the desired set. ProcessPendingChunks also reads the per-request token before removing it,
|
||||||
|
// closing the race where the worker enqueues just after CancelObsoleteTileWork flips the flag.
|
||||||
|
if (Result.bAborted || Result.Epoch != GenerationEpoch
|
||||||
|
|| !IsDesired(Result.Tile))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2070,9 +2343,8 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
// in HandleTileCollisionCookComplete.
|
// in HandleTileCollisionCookComplete.
|
||||||
const bool bResultEmpty = Result.bEmpty || !Result.Streams;
|
const bool bResultEmpty = Result.bEmpty || !Result.Streams;
|
||||||
const bool bTraceTile = VoxelForgeStartupTrace::IsActive();
|
const bool bTraceTile = VoxelForgeStartupTrace::IsActive();
|
||||||
const bool bTelemetry = GVoxelForgeProfileTileGeneration != 0 || bTraceTile;
|
const uint64 ApplyStartCycles = FPlatformTime::Cycles64();
|
||||||
const uint64 ApplyStartCycles = bTelemetry
|
Result.ApplyStartCycles = ApplyStartCycles;
|
||||||
? FPlatformTime::Cycles64() : 0;
|
|
||||||
const auto LogTileApplied = [&]()
|
const auto LogTileApplied = [&]()
|
||||||
{
|
{
|
||||||
if ((GVoxelForgeProfileTileGeneration != 0 || bTraceTile)
|
if ((GVoxelForgeProfileTileGeneration != 0 || bTraceTile)
|
||||||
@@ -2112,8 +2384,6 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
Sample.Level = Result.Tile.Level;
|
Sample.Level = Result.Tile.Level;
|
||||||
Sample.Verdict = Result.ClassifyVerdict;
|
Sample.Verdict = Result.ClassifyVerdict;
|
||||||
Sample.bEmpty = bResultEmpty;
|
Sample.bEmpty = bResultEmpty;
|
||||||
Sample.bCacheHit = Result.bClassifierCacheHit;
|
|
||||||
Sample.bRegionHit = Result.bClassifierRegionHit;
|
|
||||||
Sample.Triangles = Result.NumTriangles;
|
Sample.Triangles = Result.NumTriangles;
|
||||||
Sample.RequestToApplySeconds = RequestToApply;
|
Sample.RequestToApplySeconds = RequestToApply;
|
||||||
Sample.WorkerQueueSeconds = WorkerQueueSeconds;
|
Sample.WorkerQueueSeconds = WorkerQueueSeconds;
|
||||||
@@ -2179,11 +2449,13 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
FPlatformTime::Cycles64() - EmptyReleaseStartCycles));
|
FPlatformTime::Cycles64() - EmptyReleaseStartCycles));
|
||||||
}
|
}
|
||||||
LoadedTiles.Add(Result.Tile);
|
LoadedTiles.Add(Result.Tile);
|
||||||
// Une tuile en vol n'est JAMAIS annulée : si le desired set a bougé pendant sa gen, elle
|
|
||||||
// arrive ici hors desired — le delta cull ne re-scanne plus tout, donc on l'inscrit en
|
|
||||||
// TransitionHold pour qu'elle soit re-considérée au prochain crossing (ou au settled cull).
|
|
||||||
if (!IsDesired(Result.Tile)) { AddToTransitionHold(Result.Tile); }
|
|
||||||
LogTileApplied();
|
LogTileApplied();
|
||||||
|
if (Result.Tile.Level == 0 && Result.bRecordStreamingLatency)
|
||||||
|
{
|
||||||
|
RecordLOD0ReadySample(Result.RequestStartCycles, Result.GenerationStartCycles,
|
||||||
|
Result.GenerationEndCycles, Result.ApplyStartCycles,
|
||||||
|
FPlatformTime::Cycles64(), 0);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2204,7 +2476,6 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
LoadedTiles.Add(Result.Tile);
|
LoadedTiles.Add(Result.Tile);
|
||||||
if (!IsDesired(Result.Tile)) { AddToTransitionHold(Result.Tile); }
|
|
||||||
LogTileApplied();
|
LogTileApplied();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2238,6 +2509,8 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
|||||||
}
|
}
|
||||||
|
|
||||||
FChunkResult Result;
|
FChunkResult Result;
|
||||||
|
Result.DesiredEpoch = DesiredEpoch;
|
||||||
|
Result.bRecordStreamingLatency = false;
|
||||||
Result.RequestStartCycles = bProfileModification
|
Result.RequestStartCycles = bProfileModification
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
const uint64 GenerateStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
const uint64 GenerateStartCycles = bProfileModification ? FPlatformTime::Cycles64() : 0;
|
||||||
@@ -2245,7 +2518,7 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
|||||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
|
GenerateTileResult(Tile, OriginVoxels, Step, Cells, GenerationEpoch, /*bWantCapture*/ false,
|
||||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||||
/*bSheetTile*/ false, /*SheetChunkZ*/ 0,
|
/*bSheetTile*/ false, /*SheetChunkZ*/ 0,
|
||||||
/*Hole*/ 0, 0, 0, 0, Result); // hole unused (not a sheet tile)
|
/*Hole*/ 0, 0, 0, 0, Result, nullptr); // hole unused (not a sheet tile)
|
||||||
Result.GenerationEndCycles = GenerateStartCycles != 0
|
Result.GenerationEndCycles = GenerateStartCycles != 0
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
|
|
||||||
@@ -2335,13 +2608,17 @@ static FORCEINLINE void VF_OuterShell(const UVoxelSettings* Settings, int32 R, i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, TArray<FVoxelTileKey>& OutLeavers)
|
void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, const FVector& PlayerPosition,
|
||||||
|
APawn* PlayerPawn, const FVector& PlayerHeading,
|
||||||
|
TArray<FVoxelTileKey>& OutLeavers)
|
||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildDesiredTiles);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_BuildDesiredTiles);
|
||||||
DesiredSorted.Reset();
|
DesiredSorted.Reset();
|
||||||
|
CriticalDesiredTiles.Reset();
|
||||||
OutLeavers.Reset();
|
OutLeavers.Reset();
|
||||||
CollisionOnlyTiles.Reset(); // §9.4 — rebuilt by AddAnchorDesiredTiles below
|
CollisionOnlyTiles.Reset(); // §9.4 — rebuilt by AddAnchorDesiredTiles below
|
||||||
++DesiredStamp; // les upserts ci-dessous marquent le crossing courant
|
++DesiredStamp; // les upserts ci-dessous marquent le crossing courant
|
||||||
|
++DesiredEpoch;
|
||||||
|
|
||||||
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
||||||
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
|
const int32 MaxLevel = Settings ? FMath::Clamp(Settings->MaxClipLevel, 0, 8) : 4;
|
||||||
@@ -2487,14 +2764,110 @@ void AVoxelWorld::BuildDesiredTiles(const FIntVector& Center, TArray<FVoxelTileK
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nearest-first (by tile-centre distance to the player), so the closest tiles stream first.
|
// Cancel work that no longer belongs to this desired set before scheduling replacements. A
|
||||||
const FVector PlayerVoxel = (FVector(Center) + FVector(0.5f, 0.5f, 0.5f)) * (float)CHUNK_SIZE;
|
// worker owns no mutable world state; it only observes its request token in GenerateTileResult.
|
||||||
|
CancelObsoleteTileWork();
|
||||||
|
|
||||||
|
// Nearest-first (by tile-centre distance to the TRUE pawn position), so a large tile's
|
||||||
|
// artificial chunk-centre approximation cannot put work behind the player. The explicit key
|
||||||
|
// tie-break keeps equal-distance ordering deterministic across runs.
|
||||||
|
const FVector PlayerVoxel = WorldToLocalCm(PlayerPosition) / VOXEL_SIZE;
|
||||||
DesiredSorted.Sort([&PlayerVoxel](const FVoxelTileKey& A, const FVoxelTileKey& B)
|
DesiredSorted.Sort([&PlayerVoxel](const FVoxelTileKey& A, const FVoxelTileKey& B)
|
||||||
{
|
{
|
||||||
const FVector CA = A.CenterCm() / VOXEL_SIZE;
|
const FVector CA = A.CenterCm() / VOXEL_SIZE;
|
||||||
const FVector CB = B.CenterCm() / VOXEL_SIZE;
|
const FVector CB = B.CenterCm() / VOXEL_SIZE;
|
||||||
return FVector::DistSquared(CA, PlayerVoxel) < FVector::DistSquared(CB, PlayerVoxel);
|
const float DA = FVector::DistSquared(CA, PlayerVoxel);
|
||||||
|
const float DB = FVector::DistSquared(CB, PlayerVoxel);
|
||||||
|
if (DA != DB) { return DA < DB; }
|
||||||
|
if (A.Level != B.Level) { return A.Level < B.Level; }
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Absolute floor priority. The support tile is the level-0 tile under the pawn's feet — the
|
||||||
|
// one that can catch a falling pawn — followed by the adjacent tile in the dominant movement
|
||||||
|
// direction. Keep the actor-centre tile as a fallback for capsule/offset edge cases. Only keys
|
||||||
|
// in the current desired set are promoted, so this never expands the streaming footprint.
|
||||||
|
const FVoxelTileKey PlayerTile(
|
||||||
|
WorldToChunkCoord(WorldToLocalCm(PlayerPosition)), 0);
|
||||||
|
FVoxelTileKey OccupiedTile = PlayerTile;
|
||||||
|
FVoxelTileKey SupportTile;
|
||||||
|
if (GetPlayerSupportTile(PlayerPawn, PlayerPosition, SupportTile))
|
||||||
|
{
|
||||||
|
OccupiedTile = SupportTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto AddCriticalTile = [this](const FVoxelTileKey& Key)
|
||||||
|
{
|
||||||
|
if (IsDesired(Key) && !CriticalDesiredTiles.Contains(Key))
|
||||||
|
{
|
||||||
|
CriticalDesiredTiles.Add(Key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AddCriticalTile(OccupiedTile);
|
||||||
|
|
||||||
|
FVector Heading = PlayerHeading;
|
||||||
|
if (Heading.IsNearlyZero() && IsValid(PlayerPawn))
|
||||||
|
{
|
||||||
|
Heading = PlayerPawn->GetActorForwardVector();
|
||||||
|
}
|
||||||
|
Heading = GetActorTransform().InverseTransformVectorNoScale(Heading);
|
||||||
|
const float AbsX = FMath::Abs(Heading.X);
|
||||||
|
const float AbsY = FMath::Abs(Heading.Y);
|
||||||
|
const float AbsZ = FMath::Abs(Heading.Z);
|
||||||
|
FIntVector HeadingStep = FIntVector::ZeroValue;
|
||||||
|
if (AbsX > KINDA_SMALL_NUMBER || AbsY > KINDA_SMALL_NUMBER || AbsZ > KINDA_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
if (AbsX >= AbsY && AbsX >= AbsZ)
|
||||||
|
{
|
||||||
|
HeadingStep.X = Heading.X >= 0.0f ? 1 : -1;
|
||||||
|
}
|
||||||
|
else if (AbsY >= AbsZ)
|
||||||
|
{
|
||||||
|
HeadingStep.Y = Heading.Y >= 0.0f ? 1 : -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HeadingStep.Z = Heading.Z >= 0.0f ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (HeadingStep != FIntVector::ZeroValue)
|
||||||
|
{
|
||||||
|
FVoxelTileKey NextTile = OccupiedTile;
|
||||||
|
NextTile.Coord += HeadingStep;
|
||||||
|
AddCriticalTile(NextTile);
|
||||||
|
}
|
||||||
|
AddCriticalTile(PlayerTile);
|
||||||
|
|
||||||
|
// Insert in reverse so CriticalDesiredTiles[0] remains the occupied floor, [1] the heading
|
||||||
|
// tile, and all ordinary distance-sorted work follows both.
|
||||||
|
for (int32 Index = CriticalDesiredTiles.Num() - 1; Index >= 0; --Index)
|
||||||
|
{
|
||||||
|
const int32 ExistingIndex = DesiredSorted.Find(CriticalDesiredTiles[Index]);
|
||||||
|
if (ExistingIndex > 0)
|
||||||
|
{
|
||||||
|
const FVoxelTileKey Key = DesiredSorted[ExistingIndex];
|
||||||
|
DesiredSorted.RemoveAt(ExistingIndex, 1, EAllowShrinking::No);
|
||||||
|
DesiredSorted.Insert(Key, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AVoxelWorld::CancelObsoleteTileWork()
|
||||||
|
{
|
||||||
|
for (const FVoxelTileKey& Tile : PendingTiles)
|
||||||
|
{
|
||||||
|
if (IsDesired(Tile))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>* Cancellation =
|
||||||
|
PendingTileCancellation.Find(Tile))
|
||||||
|
{
|
||||||
|
(*Cancellation)->bObsolete.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fold every registered anchor's small level-0 box into the current desired set (§9.3). Runs inside
|
// Fold every registered anchor's small level-0 box into the current desired set (§9.3). Runs inside
|
||||||
@@ -2648,10 +3021,10 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
|||||||
return FMath::Clamp(Asset, 1, CoreCap);
|
return FMath::Clamp(Asset, 1, CoreCap);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APawn* PlayerPawn,
|
||||||
|
const FVector& PlayerHeading)
|
||||||
{
|
{
|
||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_UpdateChunks);
|
||||||
VF_ParseTestStreamingCenter();
|
|
||||||
const int32 MaxTasks = GetMaxConcurrentTasks();
|
const int32 MaxTasks = GetMaxConcurrentTasks();
|
||||||
if (GVoxelForgeProfileTileGeneration != 0
|
if (GVoxelForgeProfileTileGeneration != 0
|
||||||
&& !GVoxelForgeStreamingBudgetReported)
|
&& !GVoxelForgeStreamingBudgetReported)
|
||||||
@@ -2676,11 +3049,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
|||||||
GVoxelForgeStreamingBudgetReported = true;
|
GVoxelForgeStreamingBudgetReported = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(CenterPosition)); // player's level-0 tile
|
const FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(CenterPosition)); // player's level-0 tile
|
||||||
if (GVoxelForgeUseTestStreamingCenter)
|
|
||||||
{
|
|
||||||
CenterChunk = GVoxelForgeTestStreamingCenter;
|
|
||||||
}
|
|
||||||
CurrentCenterChunk = CenterChunk;
|
CurrentCenterChunk = CenterChunk;
|
||||||
|
|
||||||
// Streaming anchors (AI / remote players, §9.3): prune dead ones + detect chunk crossings so the
|
// Streaming anchors (AI / remote players, §9.3): prune dead ones + detect chunk crossings so the
|
||||||
@@ -2714,12 +3083,11 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
|||||||
bAllChunksLoaded = false;
|
bAllChunksLoaded = false;
|
||||||
bForceDesiredRebuild = false; // consumed
|
bForceDesiredRebuild = false; // consumed
|
||||||
|
|
||||||
// DELTA CULL: BuildDesiredTiles renvoie les LEAVERS (désirées au crossing précédent, plus
|
// DELTA CULL: BuildDesiredTiles returns the LEAVERS (desired on the previous crossing, but
|
||||||
// maintenant). Seuls candidats au cull : ces leavers + la TransitionHold (retenues des
|
// not now). Only those leavers + TransitionHold (retained across prior crossings) enter
|
||||||
// crossings passés). Fini le re-scan de TOUTES les tuiles chargées à chaque crossing —
|
// the cull. Pending work that leaves desired is marked obsolete before replacement submit;
|
||||||
// c'était le spike CullTiles ~1.6 ms/crossing (trace 2026-07-05). Les tuiles en vol qui
|
// its aborted result is drained and dropped, while the settled cull remains the full-scan
|
||||||
// finissent hors desired sont capturées à l'apply (ProcessPendingChunks → TransitionHold),
|
// safety net.
|
||||||
// et le settled cull (tout chargé) reste le filet de sécurité plein-scan.
|
|
||||||
TArray<FVoxelTileKey> Leavers;
|
TArray<FVoxelTileKey> Leavers;
|
||||||
const bool bTraceFirstDesired = VoxelForgeStartupTrace::IsActive()
|
const bool bTraceFirstDesired = VoxelForgeStartupTrace::IsActive()
|
||||||
&& !bStartupTraceDesiredRecorded;
|
&& !bStartupTraceDesiredRecorded;
|
||||||
@@ -2727,7 +3095,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
|||||||
{
|
{
|
||||||
VoxelForgeStartupTrace::StageBegin(TEXT("BuildDesiredTiles.first"));
|
VoxelForgeStartupTrace::StageBegin(TEXT("BuildDesiredTiles.first"));
|
||||||
}
|
}
|
||||||
BuildDesiredTiles(CenterChunk, Leavers);
|
BuildDesiredTiles(CenterChunk, CenterPosition, PlayerPawn, PlayerHeading, Leavers);
|
||||||
if (bTraceFirstDesired)
|
if (bTraceFirstDesired)
|
||||||
{
|
{
|
||||||
VoxelForgeStartupTrace::StageEnd(TEXT("BuildDesiredTiles.first"));
|
VoxelForgeStartupTrace::StageEnd(TEXT("BuildDesiredTiles.first"));
|
||||||
@@ -2992,7 +3360,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
// Submit pending work (budgeted, nearest-first). Once everything desired is loaded,
|
// Submit pending work (budgeted, critical floor prefix then nearest-first). Once everything desired is loaded,
|
||||||
// do the "settled" cull of the deferred LOD-transition tiles, then go idle.
|
// do the "settled" cull of the deferred LOD-transition tiles, then go idle.
|
||||||
//=========================================================================
|
//=========================================================================
|
||||||
if (!bAllChunksLoaded)
|
if (!bAllChunksLoaded)
|
||||||
@@ -3000,14 +3368,25 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
|||||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles);
|
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles);
|
||||||
int32 Submitted = 0;
|
int32 Submitted = 0;
|
||||||
|
|
||||||
// DIG RESPONSIVENESS — drain player-carve re-meshes FIRST (ahead of streaming + band) at
|
// FLOOR RESPONSIVENESS — the occupied support tile and the next tile along the pawn heading
|
||||||
// BackgroundHigh, so a dig gets the task budget before any streaming gen. A tile that's still
|
// are an absolute prefix, ahead of dirty remeshes, band work, and ordinary clipmap work.
|
||||||
// in flight is KEPT queued (retried next frame) so its stale pre-carve result is corrected.
|
// They run at BackgroundHigh so the pawn's floor gets the first available worker.
|
||||||
|
for (const FVoxelTileKey& T : CriticalDesiredTiles)
|
||||||
|
{
|
||||||
|
if (PendingTiles.Num() >= MaxTasks) break;
|
||||||
|
if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue;
|
||||||
|
LoadTile(T, /*bHighPriority*/ true);
|
||||||
|
++Submitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DIG RESPONSIVENESS — drain player-carve re-meshes next at BackgroundHigh. A tile that's
|
||||||
|
// still in flight is KEPT queued (retried next frame) so its stale pre-carve result is corrected.
|
||||||
for (auto It = DirtyRemeshQueue.CreateIterator(); It; ++It)
|
for (auto It = DirtyRemeshQueue.CreateIterator(); It; ++It)
|
||||||
{
|
{
|
||||||
if (PendingTiles.Num() >= MaxTasks) break;
|
if (PendingTiles.Num() >= MaxTasks) break;
|
||||||
const FVoxelTileKey T = *It;
|
const FVoxelTileKey T = *It;
|
||||||
if (!LoadedTiles.Contains(T)) { It.RemoveCurrent(); continue; } // unloaded — drop
|
if (!LoadedTiles.Contains(T)) { It.RemoveCurrent(); continue; } // unloaded — drop
|
||||||
|
if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop
|
||||||
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
|
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
|
||||||
It.RemoveCurrent();
|
It.RemoveCurrent();
|
||||||
LoadTile(T, /*bHighPriority*/ true);
|
LoadTile(T, /*bHighPriority*/ true);
|
||||||
@@ -3030,7 +3409,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
|||||||
if (PendingTiles.Num() >= MaxTasks) break;
|
if (PendingTiles.Num() >= MaxTasks) break;
|
||||||
const FVoxelTileKey T = *It;
|
const FVoxelTileKey T = *It;
|
||||||
It.RemoveCurrent();
|
It.RemoveCurrent();
|
||||||
if (PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
||||||
LoadTile(T);
|
LoadTile(T);
|
||||||
++Submitted;
|
++Submitted;
|
||||||
}
|
}
|
||||||
@@ -3087,10 +3466,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
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 bool bTelemetry = GVoxelForgeProfileTileGeneration != 0
|
const uint64 RequestStartCycles = FPlatformTime::Cycles64();
|
||||||
|| VoxelForgeStartupTrace::IsActive();
|
|
||||||
const uint64 RequestStartCycles = bTelemetry
|
|
||||||
? FPlatformTime::Cycles64() : 0;
|
|
||||||
|
|
||||||
const FIntVector OriginVoxels = Tile.OriginVoxels(); // min corner, voxel coords
|
const FIntVector OriginVoxels = Tile.OriginVoxels(); // min corner, voxel coords
|
||||||
|
|
||||||
@@ -3118,6 +3494,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
}
|
}
|
||||||
const int32 Step = FMath::Max(1, Extent / Cells);
|
const int32 Step = FMath::Max(1, Extent / Cells);
|
||||||
const uint32 TaskEpoch = GenerationEpoch;
|
const uint32 TaskEpoch = GenerationEpoch;
|
||||||
|
const uint32 TaskDesiredEpoch = DesiredEpoch;
|
||||||
|
|
||||||
// CAPTURE-DURING-MESHING: only level-0 full-res tiles map 1:1 onto a density-clipmap level
|
// CAPTURE-DURING-MESHING: only level-0 full-res tiles map 1:1 onto a density-clipmap level
|
||||||
// (Step == 1<<Level, Cells == CHUNK_SIZE). When the density volume is active, ask the mesher to
|
// (Step == 1<<Level, Cells == CHUNK_SIZE). When the density volume is active, ask the mesher to
|
||||||
@@ -3144,6 +3521,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
FChunkResult Empty;
|
FChunkResult Empty;
|
||||||
Empty.Tile = Tile;
|
Empty.Tile = Tile;
|
||||||
Empty.Epoch = TaskEpoch;
|
Empty.Epoch = TaskEpoch;
|
||||||
|
Empty.DesiredEpoch = TaskDesiredEpoch;
|
||||||
Empty.RequestStartCycles = RequestStartCycles;
|
Empty.RequestStartCycles = RequestStartCycles;
|
||||||
ProcessQueue.Enqueue(MoveTemp(Empty));
|
ProcessQueue.Enqueue(MoveTemp(Empty));
|
||||||
return;
|
return;
|
||||||
@@ -3174,6 +3552,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
FChunkResult Empty;
|
FChunkResult Empty;
|
||||||
Empty.Tile = Tile;
|
Empty.Tile = Tile;
|
||||||
Empty.Epoch = TaskEpoch;
|
Empty.Epoch = TaskEpoch;
|
||||||
|
Empty.DesiredEpoch = TaskDesiredEpoch;
|
||||||
Empty.RequestStartCycles = RequestStartCycles;
|
Empty.RequestStartCycles = RequestStartCycles;
|
||||||
Empty.BandChunkLo = BandChunkLo;
|
Empty.BandChunkLo = BandChunkLo;
|
||||||
Empty.BandChunkHi = BandChunkHi;
|
Empty.BandChunkHi = BandChunkHi;
|
||||||
@@ -3182,22 +3561,25 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe> Cancellation =
|
||||||
|
MakeShared<FVoxelTileCancellationState, ESPMode::ThreadSafe>();
|
||||||
|
PendingTileCancellation.Add(Tile, Cancellation);
|
||||||
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
ActiveTaskCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
||||||
// BackgroundNormal priority: gen runs on background workers that YIELD to foreground
|
// BackgroundNormal priority: gen runs on background workers that YIELD to foreground
|
||||||
// (game/render-thread) tasks. Without this, raising MaxConcurrentTasks past the spare
|
// (game/render-thread) tasks. Without this, raising MaxConcurrentTasks past the spare
|
||||||
// core count saturates the scheduler and starves the frame (the "over 12 = lag" symptom).
|
// core count saturates the scheduler and starves the frame (the "over 12 = lag" symptom).
|
||||||
// At background priority the frame keeps its cores; gen just fills in around it.
|
// At background priority the frame keeps its cores; gen just fills in around it.
|
||||||
// DIG RESPONSIVENESS: player carves launch at BackgroundHigh (bHighPriority) — still a background
|
// FLOOR/DIG RESPONSIVENESS: critical support requests and player carves launch at BackgroundHigh
|
||||||
// worker (yields to the frame, keeps the invariant) but jumps AHEAD of all pending streaming gen,
|
// (bHighPriority) — still a background worker (yields to the frame, keeps the invariant) but
|
||||||
// so a dig is never queued behind a shell of streaming tasks.
|
// jumps AHEAD of ordinary pending streaming gen.
|
||||||
const UE::Tasks::ETaskPriority TaskPriority = bHighPriority
|
const UE::Tasks::ETaskPriority TaskPriority = bHighPriority
|
||||||
? UE::Tasks::ETaskPriority::BackgroundHigh
|
? UE::Tasks::ETaskPriority::BackgroundHigh
|
||||||
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
||||||
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
||||||
RequestStartCycles, bTelemetry]()
|
RequestStartCycles, TaskDesiredEpoch, Cancellation]()
|
||||||
{
|
{
|
||||||
// RAII: decrement the counter on every exit path.
|
// RAII: decrement the counter on every exit path.
|
||||||
struct FTaskGuard
|
struct FTaskGuard
|
||||||
@@ -3206,19 +3588,51 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||||
} Guard{ActiveTaskCount};
|
} Guard{ActiveTaskCount};
|
||||||
|
|
||||||
|
auto EnqueueObsoleteResult = [&]()
|
||||||
|
{
|
||||||
|
if (bShuttingDown.load(std::memory_order_relaxed)
|
||||||
|
|| bGenerationPaused.load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FChunkResult Canceled;
|
||||||
|
Canceled.Tile = Tile;
|
||||||
|
Canceled.Epoch = TaskEpoch;
|
||||||
|
Canceled.DesiredEpoch = TaskDesiredEpoch;
|
||||||
|
Canceled.RequestStartCycles = RequestStartCycles;
|
||||||
|
Canceled.bAborted = true;
|
||||||
|
Canceled.bObsolete = true;
|
||||||
|
ProcessQueue.Enqueue(MoveTemp(Canceled));
|
||||||
|
};
|
||||||
|
|
||||||
if (ShouldAbortWork()) return;
|
if (ShouldAbortWork()) return;
|
||||||
|
if (Cancellation->bObsolete.load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
EnqueueObsoleteResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
FChunkResult Result;
|
FChunkResult Result;
|
||||||
const uint64 GenerationStartCycles = bTelemetry
|
Result.DesiredEpoch = TaskDesiredEpoch;
|
||||||
? FPlatformTime::Cycles64() : 0;
|
const uint64 GenerationStartCycles = FPlatformTime::Cycles64();
|
||||||
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,
|
||||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY, Result);
|
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
||||||
|
Result, &Cancellation->bObsolete);
|
||||||
Result.GenerationEndCycles = GenerationStartCycles != 0
|
Result.GenerationEndCycles = GenerationStartCycles != 0
|
||||||
? FPlatformTime::Cycles64() : 0;
|
? FPlatformTime::Cycles64() : 0;
|
||||||
Result.RequestStartCycles = RequestStartCycles;
|
Result.RequestStartCycles = RequestStartCycles;
|
||||||
|
|
||||||
|
if (Cancellation->bObsolete.load(std::memory_order_relaxed) || Result.bAborted)
|
||||||
|
{
|
||||||
|
if (!ShouldAbortWork())
|
||||||
|
{
|
||||||
|
EnqueueObsoleteResult();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ShouldAbortWork())
|
if (!ShouldAbortWork())
|
||||||
{
|
{
|
||||||
ProcessQueue.Enqueue(MoveTemp(Result)); // move: don't copy the geometry payload
|
ProcessQueue.Enqueue(MoveTemp(Result)); // move: don't copy the geometry payload
|
||||||
@@ -3229,11 +3643,12 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
|||||||
// Worker-side gen for one tile (shared by the async ChunkGen task and the synchronous carve path).
|
// Worker-side gen for one tile (shared by the async ChunkGen task and the synchronous carve path).
|
||||||
// READS Generator/Mesher only — safe on a worker or the game thread. Fills Result; no enqueue.
|
// READS Generator/Mesher only — safe on a worker or the game thread. Fills Result; no enqueue.
|
||||||
void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector& OriginVoxels,
|
||||||
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
|
int32 Step, int32 Cells, uint32 Epoch, bool bWantCapture,
|
||||||
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
bool bSheetTile, int32 SheetChunkZ,
|
bool bSheetTile, int32 SheetChunkZ,
|
||||||
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
||||||
FChunkResult& Result)
|
FChunkResult& Result,
|
||||||
|
const std::atomic<bool>* ObsoleteFlag)
|
||||||
{
|
{
|
||||||
const bool bLogTileProfile = GVoxelForgeProfileTileGeneration != 0;
|
const bool bLogTileProfile = GVoxelForgeProfileTileGeneration != 0;
|
||||||
const bool bMeasureTile = bLogTileProfile || VoxelForgeStartupTrace::IsActive();
|
const bool bMeasureTile = bLogTileProfile || VoxelForgeStartupTrace::IsActive();
|
||||||
@@ -3346,10 +3761,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
Result.Tile = Tile;
|
Result.Tile = Tile;
|
||||||
Result.Epoch = Epoch;
|
Result.Epoch = Epoch;
|
||||||
Result.bAborted = false;
|
Result.bAborted = false;
|
||||||
|
Result.bObsolete = false;
|
||||||
Result.bEmpty = true;
|
Result.bEmpty = true;
|
||||||
Result.ClassifyVerdict = -1;
|
Result.ClassifyVerdict = -1;
|
||||||
Result.bClassifierCacheHit = false;
|
|
||||||
Result.bClassifierRegionHit = false;
|
|
||||||
Result.ClassifySeconds = 0.0;
|
Result.ClassifySeconds = 0.0;
|
||||||
Result.MeshSeconds = 0.0;
|
Result.MeshSeconds = 0.0;
|
||||||
Result.StreamSeconds = 0.0;
|
Result.StreamSeconds = 0.0;
|
||||||
@@ -3364,11 +3778,13 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
auto AbortResult = [&]()
|
auto AbortResult = [&]()
|
||||||
{
|
{
|
||||||
Result.bAborted = true;
|
Result.bAborted = true;
|
||||||
|
Result.bObsolete = ObsoleteFlag != nullptr
|
||||||
|
&& ObsoleteFlag->load(std::memory_order_relaxed);
|
||||||
Result.bEmpty = true;
|
Result.bEmpty = true;
|
||||||
Result.Streams.Reset();
|
Result.Streams.Reset();
|
||||||
Result.CaptureGrid.Reset();
|
Result.CaptureGrid.Reset();
|
||||||
};
|
};
|
||||||
if (ShouldAbortWork())
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
{
|
{
|
||||||
AbortResult();
|
AbortResult();
|
||||||
EmitTileProfile();
|
EmitTileProfile();
|
||||||
@@ -3414,7 +3830,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
bool bExactUniform = true;
|
bool bExactUniform = true;
|
||||||
for (int32 Z = 0; Z <= ValidationCells && bExactUniform && !bValidationAborted; ++Z)
|
for (int32 Z = 0; Z <= ValidationCells && bExactUniform && !bValidationAborted; ++Z)
|
||||||
{
|
{
|
||||||
if (ShouldAbortWork())
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
{
|
{
|
||||||
bValidationAborted = true;
|
bValidationAborted = true;
|
||||||
break;
|
break;
|
||||||
@@ -3451,22 +3867,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ClassifySeconds = bMeasureTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0;
|
ClassifySeconds = bMeasureTile ? FPlatformTime::Seconds() - ClassifyStartSeconds : 0.0;
|
||||||
if (bMeasureTile)
|
|
||||||
{
|
|
||||||
const VoxelDensityProfile::FSnapshot ClassifyProfileEnd =
|
|
||||||
VoxelDensityProfile::SnapshotCurrentThread();
|
|
||||||
const int32 CacheHitIndex = static_cast<int32>(
|
|
||||||
VoxelDensityProfile::ECounter::TileVerdictCacheHits);
|
|
||||||
const int32 RegionHitIndex = static_cast<int32>(
|
|
||||||
VoxelDensityProfile::ECounter::TileVerdictRegionHits);
|
|
||||||
Result.bClassifierCacheHit =
|
|
||||||
ClassifyProfileEnd.Counters[CacheHitIndex] > TileProfileStart.Counters[CacheHitIndex];
|
|
||||||
Result.bClassifierRegionHit =
|
|
||||||
ClassifyProfileEnd.Counters[RegionHitIndex] > TileProfileStart.Counters[RegionHitIndex];
|
|
||||||
}
|
|
||||||
ClassifyVerdict = static_cast<int32>(ValidatedVerdict);
|
ClassifyVerdict = static_cast<int32>(ValidatedVerdict);
|
||||||
Result.ClassifyVerdict = ClassifyVerdict;
|
Result.ClassifyVerdict = ClassifyVerdict;
|
||||||
if (ShouldAbortWork())
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
{
|
{
|
||||||
AbortResult();
|
AbortResult();
|
||||||
EmitTileProfile();
|
EmitTileProfile();
|
||||||
@@ -3501,7 +3904,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
BandVoxLo, BandVoxHi);
|
BandVoxLo, BandVoxHi);
|
||||||
MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
|
MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
|
||||||
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed);
|
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed);
|
||||||
if (ShouldAbortWork())
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
{
|
{
|
||||||
AbortResult();
|
AbortResult();
|
||||||
EmitTileProfile();
|
EmitTileProfile();
|
||||||
@@ -3518,7 +3921,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
|||||||
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
|
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
|
||||||
BuildTileStreamSet(*Result.Streams, MeshData);
|
BuildTileStreamSet(*Result.Streams, MeshData);
|
||||||
StreamSeconds = bMeasureTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0;
|
StreamSeconds = bMeasureTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0;
|
||||||
if (ShouldAbortWork())
|
if (ShouldAbortWork(ObsoleteFlag))
|
||||||
{
|
{
|
||||||
AbortResult();
|
AbortResult();
|
||||||
EmitTileProfile();
|
EmitTileProfile();
|
||||||
@@ -3640,6 +4043,13 @@ void AVoxelWorld::UnloadTile(const FVoxelTileKey& Tile)
|
|||||||
{
|
{
|
||||||
// RMC futures are not cancellable from the world side. Remove this tile's state first so a
|
// RMC futures are not cancellable from the world side. Remove this tile's state first so a
|
||||||
// late completion from the old component cannot resurrect readiness after pool reuse.
|
// late completion from the old component cannot resurrect readiness after pool reuse.
|
||||||
|
// Generation work is different: mark it obsolete and let its normal aborted result drain so
|
||||||
|
// PendingTiles cannot be removed on behalf of an older request after a replacement is queued.
|
||||||
|
if (TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>* Cancellation =
|
||||||
|
PendingTileCancellation.Find(Tile))
|
||||||
|
{
|
||||||
|
(*Cancellation)->bObsolete.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
CollisionReadyTiles.Remove(Tile);
|
CollisionReadyTiles.Remove(Tile);
|
||||||
CollisionNotRequiredTiles.Remove(Tile);
|
CollisionNotRequiredTiles.Remove(Tile);
|
||||||
CollisionSolidTiles.Remove(Tile);
|
CollisionSolidTiles.Remove(Tile);
|
||||||
@@ -3652,7 +4062,6 @@ void AVoxelWorld::UnloadTile(const FVoxelTileKey& Tile)
|
|||||||
TileComponents.Remove(Tile);
|
TileComponents.Remove(Tile);
|
||||||
}
|
}
|
||||||
LoadedTiles.Remove(Tile);
|
LoadedTiles.Remove(Tile);
|
||||||
PendingTiles.Remove(Tile);
|
|
||||||
TransitionHold.Remove(Tile); // couvre aussi le settled cull (qui ne tient pas la hold à jour)
|
TransitionHold.Remove(Tile); // couvre aussi le settled cull (qui ne tient pas la hold à jour)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3866,6 +4275,11 @@ bool AVoxelWorld::ApplyMeshToTile(FChunkResult& Result)
|
|||||||
Pending.SubmissionId = SubmissionId;
|
Pending.SubmissionId = SubmissionId;
|
||||||
Pending.MeshSubmittedCycles = MeshSubmittedCycles;
|
Pending.MeshSubmittedCycles = MeshSubmittedCycles;
|
||||||
Pending.CollisionSubmittedCycles = CollisionSubmittedCycles;
|
Pending.CollisionSubmittedCycles = CollisionSubmittedCycles;
|
||||||
|
Pending.RequestStartCycles = Result.RequestStartCycles;
|
||||||
|
Pending.GenerationStartCycles = Result.GenerationStartCycles;
|
||||||
|
Pending.GenerationEndCycles = Result.GenerationEndCycles;
|
||||||
|
Pending.ApplyStartCycles = Result.ApplyStartCycles;
|
||||||
|
Pending.bRecordStreamingLatency = Result.bRecordStreamingLatency;
|
||||||
PendingCollisionCooks.Add(Tile, Pending);
|
PendingCollisionCooks.Add(Tile, Pending);
|
||||||
|
|
||||||
// Register the serial before asking RMC to dirty collision. The current implementation
|
// Register the serial before asking RMC to dirty collision. The current implementation
|
||||||
|
|||||||
@@ -120,9 +120,6 @@ namespace VoxelDensityProfile
|
|||||||
TileClassifyAllSolid,
|
TileClassifyAllSolid,
|
||||||
TileClassifyAllAir,
|
TileClassifyAllAir,
|
||||||
TileClassifyMixed,
|
TileClassifyMixed,
|
||||||
TileVerdictCacheHits,
|
|
||||||
TileVerdictRegionHits,
|
|
||||||
TileVerdictCacheStores,
|
|
||||||
FusedTunnelSamples,
|
FusedTunnelSamples,
|
||||||
FusedTunnelDetailSamples,
|
FusedTunnelDetailSamples,
|
||||||
FusedTunnelDetailSkipped,
|
FusedTunnelDetailSkipped,
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "HAL/CriticalSection.h"
|
|
||||||
#include "VoxelTypes.h"
|
#include "VoxelTypes.h"
|
||||||
#include "VoxelStrateTypes.h"
|
#include "VoxelStrateTypes.h"
|
||||||
#include "VoxelBiomeTypes.h"
|
#include "VoxelBiomeTypes.h"
|
||||||
|
|
||||||
#include "VoxelGenerator.generated.h"
|
#include "VoxelGenerator.generated.h"
|
||||||
|
|
||||||
// Forward decls (évite les includes transitifs)
|
// Forward decls (évite les includes transitifs)
|
||||||
@@ -119,12 +119,10 @@ public:
|
|||||||
void SetStrateManager(const UVoxelStrateManager* InManager)
|
void SetStrateManager(const UVoxelStrateManager* InManager)
|
||||||
{
|
{
|
||||||
StrateManager = InManager;
|
StrateManager = InManager;
|
||||||
InvalidateTileVerdictCache();
|
|
||||||
}
|
}
|
||||||
void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer)
|
void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer)
|
||||||
{
|
{
|
||||||
DiffLayer = InDiffLayer;
|
DiffLayer = InDiffLayer;
|
||||||
InvalidateTileVerdictCache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copie le seed depuis les settings. Appelé au démarrage et lors d'un ChangeSeed.
|
// Copie le seed depuis les settings. Appelé au démarrage et lors d'un ChangeSeed.
|
||||||
@@ -381,53 +379,6 @@ private:
|
|||||||
* Process-unique owner identity for the `CP_*` thread-local cache key. */
|
* Process-unique owner identity for the `CP_*` thread-local cache key. */
|
||||||
uint64 DensityCacheOwnerId = 0;
|
uint64 DensityCacheOwnerId = 0;
|
||||||
|
|
||||||
/**
|
|
||||||
* A bounded, session-resident proof cache for uniform MC tiles.
|
|
||||||
*
|
|
||||||
* This is deliberately not a disk cache: a verdict is only useful while the exact generator
|
|
||||||
* and its immutable layout are alive, and a stale "all air" verdict would be a hole. The key
|
|
||||||
* carries every runtime invalidator that can change the sampled field. Entries are only
|
|
||||||
* stored for AllSolid/AllAir; Mixed is never remembered because it is not a proof.
|
|
||||||
*
|
|
||||||
* A larger proven tile is also a proof for an aligned sub-tile. FindTileVerdict therefore
|
|
||||||
* accepts an enclosing entry and records that separately from an exact revisit. Capacity is
|
|
||||||
* fixed so repeated exploration cannot grow memory without bound.
|
|
||||||
*/
|
|
||||||
struct FTileVerdictCacheEntry
|
|
||||||
{
|
|
||||||
FIntVector Origin = FIntVector::ZeroValue;
|
|
||||||
int32 Step = 1;
|
|
||||||
int32 CellsPerAxis = 0;
|
|
||||||
int32 GeneratorSeed = 0;
|
|
||||||
int32 ManagerWorldSeed = 0;
|
|
||||||
float OriginSpineRadius = 0.0f;
|
|
||||||
float WorldRadiusVoxels = 0.0f;
|
|
||||||
float EdgeSealThickness = 0.0f;
|
|
||||||
uint64 ManagerLifetimeId = 0;
|
|
||||||
uint32 LayoutVersion = 0;
|
|
||||||
uint32 DiffVersion = 0;
|
|
||||||
uint64 ParamsFingerprint = 0;
|
|
||||||
uint8 PassageSupportFloorWrites = 1;
|
|
||||||
EVoxelTileClass Verdict = EVoxelTileClass::Mixed;
|
|
||||||
uint32 LastUse = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr int32 TileVerdictCacheCapacity = 512;
|
|
||||||
mutable FCriticalSection TileVerdictCacheLock;
|
|
||||||
mutable TArray<FTileVerdictCacheEntry> TileVerdictCache;
|
|
||||||
mutable uint32 TileVerdictCacheClock = 0;
|
|
||||||
|
|
||||||
void InvalidateTileVerdictCache() const;
|
|
||||||
bool FindTileVerdict(
|
|
||||||
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
|
|
||||||
int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion,
|
|
||||||
uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites,
|
|
||||||
EVoxelTileClass& OutVerdict, bool& bOutRegionHit) const;
|
|
||||||
void StoreTileVerdict(
|
|
||||||
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
|
|
||||||
int32 ManagerWorldSeed, uint32 LayoutVersion, uint32 DiffVersion,
|
|
||||||
uint64 ParamsFingerprint, uint8 PassageSupportFloorWrites,
|
|
||||||
EVoxelTileClass Verdict) const;
|
|
||||||
EVoxelTileClass ClassifyTileUncached(
|
EVoxelTileClass ClassifyTileUncached(
|
||||||
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
|
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
|
||||||
struct FVoxelTileClassificationStats* OutStats) const;
|
struct FVoxelTileClassificationStats* OutStats) const;
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ namespace VoxelForgeStartupTrace
|
|||||||
int32 Level = -1;
|
int32 Level = -1;
|
||||||
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path)
|
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path)
|
||||||
bool bEmpty = true;
|
bool bEmpty = true;
|
||||||
bool bCacheHit = false;
|
|
||||||
bool bRegionHit = false;
|
|
||||||
int32 Triangles = 0;
|
int32 Triangles = 0;
|
||||||
// This is worker-result -> game-thread mesh submission. Collision readiness is a later
|
// This is worker-result -> game-thread mesh submission. Collision readiness is a later
|
||||||
// per-tile RMC completion event recorded separately as collision_ready.
|
// per-tile RMC completion event recorded separately as collision_ready.
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ class UMaterialInstanceDynamic;
|
|||||||
class FScopedGenerationPause;
|
class FScopedGenerationPause;
|
||||||
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
|
namespace RealtimeMesh { struct FRealtimeMeshStreamSet; } // T1.f — worker-built geometry buffers
|
||||||
|
|
||||||
|
/** Per-request cancellation state. The game thread flips the flag when a pending tile leaves the
|
||||||
|
* desired set; workers only read it and return an aborted result through the normal MPSC queue. */
|
||||||
|
struct FVoxelTileCancellationState
|
||||||
|
{
|
||||||
|
std::atomic<bool> bObsolete{false};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How a streaming anchor wants its tiles built (ARCHITECTURE §9.3). The local player is an implicit
|
* How a streaming anchor wants its tiles built (ARCHITECTURE §9.3). The local player is an implicit
|
||||||
* FullVisual anchor (the clipmap). Extra anchors — AI, and later remote players — are registered so
|
* FullVisual anchor (the clipmap). Extra anchors — AI, and later remote players — are registered so
|
||||||
@@ -89,14 +96,21 @@ struct FChunkResult
|
|||||||
// Null ⇒ no-surface tile (uniform all-air/all-solid or an empty content band; no component).
|
// Null ⇒ no-surface tile (uniform all-air/all-solid or an empty content band; no component).
|
||||||
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
|
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
|
||||||
uint32 Epoch = 0; // Generation epoch — discard if stale
|
uint32 Epoch = 0; // Generation epoch — discard if stale
|
||||||
// Monotonic request timestamp used only for streaming telemetry. It is carried through the
|
// Monotonic request timestamp used for the low-overhead streaming latency summary and the
|
||||||
// worker queue so the profile can report request-to-apply, not just worker generation time.
|
// opt-in profile. It is carried through the worker queue so the report can separate scheduler
|
||||||
|
// wait from generation time.
|
||||||
uint64 RequestStartCycles = 0;
|
uint64 RequestStartCycles = 0;
|
||||||
// Worker timing boundaries for separating scheduler wait from generation and result/apply wait.
|
// Worker timing boundaries for separating scheduler wait from generation and result/apply wait.
|
||||||
// These are telemetry-only and remain zero when tile profiling is disabled.
|
// These are captured with low overhead for the clean streaming-latency summary; expensive
|
||||||
|
// per-tile profiling remains opt-in.
|
||||||
uint64 GenerationStartCycles = 0;
|
uint64 GenerationStartCycles = 0;
|
||||||
uint64 GenerationEndCycles = 0; int32 ClassifyVerdict = -1; bool bClassifierCacheHit = false; bool bClassifierRegionHit = false; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0;
|
uint64 GenerationEndCycles = 0;
|
||||||
bool bAborted = false; // Worker observed shutdown; never mark this tile loaded
|
uint64 ApplyStartCycles = 0;
|
||||||
|
int32 ClassifyVerdict = -1; double ClassifySeconds = 0.0; double MeshSeconds = 0.0; double StreamSeconds = 0.0; int32 NumTriangles = 0;
|
||||||
|
bool bAborted = false; // Worker observed shutdown or cancellation; never mark this tile loaded
|
||||||
|
bool bObsolete = false; // Worker observed per-tile desired-set cancellation
|
||||||
|
uint32 DesiredEpoch = 0; // Desired-set epoch captured at request time
|
||||||
|
bool bRecordStreamingLatency = true;
|
||||||
bool bEmpty = true; // true ⇒ no surface (uniform all-air/all-solid or empty band); still loaded
|
bool bEmpty = true; // true ⇒ no surface (uniform all-air/all-solid or empty band); still loaded
|
||||||
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
|
// F17 — the mesher classifies every triangle semantically (sky-cap = down-facing near the
|
||||||
// column's CeilSurf; overhangs/cave roofs stay ground) and packs them as two contiguous runs
|
// column's CeilSurf; overhangs/cave roofs stay ground) and packs them as two contiguous runs
|
||||||
@@ -225,6 +239,12 @@ public:
|
|||||||
uint64 CollisionSubmittedCycles = 0;
|
uint64 CollisionSubmittedCycles = 0;
|
||||||
bool bBodyUpdatedEventSeen = false;
|
bool bBodyUpdatedEventSeen = false;
|
||||||
uint64 BodyUpdatedEventCycles = 0;
|
uint64 BodyUpdatedEventCycles = 0;
|
||||||
|
// Game-thread-only timing copied from the generation result for the clean LOD0 summary.
|
||||||
|
uint64 RequestStartCycles = 0;
|
||||||
|
uint64 GenerationStartCycles = 0;
|
||||||
|
uint64 GenerationEndCycles = 0;
|
||||||
|
uint64 ApplyStartCycles = 0;
|
||||||
|
bool bRecordStreamingLatency = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** One serialised completion record per tile. The serial prevents a late RMC future from
|
/** One serialised completion record per tile. The serial prevents a late RMC future from
|
||||||
@@ -474,6 +494,14 @@ private:
|
|||||||
/** Apply the current TVP0..4 + level-0 volume texture to one MID (also used on MID creation). */
|
/** Apply the current TVP0..4 + level-0 volume texture to one MID (also used on MID creation). */
|
||||||
void SetVolumeParamsOnMID(UMaterialInstanceDynamic* MID) const;
|
void SetVolumeParamsOnMID(UMaterialInstanceDynamic* MID) const;
|
||||||
|
|
||||||
|
/** Explicitly opt-in headless streaming validation. This is a command-line-only harness path;
|
||||||
|
* normal gameplay never moves the pawn here. It supplies a repeatable moving session and lets
|
||||||
|
* -nullrhi request a graceful EndPlay so the clean latency summary is flushed. */
|
||||||
|
void ConfigureHeadlessStreamingTest();
|
||||||
|
void AdvanceHeadlessStreamingTest(FVector& InOutPlayerPosition,
|
||||||
|
FVector& InOutPlayerHeading, APawn* PlayerPawn);
|
||||||
|
void MaybeFinishHeadlessStreamingTest();
|
||||||
|
|
||||||
// Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector
|
// Packed shader params, recomputed each Tick. ALL meaningful data is in .xyz — a material Vector
|
||||||
// Parameter only delivers float3 (RGB) into a Custom node (the alpha is dropped), so we never use .w.
|
// Parameter only delivers float3 (RGB) into a Custom node (the alpha is dropped), so we never use .w.
|
||||||
// TVP0 = L0 WindowOrigin.xyz (world cm) TVP1 = L0 OriginMod.xyz (cells)
|
// TVP0 = L0 WindowOrigin.xyz (world cm) TVP1 = L0 OriginMod.xyz (cells)
|
||||||
@@ -647,7 +675,8 @@ public:
|
|||||||
*
|
*
|
||||||
* @param CenterPosition - Usually the player's position
|
* @param CenterPosition - Usually the player's position
|
||||||
*/
|
*/
|
||||||
void UpdateChunksAroundPosition(const FVector& CenterPosition);
|
void UpdateChunksAroundPosition(const FVector& CenterPosition, APawn* PlayerPawn,
|
||||||
|
const FVector& PlayerHeading);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a single chunk at the given coordinate.
|
* Load a single chunk at the given coordinate.
|
||||||
@@ -673,7 +702,8 @@ public:
|
|||||||
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
||||||
bool bSheetTile, int32 SheetChunkZ,
|
bool bSheetTile, int32 SheetChunkZ,
|
||||||
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
||||||
FChunkResult& Result);
|
FChunkResult& Result,
|
||||||
|
const std::atomic<bool>* ObsoleteFlag = nullptr);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Game-thread apply for one gen result (shared by ProcessPendingChunks + SyncRemeshTile):
|
* Game-thread apply for one gen result (shared by ProcessPendingChunks + SyncRemeshTile):
|
||||||
@@ -736,7 +766,10 @@ public:
|
|||||||
/** Build the clipmap desired-tile set (concentric shells) around the player tile.
|
/** Build the clipmap desired-tile set (concentric shells) around the player tile.
|
||||||
* OutLeavers = les tuiles désirées au crossing PRÉCÉDENT qui ne le sont plus — les seuls
|
* OutLeavers = les tuiles désirées au crossing PRÉCÉDENT qui ne le sont plus — les seuls
|
||||||
* candidats au cull (delta), au lieu de re-scanner TOUTES les tuiles chargées par crossing. */
|
* candidats au cull (delta), au lieu de re-scanner TOUTES les tuiles chargées par crossing. */
|
||||||
void BuildDesiredTiles(const FIntVector& CenterChunkCoord, TArray<FVoxelTileKey>& OutLeavers);
|
void BuildDesiredTiles(const FIntVector& CenterChunkCoord, const FVector& PlayerPosition,
|
||||||
|
APawn* PlayerPawn, const FVector& PlayerHeading,
|
||||||
|
TArray<FVoxelTileKey>& OutLeavers);
|
||||||
|
void CancelObsoleteTileWork();
|
||||||
|
|
||||||
/** True if a tile's world footprint is still within the outermost clip shell (so a
|
/** True if a tile's world footprint is still within the outermost clip shell (so a
|
||||||
* not-desired loaded tile there is mid-LOD-transition and must wait for its replacement,
|
* not-desired loaded tile there is mid-LOD-transition and must wait for its replacement,
|
||||||
@@ -779,6 +812,10 @@ public:
|
|||||||
void BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh);
|
void BindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh);
|
||||||
void UnbindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh);
|
void UnbindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh);
|
||||||
void HandleRealtimeMeshCollisionBodyUpdated(URealtimeMesh* Mesh, UBodySetup* BodySetup);
|
void HandleRealtimeMeshCollisionBodyUpdated(URealtimeMesh* Mesh, UBodySetup* BodySetup);
|
||||||
|
void RecordLOD0ReadySample(uint64 RequestStartCycles, uint64 GenerationStartCycles,
|
||||||
|
uint64 GenerationEndCycles, uint64 ApplyStartCycles,
|
||||||
|
uint64 ReadyCycles, uint64 CollisionSubmittedCycles);
|
||||||
|
void LogStreamingLatencySummary() const;
|
||||||
|
|
||||||
// (GetLODForChunk / LODToStep / IsChunkInRange removed — dead since the clipmap
|
// (GetLODForChunk / LODToStep / IsChunkInRange removed — dead since the clipmap
|
||||||
// streaming replaced the distance-LOD scheme; the level lives in FVoxelTileKey.)
|
// streaming replaced the distance-LOD scheme; the level lives in FVoxelTileKey.)
|
||||||
@@ -793,6 +830,9 @@ public:
|
|||||||
// budget is exhausted and streaming stalls permanently. Mpsc guards the producer side.
|
// budget is exhausted and streaming stalls permanently. Mpsc guards the producer side.
|
||||||
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
||||||
TSet<FVoxelTileKey> PendingTiles; // tiles with a gen task in flight
|
TSet<FVoxelTileKey> PendingTiles; // tiles with a gen task in flight
|
||||||
|
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
|
||||||
|
TMap<FVoxelTileKey, TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>>
|
||||||
|
PendingTileCancellation;
|
||||||
|
|
||||||
// STRATE CONTENT CUT (see UVoxelSettings::StrateContentCutMinLevel) — the player-strate
|
// STRATE CONTENT CUT (see UVoxelSettings::StrateContentCutMinLevel) — the player-strate
|
||||||
// chunk-Z band coarse tiles are meshed to (MIN/MAX sentinels = no cut, e.g. in the gap).
|
// chunk-Z band coarse tiles are meshed to (MIN/MAX sentinels = no cut, e.g. in the gap).
|
||||||
@@ -830,10 +870,12 @@ public:
|
|||||||
// Number of async tasks currently running — EndPlay waits for this to reach 0
|
// Number of async tasks currently running — EndPlay waits for this to reach 0
|
||||||
std::atomic<int32> ActiveTaskCount{0};
|
std::atomic<int32> ActiveTaskCount{0};
|
||||||
|
|
||||||
FORCEINLINE bool ShouldAbortWork() const
|
FORCEINLINE bool ShouldAbortWork(const std::atomic<bool>* ObsoleteFlag = nullptr) const
|
||||||
{
|
{
|
||||||
return bShuttingDown.load(std::memory_order_relaxed)
|
return bShuttingDown.load(std::memory_order_relaxed)
|
||||||
|| bGenerationPaused.load(std::memory_order_relaxed);
|
|| bGenerationPaused.load(std::memory_order_relaxed)
|
||||||
|
|| (ObsoleteFlag != nullptr
|
||||||
|
&& ObsoleteFlag->load(std::memory_order_relaxed));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Player's level-0 tile coord (= chunk coord). The desired set is rebuilt when this changes.
|
// Player's level-0 tile coord (= chunk coord). The desired set is rebuilt when this changes.
|
||||||
@@ -861,13 +903,40 @@ public:
|
|||||||
void ReconcileAnchorTileVisibility();
|
void ReconcileAnchorTileVisibility();
|
||||||
|
|
||||||
// --- Streaming work-avoidance (perf) ---
|
// --- Streaming work-avoidance (perf) ---
|
||||||
// The desired tile set only changes when the player crosses a level-0 tile boundary.
|
// The desired tile set only changes when the player/anchor crosses a level-0 tile boundary or
|
||||||
// We cache it and only rebuild/cull/sort on a real move, and go idle once every desired
|
// a rebuild is forced. We cache it and only rebuild/cull/sort then, and go idle once every
|
||||||
// tile is streamed in — so a stationary player costs ~nothing per frame.
|
// desired tile is streamed in — so a stationary player costs ~nothing per frame.
|
||||||
FIntVector LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
FIntVector LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||||
bool bAllChunksLoaded = false;
|
bool bAllChunksLoaded = false;
|
||||||
bool bStartupTraceDesiredRecorded = false;
|
bool bStartupTraceDesiredRecorded = false;
|
||||||
TArray<FVoxelTileKey> DesiredSorted; // desired tiles, nearest-first
|
TArray<FVoxelTileKey> DesiredSorted; // critical floor prefix, then true-pawn distance-first
|
||||||
|
TArray<FVoxelTileKey> CriticalDesiredTiles; // occupied/support tile, then heading tile
|
||||||
|
uint32 DesiredEpoch = 0; // increments whenever DesiredStamped is rebuilt
|
||||||
|
int32 ObsoleteTileAbortCount = 0; // cumulative per-world moving-session cancellation count
|
||||||
|
float PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
||||||
|
|
||||||
|
struct FStreamingLatencySample
|
||||||
|
{
|
||||||
|
double RequestToReadySeconds = 0.0;
|
||||||
|
double QueueWaitSeconds = 0.0;
|
||||||
|
double GenerationSeconds = 0.0;
|
||||||
|
double CollisionCookSeconds = 0.0;
|
||||||
|
};
|
||||||
|
TArray<FStreamingLatencySample> LOD0ReadySamples;
|
||||||
|
|
||||||
|
// Headless validation hook. These are intentionally non-UPROPERTY state and are enabled only
|
||||||
|
// by explicit command-line switches; they do not alter ordinary player movement or streaming.
|
||||||
|
bool bHeadlessStreamingTestMovement = false;
|
||||||
|
bool bHeadlessStreamingTestExitRequested = false;
|
||||||
|
int32 HeadlessStreamingTestMoveAttempts = 0;
|
||||||
|
double HeadlessStreamingTestDistanceCm = 0.0;
|
||||||
|
float HeadlessStreamingTestSpeedCmPerSecond = 0.0f;
|
||||||
|
float HeadlessStreamingTestStartSeconds = 0.0f;
|
||||||
|
float HeadlessStreamingTestExitSeconds = 0.0f;
|
||||||
|
FVector HeadlessStreamingTestDirection = FVector::XAxisVector;
|
||||||
|
FVector HeadlessStreamingTestLastActualPosition = FVector::ZeroVector;
|
||||||
|
double HeadlessStreamingTestBeginSeconds = 0.0;
|
||||||
|
double HeadlessStreamingTestLastElapsedSeconds = 0.0;
|
||||||
|
|
||||||
// PLAYER COLLISION GATE — the pawn is allowed to enter/leave a tile only when the level-0
|
// PLAYER COLLISION GATE — the pawn is allowed to enter/leave a tile only when the level-0
|
||||||
// collision body covering its feet has completed the RMC cook. The movement component state is
|
// collision body covering its feet has completed the RMC cook. The movement component state is
|
||||||
|
|||||||
@@ -268,7 +268,6 @@ struct FExploreArguments
|
|||||||
bool bProfileDensityFull = false;
|
bool bProfileDensityFull = false;
|
||||||
bool bProfileLod = false;
|
bool bProfileLod = false;
|
||||||
bool bOpBounds = false;
|
bool bOpBounds = false;
|
||||||
bool bProfileTileVerdictCache = false;
|
|
||||||
bool bFailureFocusRender = false;
|
bool bFailureFocusRender = false;
|
||||||
FString OutDirectory;
|
FString OutDirectory;
|
||||||
|
|
||||||
@@ -361,8 +360,6 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
|||||||
OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull;
|
OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull;
|
||||||
OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod"));
|
OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod"));
|
||||||
OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds"));
|
OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds"));
|
||||||
OutArguments.bProfileTileVerdictCache = FParse::Param(
|
|
||||||
*Params, TEXT("profiletileverdictcache"));
|
|
||||||
FParse::Value(*Params, TEXT("out="), OutText);
|
FParse::Value(*Params, TEXT("out="), OutText);
|
||||||
FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth);
|
FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth);
|
||||||
FParse::Value(*Params, TEXT("renderheight="), OutArguments.RenderHeight);
|
FParse::Value(*Params, TEXT("renderheight="), OutArguments.RenderHeight);
|
||||||
@@ -4352,8 +4349,6 @@ FString BuildExploreJson(
|
|||||||
Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity);
|
Writer->WriteValue(TEXT("profile_density"), Arguments.bProfileDensity);
|
||||||
Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull);
|
Writer->WriteValue(TEXT("profile_density_full"), Arguments.bProfileDensityFull);
|
||||||
Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod);
|
Writer->WriteValue(TEXT("profile_lod"), Arguments.bProfileLod);
|
||||||
Writer->WriteValue(TEXT("profile_tile_verdict_cache"),
|
|
||||||
Arguments.bProfileTileVerdictCache);
|
|
||||||
Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds);
|
Writer->WriteValue(TEXT("op_bounds"), Arguments.bOpBounds);
|
||||||
Writer->WriteValue(TEXT("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes));
|
Writer->WriteValue(TEXT("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes));
|
||||||
Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory);
|
Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory);
|
||||||
@@ -5410,66 +5405,6 @@ bool RunOpBounds(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RunTileVerdictCacheProbe(
|
|
||||||
const FExploreArguments& Arguments,
|
|
||||||
FExploreWorld& World,
|
|
||||||
FString& OutError)
|
|
||||||
{
|
|
||||||
if (!World.Generator)
|
|
||||||
{
|
|
||||||
OutError = TEXT("tile-verdict cache probe has no generator.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deliberately use a region well above the synthetic layout. The first 32-cell proof is a
|
|
||||||
// genuine enclosing-region certificate; the 8-cell requests below are aligned subregions of
|
|
||||||
// that same lattice. No generated cave data is involved in this probe.
|
|
||||||
const FIntVector RegionOrigin(-32, -32, World.TargetTopWorldZ + 1024);
|
|
||||||
const EVoxelTileClass RegionVerdict = World.Generator->ClassifyTile(
|
|
||||||
RegionOrigin, 1, 32);
|
|
||||||
if (RegionVerdict != EVoxelTileClass::AllAir)
|
|
||||||
{
|
|
||||||
OutError = FString::Printf(
|
|
||||||
TEXT("tile-verdict cache probe expected an all-air enclosing region, got %d."),
|
|
||||||
static_cast<int32>(RegionVerdict));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ClassifyChildren = [&World, &RegionOrigin]()
|
|
||||||
{
|
|
||||||
for (int32 Z = 0; Z < 32; Z += 8)
|
|
||||||
for (int32 Y = 0; Y < 32; Y += 8)
|
|
||||||
for (int32 X = 0; X < 32; X += 8)
|
|
||||||
{
|
|
||||||
World.Generator->ClassifyTile(
|
|
||||||
RegionOrigin + FIntVector(X, Y, Z), 1, 8);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ClassifyChildren();
|
|
||||||
World.Generator->ClassifyTile(RegionOrigin, 1, 32);
|
|
||||||
ClassifyChildren();
|
|
||||||
|
|
||||||
const VoxelDensityProfile::FSnapshot Snapshot = VoxelDensityProfile::Snapshot();
|
|
||||||
const int32 CallsIndex = static_cast<int32>(
|
|
||||||
VoxelDensityProfile::ECounter::TileClassifyCalls);
|
|
||||||
const int32 HitsIndex = static_cast<int32>(
|
|
||||||
VoxelDensityProfile::ECounter::TileVerdictCacheHits);
|
|
||||||
const int32 RegionHitsIndex = static_cast<int32>(
|
|
||||||
VoxelDensityProfile::ECounter::TileVerdictRegionHits);
|
|
||||||
const int32 StoresIndex = static_cast<int32>(
|
|
||||||
VoxelDensityProfile::ECounter::TileVerdictCacheStores);
|
|
||||||
UE_LOG(LogTemp, Display,
|
|
||||||
TEXT("[VoxelForgeTileVerdictCache] probe region=(%d,%d,%d) cells=32 verdict=AllAir "
|
|
||||||
"calls=%llu hits=%llu region_hits=%llu stores=%llu"),
|
|
||||||
RegionOrigin.X, RegionOrigin.Y, RegionOrigin.Z,
|
|
||||||
static_cast<unsigned long long>(Snapshot.Counters[CallsIndex]),
|
|
||||||
static_cast<unsigned long long>(Snapshot.Counters[HitsIndex]),
|
|
||||||
static_cast<unsigned long long>(Snapshot.Counters[RegionHitsIndex]),
|
|
||||||
static_cast<unsigned long long>(Snapshot.Counters[StoresIndex]));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
|
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
|
||||||
@@ -5526,16 +5461,6 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
|||||||
}
|
}
|
||||||
const double SetupSeconds = FPlatformTime::Seconds() - SetupStartSeconds;
|
const double SetupSeconds = FPlatformTime::Seconds() - SetupStartSeconds;
|
||||||
|
|
||||||
if (Arguments.bProfileTileVerdictCache)
|
|
||||||
{
|
|
||||||
if (!RunTileVerdictCacheProbe(Arguments, World, Error))
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Error,
|
|
||||||
TEXT("[VoxelForgeExplore] tile-verdict cache probe failed: %s"), *Error);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bRequestedModeFailed = false;
|
bool bRequestedModeFailed = false;
|
||||||
if (Arguments.bOpBounds)
|
if (Arguments.bOpBounds)
|
||||||
{
|
{
|
||||||
|
|||||||
+7
-6
@@ -72,14 +72,15 @@ operation sits in the pipeline. It also makes interval proofs harder to trust.
|
|||||||
|
|
||||||
**Do**: make the stack the single semantic source of truth. Likely overlaps heavily with item 2.
|
**Do**: make the stack the single semantic source of truth. Likely overlaps heavily with item 2.
|
||||||
|
|
||||||
## 5. Prove the verdict cache earns its place, or remove it · small
|
## 5. Verdict cache removed after moving-session audit · complete
|
||||||
|
|
||||||
Zero outer hits in **both** traces. Global lock, linear scan, 512 entries, keyed on sample step so it
|
The clean moving-session audit found zero outer or nested hits: 2,109 outer probes and 19,904 nested
|
||||||
barely reuses across LODs. It helped a deliberately repeated commandlet case, which is not how a
|
probes produced 15,877 stores and 15,365 replacements in the full 512-entry table. Same-binary
|
||||||
moving player streams.
|
cache-on/cache-off request-to-ready was 0.367645/0.365978 s p95 and generation was 0.285559/0.285992
|
||||||
|
s p95, so the cache was neutral within run noise while paying the global lock and linear scans.
|
||||||
|
|
||||||
**Do**: measure nested/outer calls, hits and evictions on a real session. If it is neutral or negative,
|
The global verdict cache, lock, entry table, invalidation path, cache-only profiler fields, and
|
||||||
take it out and reclaim the memory.
|
deliberate cache probe are removed. The proof classifier remains; item 4 is still deliberately untouched.
|
||||||
|
|
||||||
## 6. Three small, sharp ones · tiny
|
## 6. Three small, sharp ones · tiny
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user