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
+650
-504
No files matched your search
+24
-12
@@ -557,10 +557,16 @@ ApplyModification` (BlueprintCallable) + `EditorCarveSphere/EditorFillSphere` (C
|
||||
driven by `EditorBrush*` props.
|
||||
|
||||
### 8.10 Performance invariants (DON'T regress)
|
||||
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set ONLY when the
|
||||
player crosses a chunk boundary (`LastUpdateCenter`); use the `DesiredSet` TSet for the cull;
|
||||
idle via `bAllChunksLoaded`. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.)
|
||||
- **LOD** changes HOT-SWAP (`LoadChunk` only, never unload-first) → no holes. LOD
|
||||
- **Streaming** (`UpdateChunksAroundPosition`): rebuild/cull the desired set only when the player
|
||||
crosses a level-0 chunk boundary, an anchor moves, or a rebuild is forced; use the stamped
|
||||
desired map for the cull and idle via `bAllChunksLoaded`. On every rebuild, pending requests whose
|
||||
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),
|
||||
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
|
||||
@@ -688,9 +694,13 @@ driven by `EditorBrush*` props.
|
||||
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-
|
||||
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 —
|
||||
they finish, apply, and are culled later if no longer desired. 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
|
||||
the better mesh is wholly in, then swaps. In-flight requests are cancelled when their key leaves the
|
||||
desired set: the game thread flips the request token, the worker returns an aborted result, and the
|
||||
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),
|
||||
`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
|
||||
@@ -756,11 +766,13 @@ driven by `EditorBrush*` props.
|
||||
(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
|
||||
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
|
||||
no longer desired are caught at apply time (`ProcessPendingChunks` adds them to the hold);
|
||||
`UnloadTile` drops hold entries; the settled cull (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**
|
||||
trace, plus `BuildDesiredTiles` 0.66 ms on the same frames). Pending requests that become leavers
|
||||
are marked obsolete before replacement submission; `ProcessPendingChunks` reads the token, drops
|
||||
the aborted result, and does not put it in the hold. `ApplyTileResult` independently rejects a
|
||||
wrong generation epoch or undesired key. `UnloadTile` drops hold entries; the settled cull
|
||||
(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
|
||||
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
|
||||
|
||||
+13
-11
@@ -47,7 +47,7 @@ A **density-field voxel terrain** plugin for Unreal Engine, built around undergr
|
||||
Tick → UpdateChunksAroundPosition
|
||||
│ (load/unload around player, by distance + LOD)
|
||||
▼
|
||||
LoadChunk → UE::Tasks::Launch ──────────► background thread
|
||||
LoadTile → UE::Tasks::Launch ──────────► background thread
|
||||
│
|
||||
UVoxelMarchingCubesMesher::GenerateMesh(chunk, step) ◄────────────────┘
|
||||
│ 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`
|
||||
`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`,
|
||||
`DiffLayer` (VoxelWorld.h:55-78). **Storage:** `Chunks` map, `ChunkMeshes` map,
|
||||
`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** :
|
||||
`(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`). |
|
||||
| `EndPlay` | 140 | Sets `bShuttingDown`, **waits for `ActiveTaskCount`→0**, unbinds delegate. |
|
||||
| `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.) |
|
||||
| `ProcessPendingChunks` | 301 | Drains ProcessQueue under per-frame budget; applies each 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). |
|
||||
| `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 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. |
|
||||
| `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. |
|
||||
| `BuildDesiredTiles` | ~722 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells + F18 sheet ring) then `AddAnchorDesiredTiles()` before the leaver sweep. |
|
||||
| `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; 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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `UnloadTile` | — | Clears tile state; the component is PARKED in the pool (T2.c), not destroyed. |
|
||||
| `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; 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. |
|
||||
| `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. |
|
||||
@@ -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). |
|
||||
| LOD step mapping | `AVoxelWorld::LODToStep` VoxelWorld.cpp:268; `GetLODForChunk` :242. |
|
||||
| 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`. |
|
||||
| Tweak room/tunnel shapes | `VoxelCaveMorphology.cpp` `BuildChunkCache` :47 / `EvaluateSDFCached` :757. |
|
||||
| Worm tunnel behavior | `GetDensityWithParams` Step 5, VoxelGenerator.cpp:1241. |
|
||||
|
||||
@@ -480,9 +480,6 @@ namespace VoxelDensityProfile
|
||||
case ECounter::TileClassifyAllSolid: return TEXT("TileClassifyAllSolid");
|
||||
case ECounter::TileClassifyAllAir: return TEXT("TileClassifyAllAir");
|
||||
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::FusedTunnelDetailSamples: return TEXT("FusedTunnelDetailSamples");
|
||||
case ECounter::FusedTunnelDetailSkipped: return TEXT("FusedTunnelDetailSkipped");
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/ScopeLock.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
#include "VoxelStrateComposer.h"
|
||||
@@ -56,6 +55,7 @@ namespace
|
||||
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)
|
||||
{
|
||||
// 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;
|
||||
WorldRadiusVoxels = Settings ? Settings->GetEffectiveWorldRadiusVoxels() : 0.0f;
|
||||
EdgeSealThickness = Settings ? Settings->EdgeSealThickness : 64.0f;
|
||||
InvalidateTileVerdictCache();
|
||||
}
|
||||
|
||||
void UVoxelGenerator::BeginDensityBlock(FIntVector OriginVoxels, int32 Step,
|
||||
@@ -5115,48 +4951,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
VoxelDensityProfile::AddCounter(
|
||||
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(
|
||||
OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, OutStats);
|
||||
if (Verdict == EVoxelTileClass::AllSolid)
|
||||
@@ -5175,15 +4969,6 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,6 @@ namespace
|
||||
int32 AllAir = 0;
|
||||
int32 AllSolid = 0;
|
||||
int32 Mixed = 0;
|
||||
int32 CacheHits = 0;
|
||||
int32 RegionHits = 0;
|
||||
int64 Triangles = 0;
|
||||
|
||||
for (const FTileSample& Sample : Samples)
|
||||
@@ -110,8 +108,6 @@ namespace
|
||||
if (Sample.Verdict == 0) ++Mixed;
|
||||
else if (Sample.Verdict == 1) ++AllSolid;
|
||||
else if (Sample.Verdict == 2) ++AllAir;
|
||||
if (Sample.bCacheHit) ++CacheHits;
|
||||
if (Sample.bRegionHit) ++RegionHits;
|
||||
Triangles += Sample.Triangles;
|
||||
Request.Add(Sample.RequestToApplySeconds);
|
||||
Queue.Add(Sample.QueueWaitSeconds);
|
||||
@@ -122,14 +118,14 @@ namespace
|
||||
}
|
||||
|
||||
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},"
|
||||
"\"queue_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"generation_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"classify_s\":{\"p50\":%s,\"p95\":%s,\"max\":%s},"
|
||||
"\"mesh_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(Queue, 0.50)), *Number(Percentile(Queue, 0.95)), *Number(Maximum(Queue)),
|
||||
*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->TileRecordsWritten;
|
||||
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,"
|
||||
"\"generation_s\":%s,\"classify_s\":%s,\"mesh_s\":%s,\"stream_s\":%s,\"apply_s\":%s"),
|
||||
Sample.Level, Sample.Verdict, Sample.bEmpty ? 1 : 0,
|
||||
Sample.bCacheHit ? 1 : 0, Sample.bRegionHit ? 1 : 0, Sample.Triangles,
|
||||
Sample.Level, Sample.Verdict, Sample.bEmpty ? 1 : 0, Sample.Triangles,
|
||||
*Number(Sample.RequestToApplySeconds), *Number(Sample.QueueWaitSeconds),
|
||||
*Number(Sample.WorkerQueueSeconds), *Number(Sample.ResultQueueSeconds),
|
||||
*Number(Sample.GenerationSeconds), *Number(Sample.ClassifySeconds),
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "HAL/IConsoleManager.h"
|
||||
#include "HAL/PlatformMisc.h"
|
||||
#include "ProfilingDebugging/CpuProfilerTrace.h" // Unreal Insights scopes (Perf 0)
|
||||
|
||||
#if WITH_EDITOR
|
||||
@@ -79,33 +80,6 @@ namespace
|
||||
bool GVoxelForgeStreamingBudgetReported = false;
|
||||
bool GVoxelForgeGenerationCapHitReported = false;
|
||||
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()
|
||||
@@ -473,8 +447,22 @@ void AVoxelWorld::RegenerateAllChunks()
|
||||
// Density volume: bump epoch (drop in-flight fills) + drop data → full refill next Tick.
|
||||
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
|
||||
// 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
|
||||
// try to UnloadTile coords that no longer exist.
|
||||
PendingUnload.Empty();
|
||||
@@ -489,7 +477,9 @@ void AVoxelWorld::RegenerateAllChunks()
|
||||
LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||
bAllChunksLoaded = false;
|
||||
DesiredSorted.Reset();
|
||||
CriticalDesiredTiles.Reset();
|
||||
DesiredStamped.Reset();
|
||||
++DesiredEpoch;
|
||||
TransitionHold.Reset();
|
||||
TransitionHoldQueue.Reset();
|
||||
TransitionHoldCursor = 0;
|
||||
@@ -970,13 +960,17 @@ void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
}
|
||||
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
|
||||
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();
|
||||
PendingTileCancellation.Empty();
|
||||
PendingUnload.Empty();
|
||||
DirtyRemeshQueue.Empty();
|
||||
BandRemeshQueue.Empty();
|
||||
@@ -1034,6 +1028,10 @@ void AVoxelWorld::BeginPlay()
|
||||
VoxelForgeStartupTrace::FStageScope StartupTraceStage(TEXT("BeginPlay"));
|
||||
Super::BeginPlay();
|
||||
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
|
||||
// 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)
|
||||
{
|
||||
Super::Tick(DeltaTime);
|
||||
@@ -1225,8 +1372,15 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
const bool bHasPlayer = TryGetPlayerPosition(PlayerLastPos, &PlayerPawn);
|
||||
|
||||
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);
|
||||
UpdateChunksAroundPosition(PlayerLastPos);
|
||||
UpdateChunksAroundPosition(PlayerLastPos, PlayerPawn, PlayerHeading);
|
||||
if (AtmosphereManager)
|
||||
{
|
||||
AtmosphereManager->UpdateForPlayer(PlayerLastPos);
|
||||
@@ -1323,10 +1477,12 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
TEXT("\"desired_tiles\":%d,\"loaded_tiles\":%d,\"collision_ready_tiles\":%d,"
|
||||
"\"collision_not_required_tiles\":%d,\"pending_collision_cooks\":%d,"
|
||||
"\"pawn_collision_gate_engaged\":%d,"
|
||||
"\"obsolete_tile_aborts\":%d,\"desired_epoch\":%u,"
|
||||
"\"pending_tiles\":%d,\"pending_unload\":%d,\"generation_epoch\":%u"),
|
||||
DesiredSorted.Num(), LoadedTiles.Num(), CollisionReadyTiles.Num(),
|
||||
CollisionNotRequiredTiles.Num(), PendingCollisionCooks.Num(),
|
||||
bPawnCollisionGateEngaged ? 1 : 0,
|
||||
ObsoleteTileAbortCount, DesiredEpoch,
|
||||
PendingTiles.Num(), PendingUnload.Num(), GenerationEpoch));
|
||||
TArray<int32> SteadyLevels;
|
||||
SteadyLevelCounts.GetKeys(SteadyLevels);
|
||||
@@ -1376,6 +1532,7 @@ void AVoxelWorld::Tick(float DeltaTime)
|
||||
GVoxelForgePendingModificationStrength);
|
||||
}
|
||||
ProcessUnloadQueue();
|
||||
MaybeFinishHeadlessStreamingTest();
|
||||
|
||||
#if ENABLE_DRAW_DEBUG
|
||||
// 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(
|
||||
const FVoxelTileKey& Tile, uint64 SubmissionId, URealtimeMesh* Mesh, uint8 Result)
|
||||
{
|
||||
@@ -1893,6 +2139,11 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
|
||||
const uint64 MeasuredCollisionSubmittedCycles = Pending->CollisionSubmittedCycles;
|
||||
const bool bBodyUpdatedEventSeen = Pending->bBodyUpdatedEventSeen;
|
||||
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);
|
||||
|
||||
const ERealtimeMeshCollisionUpdateResult CollisionResult =
|
||||
@@ -1913,6 +2164,12 @@ void AVoxelWorld::HandleTileCollisionCookComplete(
|
||||
}
|
||||
|
||||
const uint64 ReadyCycles = FPlatformTime::Cycles64();
|
||||
if (bBodyInstalled && Tile.Level == 0 && bRecordStreamingLatency)
|
||||
{
|
||||
RecordLOD0ReadySample(MeasuredRequestStartCycles, MeasuredGenerationStartCycles,
|
||||
MeasuredGenerationEndCycles, MeasuredApplyStartCycles,
|
||||
ReadyCycles, MeasuredCollisionSubmittedCycles);
|
||||
}
|
||||
const double MeshToCookSeconds = FPlatformTime::ToSeconds64(
|
||||
ReadyCycles - MeasuredMeshSubmittedCycles);
|
||||
const double CollisionRequestToCookSeconds = FPlatformTime::ToSeconds64(
|
||||
@@ -2008,12 +2265,25 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
while (ProcessQueue.Dequeue(DequeuedChunk))
|
||||
{
|
||||
++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);
|
||||
|
||||
// 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.
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -2059,8 +2329,11 @@ void AVoxelWorld::ProcessPendingChunks()
|
||||
// SyncRemeshTile (synchronous carve). Returns true iff a visible mesh was uploaded (budget).
|
||||
bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
{
|
||||
// Discard results from a previous generation epoch (stale).
|
||||
if (Result.bAborted || Result.Epoch != GenerationEpoch)
|
||||
// Discard results from a previous generation epoch (stale), or for a tile that has since left
|
||||
// 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;
|
||||
}
|
||||
@@ -2070,9 +2343,8 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
// in HandleTileCollisionCookComplete.
|
||||
const bool bResultEmpty = Result.bEmpty || !Result.Streams;
|
||||
const bool bTraceTile = VoxelForgeStartupTrace::IsActive();
|
||||
const bool bTelemetry = GVoxelForgeProfileTileGeneration != 0 || bTraceTile;
|
||||
const uint64 ApplyStartCycles = bTelemetry
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
const uint64 ApplyStartCycles = FPlatformTime::Cycles64();
|
||||
Result.ApplyStartCycles = ApplyStartCycles;
|
||||
const auto LogTileApplied = [&]()
|
||||
{
|
||||
if ((GVoxelForgeProfileTileGeneration != 0 || bTraceTile)
|
||||
@@ -2112,8 +2384,6 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
Sample.Level = Result.Tile.Level;
|
||||
Sample.Verdict = Result.ClassifyVerdict;
|
||||
Sample.bEmpty = bResultEmpty;
|
||||
Sample.bCacheHit = Result.bClassifierCacheHit;
|
||||
Sample.bRegionHit = Result.bClassifierRegionHit;
|
||||
Sample.Triangles = Result.NumTriangles;
|
||||
Sample.RequestToApplySeconds = RequestToApply;
|
||||
Sample.WorkerQueueSeconds = WorkerQueueSeconds;
|
||||
@@ -2179,11 +2449,13 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
FPlatformTime::Cycles64() - EmptyReleaseStartCycles));
|
||||
}
|
||||
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();
|
||||
if (Result.Tile.Level == 0 && Result.bRecordStreamingLatency)
|
||||
{
|
||||
RecordLOD0ReadySample(Result.RequestStartCycles, Result.GenerationStartCycles,
|
||||
Result.GenerationEndCycles, Result.ApplyStartCycles,
|
||||
FPlatformTime::Cycles64(), 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2204,7 +2476,6 @@ bool AVoxelWorld::ApplyTileResult(FChunkResult& Result)
|
||||
return false;
|
||||
}
|
||||
LoadedTiles.Add(Result.Tile);
|
||||
if (!IsDesired(Result.Tile)) { AddToTransitionHold(Result.Tile); }
|
||||
LogTileApplied();
|
||||
return true;
|
||||
}
|
||||
@@ -2238,6 +2509,8 @@ void AVoxelWorld::SyncRemeshTile(const FVoxelTileKey& Tile)
|
||||
}
|
||||
|
||||
FChunkResult Result;
|
||||
Result.DesiredEpoch = DesiredEpoch;
|
||||
Result.bRecordStreamingLatency = false;
|
||||
Result.RequestStartCycles = 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,
|
||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||
/*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
|
||||
? 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);
|
||||
DesiredSorted.Reset();
|
||||
CriticalDesiredTiles.Reset();
|
||||
OutLeavers.Reset();
|
||||
CollisionOnlyTiles.Reset(); // §9.4 — rebuilt by AddAnchorDesiredTiles below
|
||||
++DesiredStamp; // les upserts ci-dessous marquent le crossing courant
|
||||
++DesiredEpoch;
|
||||
|
||||
const int32 R = Settings ? FMath::Max(1, Settings->ClipRadius) : 3;
|
||||
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.
|
||||
const FVector PlayerVoxel = (FVector(Center) + FVector(0.5f, 0.5f, 0.5f)) * (float)CHUNK_SIZE;
|
||||
// Cancel work that no longer belongs to this desired set before scheduling replacements. A
|
||||
// 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)
|
||||
{
|
||||
const FVector CA = A.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
|
||||
@@ -2648,10 +3021,10 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||
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);
|
||||
VF_ParseTestStreamingCenter();
|
||||
const int32 MaxTasks = GetMaxConcurrentTasks();
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& !GVoxelForgeStreamingBudgetReported)
|
||||
@@ -2676,11 +3049,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
||||
GVoxelForgeStreamingBudgetReported = true;
|
||||
}
|
||||
|
||||
FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(CenterPosition)); // player's level-0 tile
|
||||
if (GVoxelForgeUseTestStreamingCenter)
|
||||
{
|
||||
CenterChunk = GVoxelForgeTestStreamingCenter;
|
||||
}
|
||||
const FIntVector CenterChunk = WorldToChunkCoord(WorldToLocalCm(CenterPosition)); // player's level-0 tile
|
||||
CurrentCenterChunk = CenterChunk;
|
||||
|
||||
// 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;
|
||||
bForceDesiredRebuild = false; // consumed
|
||||
|
||||
// DELTA CULL: BuildDesiredTiles renvoie les LEAVERS (désirées au crossing précédent, plus
|
||||
// maintenant). Seuls candidats au cull : ces leavers + la TransitionHold (retenues des
|
||||
// crossings passés). Fini le re-scan de TOUTES les tuiles chargées à chaque crossing —
|
||||
// c'était le spike CullTiles ~1.6 ms/crossing (trace 2026-07-05). Les tuiles en vol qui
|
||||
// finissent hors desired sont capturées à l'apply (ProcessPendingChunks → TransitionHold),
|
||||
// et le settled cull (tout chargé) reste le filet de sécurité plein-scan.
|
||||
// DELTA CULL: BuildDesiredTiles returns the LEAVERS (desired on the previous crossing, but
|
||||
// not now). Only those leavers + TransitionHold (retained across prior crossings) enter
|
||||
// the cull. Pending work that leaves desired is marked obsolete before replacement submit;
|
||||
// its aborted result is drained and dropped, while the settled cull remains the full-scan
|
||||
// safety net.
|
||||
TArray<FVoxelTileKey> Leavers;
|
||||
const bool bTraceFirstDesired = VoxelForgeStartupTrace::IsActive()
|
||||
&& !bStartupTraceDesiredRecorded;
|
||||
@@ -2727,7 +3095,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
||||
{
|
||||
VoxelForgeStartupTrace::StageBegin(TEXT("BuildDesiredTiles.first"));
|
||||
}
|
||||
BuildDesiredTiles(CenterChunk, Leavers);
|
||||
BuildDesiredTiles(CenterChunk, CenterPosition, PlayerPawn, PlayerHeading, Leavers);
|
||||
if (bTraceFirstDesired)
|
||||
{
|
||||
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.
|
||||
//=========================================================================
|
||||
if (!bAllChunksLoaded)
|
||||
@@ -3000,14 +3368,25 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles);
|
||||
int32 Submitted = 0;
|
||||
|
||||
// DIG RESPONSIVENESS — drain player-carve re-meshes FIRST (ahead of streaming + band) at
|
||||
// BackgroundHigh, so a dig gets the task budget before any streaming gen. A tile that's still
|
||||
// in flight is KEPT queued (retried next frame) so its stale pre-carve result is corrected.
|
||||
// FLOOR RESPONSIVENESS — the occupied support tile and the next tile along the pawn heading
|
||||
// are an absolute prefix, ahead of dirty remeshes, band work, and ordinary clipmap work.
|
||||
// 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)
|
||||
{
|
||||
if (PendingTiles.Num() >= MaxTasks) break;
|
||||
const FVoxelTileKey T = *It;
|
||||
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
|
||||
It.RemoveCurrent();
|
||||
LoadTile(T, /*bHighPriority*/ true);
|
||||
@@ -3030,7 +3409,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition)
|
||||
if (PendingTiles.Num() >= MaxTasks) break;
|
||||
const FVoxelTileKey T = *It;
|
||||
It.RemoveCurrent();
|
||||
if (PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
||||
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
||||
LoadTile(T);
|
||||
++Submitted;
|
||||
}
|
||||
@@ -3087,10 +3466,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
return; // Budget full — wait for a task to finish.
|
||||
}
|
||||
PendingTiles.Add(Tile);
|
||||
const bool bTelemetry = GVoxelForgeProfileTileGeneration != 0
|
||||
|| VoxelForgeStartupTrace::IsActive();
|
||||
const uint64 RequestStartCycles = bTelemetry
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
const uint64 RequestStartCycles = FPlatformTime::Cycles64();
|
||||
|
||||
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 uint32 TaskEpoch = GenerationEpoch;
|
||||
const uint32 TaskDesiredEpoch = DesiredEpoch;
|
||||
|
||||
// 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
|
||||
@@ -3144,6 +3521,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
FChunkResult Empty;
|
||||
Empty.Tile = Tile;
|
||||
Empty.Epoch = TaskEpoch;
|
||||
Empty.DesiredEpoch = TaskDesiredEpoch;
|
||||
Empty.RequestStartCycles = RequestStartCycles;
|
||||
ProcessQueue.Enqueue(MoveTemp(Empty));
|
||||
return;
|
||||
@@ -3174,6 +3552,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
FChunkResult Empty;
|
||||
Empty.Tile = Tile;
|
||||
Empty.Epoch = TaskEpoch;
|
||||
Empty.DesiredEpoch = TaskDesiredEpoch;
|
||||
Empty.RequestStartCycles = RequestStartCycles;
|
||||
Empty.BandChunkLo = BandChunkLo;
|
||||
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);
|
||||
|
||||
// BackgroundNormal priority: gen runs on background workers that YIELD to foreground
|
||||
// (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).
|
||||
// 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
|
||||
// worker (yields to the frame, keeps the invariant) but jumps AHEAD of all pending streaming gen,
|
||||
// so a dig is never queued behind a shell of streaming tasks.
|
||||
// FLOOR/DIG RESPONSIVENESS: critical support requests and player carves launch at BackgroundHigh
|
||||
// (bHighPriority) — still a background worker (yields to the frame, keeps the invariant) but
|
||||
// jumps AHEAD of ordinary pending streaming gen.
|
||||
const UE::Tasks::ETaskPriority TaskPriority = bHighPriority
|
||||
? UE::Tasks::ETaskPriority::BackgroundHigh
|
||||
: UE::Tasks::ETaskPriority::BackgroundNormal;
|
||||
UE::Tasks::Launch(TEXT("ChunkGen"), [this, Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
||||
RequestStartCycles, bTelemetry]()
|
||||
RequestStartCycles, TaskDesiredEpoch, Cancellation]()
|
||||
{
|
||||
// RAII: decrement the counter on every exit path.
|
||||
struct FTaskGuard
|
||||
@@ -3206,19 +3588,51 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, bool bHighPriority)
|
||||
~FTaskGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} 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 (Cancellation->bObsolete.load(std::memory_order_relaxed))
|
||||
{
|
||||
EnqueueObsoleteResult();
|
||||
return;
|
||||
}
|
||||
|
||||
FChunkResult Result;
|
||||
const uint64 GenerationStartCycles = bTelemetry
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
Result.DesiredEpoch = TaskDesiredEpoch;
|
||||
const uint64 GenerationStartCycles = FPlatformTime::Cycles64();
|
||||
Result.GenerationStartCycles = GenerationStartCycles;
|
||||
GenerateTileResult(Tile, OriginVoxels, Step, Cells, TaskEpoch, bWantCapture,
|
||||
BandVoxLo, BandVoxHi, BandChunkLo, BandChunkHi,
|
||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY, Result);
|
||||
bSheetTile, SheetChunkZ, HoleMinX, HoleMinY, HoleMaxX, HoleMaxY,
|
||||
Result, &Cancellation->bObsolete);
|
||||
Result.GenerationEndCycles = GenerationStartCycles != 0
|
||||
? FPlatformTime::Cycles64() : 0;
|
||||
Result.RequestStartCycles = RequestStartCycles;
|
||||
|
||||
if (Cancellation->bObsolete.load(std::memory_order_relaxed) || Result.bAborted)
|
||||
{
|
||||
if (!ShouldAbortWork())
|
||||
{
|
||||
EnqueueObsoleteResult();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ShouldAbortWork())
|
||||
{
|
||||
ProcessQueue.Enqueue(MoveTemp(Result)); // move: don't copy the geometry payload
|
||||
@@ -3233,7 +3647,8 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
||||
bool bSheetTile, int32 SheetChunkZ,
|
||||
int32 HoleMinX, int32 HoleMinY, int32 HoleMaxX, int32 HoleMaxY,
|
||||
FChunkResult& Result)
|
||||
FChunkResult& Result,
|
||||
const std::atomic<bool>* ObsoleteFlag)
|
||||
{
|
||||
const bool bLogTileProfile = GVoxelForgeProfileTileGeneration != 0;
|
||||
const bool bMeasureTile = bLogTileProfile || VoxelForgeStartupTrace::IsActive();
|
||||
@@ -3346,10 +3761,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
Result.Tile = Tile;
|
||||
Result.Epoch = Epoch;
|
||||
Result.bAborted = false;
|
||||
Result.bObsolete = false;
|
||||
Result.bEmpty = true;
|
||||
Result.ClassifyVerdict = -1;
|
||||
Result.bClassifierCacheHit = false;
|
||||
Result.bClassifierRegionHit = false;
|
||||
Result.ClassifySeconds = 0.0;
|
||||
Result.MeshSeconds = 0.0;
|
||||
Result.StreamSeconds = 0.0;
|
||||
@@ -3364,11 +3778,13 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
auto AbortResult = [&]()
|
||||
{
|
||||
Result.bAborted = true;
|
||||
Result.bObsolete = ObsoleteFlag != nullptr
|
||||
&& ObsoleteFlag->load(std::memory_order_relaxed);
|
||||
Result.bEmpty = true;
|
||||
Result.Streams.Reset();
|
||||
Result.CaptureGrid.Reset();
|
||||
};
|
||||
if (ShouldAbortWork())
|
||||
if (ShouldAbortWork(ObsoleteFlag))
|
||||
{
|
||||
AbortResult();
|
||||
EmitTileProfile();
|
||||
@@ -3414,7 +3830,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
bool bExactUniform = true;
|
||||
for (int32 Z = 0; Z <= ValidationCells && bExactUniform && !bValidationAborted; ++Z)
|
||||
{
|
||||
if (ShouldAbortWork())
|
||||
if (ShouldAbortWork(ObsoleteFlag))
|
||||
{
|
||||
bValidationAborted = true;
|
||||
break;
|
||||
@@ -3451,22 +3867,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
return;
|
||||
}
|
||||
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);
|
||||
Result.ClassifyVerdict = ClassifyVerdict;
|
||||
if (ShouldAbortWork())
|
||||
if (ShouldAbortWork(ObsoleteFlag))
|
||||
{
|
||||
AbortResult();
|
||||
EmitTileProfile();
|
||||
@@ -3501,7 +3904,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
BandVoxLo, BandVoxHi);
|
||||
MeshSeconds = bMeasureTile ? FPlatformTime::Seconds() - MeshStartSeconds : 0.0;
|
||||
INC_DWORD_STAT(STAT_VoxelForgeTilesMeshed);
|
||||
if (ShouldAbortWork())
|
||||
if (ShouldAbortWork(ObsoleteFlag))
|
||||
{
|
||||
AbortResult();
|
||||
EmitTileProfile();
|
||||
@@ -3518,7 +3921,7 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector
|
||||
Result.Streams = MakeShared<RealtimeMesh::FRealtimeMeshStreamSet>();
|
||||
BuildTileStreamSet(*Result.Streams, MeshData);
|
||||
StreamSeconds = bMeasureTile ? FPlatformTime::Seconds() - StreamStartSeconds : 0.0;
|
||||
if (ShouldAbortWork())
|
||||
if (ShouldAbortWork(ObsoleteFlag))
|
||||
{
|
||||
AbortResult();
|
||||
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
|
||||
// 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);
|
||||
CollisionNotRequiredTiles.Remove(Tile);
|
||||
CollisionSolidTiles.Remove(Tile);
|
||||
@@ -3652,7 +4062,6 @@ void AVoxelWorld::UnloadTile(const FVoxelTileKey& Tile)
|
||||
TileComponents.Remove(Tile);
|
||||
}
|
||||
LoadedTiles.Remove(Tile);
|
||||
PendingTiles.Remove(Tile);
|
||||
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.MeshSubmittedCycles = MeshSubmittedCycles;
|
||||
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);
|
||||
|
||||
// Register the serial before asking RMC to dirty collision. The current implementation
|
||||
|
||||
@@ -120,9 +120,6 @@ namespace VoxelDensityProfile
|
||||
TileClassifyAllSolid,
|
||||
TileClassifyAllAir,
|
||||
TileClassifyMixed,
|
||||
TileVerdictCacheHits,
|
||||
TileVerdictRegionHits,
|
||||
TileVerdictCacheStores,
|
||||
FusedTunnelSamples,
|
||||
FusedTunnelDetailSamples,
|
||||
FusedTunnelDetailSkipped,
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "HAL/CriticalSection.h"
|
||||
#include "VoxelTypes.h"
|
||||
#include "VoxelStrateTypes.h"
|
||||
#include "VoxelBiomeTypes.h"
|
||||
|
||||
#include "VoxelGenerator.generated.h"
|
||||
|
||||
// Forward decls (évite les includes transitifs)
|
||||
@@ -119,12 +119,10 @@ public:
|
||||
void SetStrateManager(const UVoxelStrateManager* InManager)
|
||||
{
|
||||
StrateManager = InManager;
|
||||
InvalidateTileVerdictCache();
|
||||
}
|
||||
void SetDiffLayer(const UVoxelDiffLayer* InDiffLayer)
|
||||
{
|
||||
DiffLayer = InDiffLayer;
|
||||
InvalidateTileVerdictCache();
|
||||
}
|
||||
|
||||
// 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. */
|
||||
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(
|
||||
const FIntVector& OriginVoxels, int32 Step, int32 CellsPerAxis,
|
||||
struct FVoxelTileClassificationStats* OutStats) const;
|
||||
|
||||
@@ -12,8 +12,6 @@ namespace VoxelForgeStartupTrace
|
||||
int32 Level = -1;
|
||||
int32 Verdict = -1; // Mixed=0, AllSolid=1, AllAir=2, -1=not classified (sheet/early path)
|
||||
bool bEmpty = true;
|
||||
bool bCacheHit = false;
|
||||
bool bRegionHit = false;
|
||||
int32 Triangles = 0;
|
||||
// This is worker-result -> game-thread mesh submission. Collision readiness is a later
|
||||
// per-tile RMC completion event recorded separately as collision_ready.
|
||||
|
||||
@@ -33,6 +33,13 @@ class UMaterialInstanceDynamic;
|
||||
class FScopedGenerationPause;
|
||||
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
|
||||
* 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).
|
||||
TSharedPtr<RealtimeMesh::FRealtimeMeshStreamSet> Streams;
|
||||
uint32 Epoch = 0; // Generation epoch — discard if stale
|
||||
// Monotonic request timestamp used only for streaming telemetry. It is carried through the
|
||||
// worker queue so the profile can report request-to-apply, not just worker generation time.
|
||||
// Monotonic request timestamp used for the low-overhead streaming latency summary and the
|
||||
// opt-in profile. It is carried through the worker queue so the report can separate scheduler
|
||||
// wait from generation time.
|
||||
uint64 RequestStartCycles = 0;
|
||||
// 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 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;
|
||||
bool bAborted = false; // Worker observed shutdown; never mark this tile loaded
|
||||
uint64 GenerationEndCycles = 0;
|
||||
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
|
||||
// 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
|
||||
@@ -225,6 +239,12 @@ public:
|
||||
uint64 CollisionSubmittedCycles = 0;
|
||||
bool bBodyUpdatedEventSeen = false;
|
||||
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
|
||||
@@ -474,6 +494,14 @@ private:
|
||||
/** Apply the current TVP0..4 + level-0 volume texture to one MID (also used on MID creation). */
|
||||
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
|
||||
// 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)
|
||||
@@ -647,7 +675,8 @@ public:
|
||||
*
|
||||
* @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.
|
||||
@@ -673,7 +702,8 @@ public:
|
||||
int32 BandVoxLo, int32 BandVoxHi, int32 BandChunkLo, int32 BandChunkHi,
|
||||
bool bSheetTile, int32 SheetChunkZ,
|
||||
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):
|
||||
@@ -736,7 +766,10 @@ public:
|
||||
/** 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
|
||||
* 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
|
||||
* 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 UnbindRealtimeMeshCollisionEvent(URealtimeMesh* Mesh);
|
||||
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
|
||||
// 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.
|
||||
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
||||
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
|
||||
// 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
|
||||
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)
|
||||
|| 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.
|
||||
@@ -861,13 +903,40 @@ public:
|
||||
void ReconcileAnchorTileVisibility();
|
||||
|
||||
// --- Streaming work-avoidance (perf) ---
|
||||
// The desired tile set only changes when the player crosses a level-0 tile boundary.
|
||||
// We cache it and only rebuild/cull/sort on a real move, and go idle once every desired
|
||||
// tile is streamed in — so a stationary player costs ~nothing per frame.
|
||||
// The desired tile set only changes when the player/anchor crosses a level-0 tile boundary or
|
||||
// a rebuild is forced. We cache it and only rebuild/cull/sort then, and go idle once every
|
||||
// desired tile is streamed in — so a stationary player costs ~nothing per frame.
|
||||
FIntVector LastUpdateCenter = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX);
|
||||
bool bAllChunksLoaded = 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
|
||||
// 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 bProfileLod = false;
|
||||
bool bOpBounds = false;
|
||||
bool bProfileTileVerdictCache = false;
|
||||
bool bFailureFocusRender = false;
|
||||
FString OutDirectory;
|
||||
|
||||
@@ -361,8 +360,6 @@ bool ParseArguments(const FString& Params, FExploreArguments& OutArguments, FStr
|
||||
OutArguments.bProfileDensity |= OutArguments.bProfileDensityFull;
|
||||
OutArguments.bProfileLod = FParse::Param(*Params, TEXT("profilelod"));
|
||||
OutArguments.bOpBounds = FParse::Param(*Params, TEXT("opbounds"));
|
||||
OutArguments.bProfileTileVerdictCache = FParse::Param(
|
||||
*Params, TEXT("profiletileverdictcache"));
|
||||
FParse::Value(*Params, TEXT("out="), OutText);
|
||||
FParse::Value(*Params, TEXT("renderwidth="), OutArguments.RenderWidth);
|
||||
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_full"), Arguments.bProfileDensityFull);
|
||||
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("budget_minutes"), static_cast<double>(Arguments.BudgetMinutes));
|
||||
Writer->WriteValue(TEXT("out_directory"), Arguments.OutDirectory);
|
||||
@@ -5410,66 +5405,6 @@ bool RunOpBounds(
|
||||
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
|
||||
|
||||
UVoxelForgeExploreCommandlet::UVoxelForgeExploreCommandlet()
|
||||
@@ -5526,16 +5461,6 @@ int32 RunExploreCase(const FString& Params, FString* OutJson)
|
||||
}
|
||||
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;
|
||||
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.
|
||||
|
||||
## 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
|
||||
barely reuses across LODs. It helped a deliberately repeated commandlet case, which is not how a
|
||||
moving player streams.
|
||||
The clean moving-session audit found zero outer or nested hits: 2,109 outer probes and 19,904 nested
|
||||
probes produced 15,877 stores and 15,365 replacements in the full 512-entry table. Same-binary
|
||||
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,
|
||||
take it out and reclaim the memory.
|
||||
The global verdict cache, lock, entry table, invalidation path, cache-only profiler fields, and
|
||||
deliberate cache probe are removed. The proof classifier remains; item 4 is still deliberately untouched.
|
||||
|
||||
## 6. Three small, sharp ones · tiny
|
||||
|
||||
|
||||
Reference in new issue
Block a user