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:
Fr0zkaandClaude Opus 5 committed 2026-09-12 06:49:20 +02:00
1 parent 35de59bac5
commit cf0319d480
12 files changed
+660 -514

No files matched your search

+24 -12
View File
@@ -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