wgpu streaming: screen-space AABB priority + grace period + interactive heartbeat

The chunk priority metric is now the 2D projected pixel area of the
chunk's AABB on screen — 8 corners projected through view-projection,
2D axis-aligned bbox of the projected points, clamped to viewport.
This replaces the prior bounding-sphere-radius² metric, which was a
3D approximation: it treated a 322 × 55 × 5 m slab as a 163 m sphere,
giving it the same huge priority face-on or edge-on. The new metric
genuinely answers "what would this chunk's AABB cover if rendered
solid given the current camera and viewport."

Newly-loaded chunks get a 30-frame grace period at full priority
(visibility_history floor temporarily forced to 1.0). Without it,
just-loaded chunks crashed to history=0 → effective priority = pri ×
0.05 → immediately reverse-swapped by the chunk they displaced.
Cycle starved the per-frame load budget so candidates ranked below
the cyclers never got attempted. 30 frames = HISTORY_ALPHA's time
constant — enough for visibility_history to develop meaningfully.

EVICT_PRIORITY_RATIO bumped 1.21 → 2.0 to suppress more swap noise
between similar-priority chunks.

Interactive heartbeat log added: every render in non-bench mode prints
[frame] with fps, ms, obj, sub_draws, hiz_rej, cull, stream, chunks
breakdown (resident/frustum/total + missing count), VRAM, model count.
Every 30 frames when something's missing, also dumps:
- top 8 models by missing chunk count
- top 20 missing chunks by priority (with AABBs)
- bottom 5 residents by effective priority
- all chunks of brace.ifc (one-off diagnostic, hardcoded
  for the brace-visibility investigation)

The heartbeat made the streaming bug visible: a brace model that
isolation-loads correctly is missing in the full set because slabs
covering more pixels win the priority contest. Per-model fairness or
manual pinning are the remaining options if pixel-area + grace +
hysteresis isn't enough — left for follow-up so the user can decide
based on real testing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-28 22:19:24 +10:00
parent 3d368e0079
commit c7fba1abb8
3 changed files with 405 additions and 59 deletions
+22
View File
@@ -178,6 +178,28 @@ struct WgpuModelGpuData {
// total_visible_draws > 0). The evictor picks the smallest value
// among non-visible resident chunks when it needs to free VRAM.
uint64_t last_visible_frame_idx = 0;
// EMA-smoothed visibility score, in [0, 1]. Bumped each frame
// toward 1 when total_visible_draws > 0 (the chunk's instances
// passed frustum + contribution + HiZ), toward 0 otherwise.
// Time constant ~30 frames. Used by the streaming evictor to
// de-prioritise chunks that are technically in the frustum but
// consistently HiZ-occluded — e.g. interior pipes behind a
// building's exterior walls. The smoothing prevents thrash from
// momentary HiZ flicker (a wall briefly visible behind a panning
// window doesn't displace the window from the pool).
float visibility_history = 0.0f;
// streaming_frame_idx_ when this chunk was last loaded. The
// evictor grants newly-loaded chunks ~30 frames of grace at
// full priority (max history factor = 1.0) so they have time
// for visibility_history to develop. Without this, a just-
// loaded chunk's effective priority drops to contribution ×
// 0.05 next frame, and the chunk it displaced — back as a
// candidate at full priority — re-displaces it: infinite
// cycle between equal-priority chunks. The cycle prevents any
// lower-priority candidate (e.g. a structural-brace chunk
// ranked position 20 in the missing list) from ever getting
// attempted.
uint64_t loaded_frame_idx = 0;
};
std::vector<Chunk> chunks;