wgpu: diagnostic instrumentation for cull/streaming perf

Adds three knobs and three new heartbeat numbers to support the
ongoing perf-parity work. None affect behaviour in default runs.

cull[wall|compute|upload] split timer
  The existing cull_timer wrapped both the parallel std::async dispatch
  and the sequential cullModelCpuUpload loop (queueWriteBuffer × 3 per
  resident chunk × ~120 chunks ≈ 360 wgpu calls per frame). Splitting
  them ruled upload out as the bottleneck on a 51-model federation
  scene: compute ≈ 16-17 ms, upload ≈ 1 ms.

WGPU_CULL_THREADS=0 — force sequential cull
  std::async-per-model was already in place; this env var disables it
  so we can compare wall time vs sequential and confirm parallelism is
  working. On the federation scene with 52 models: sequential 74 ms vs
  parallel 17 ms = 4.4× speedup. Confirmed; the 17 ms floor is not a
  parallelism failure, it's the cost of culling the largest single
  model (model 43, 114k instances) ÷ no parallelism within that model.

WGPU_STREAM_DEBUG=1 — per-frame [stream-debug] log
  Surfaces cands/enq/drained/ev_lru/ev_pri/blocked/resident/cycled/
  max_load each frame from driveStreamingLoads. The "cycled" /
  "max_load" pair makes thrash vs eviction-churn vs just-loading
  distinguishable. Off by default; opt-in via the env var.

Bench-warm timeout dump
  When [bench warm] times out (600 frames without 0-loads streak),
  prints a structured summary: resident/missing/total chunks,
  cycled count, pool usage, largest free run, avg missing chunk
  size, and an auto-classifier diagnosis (POOL FRAGMENTED vs
  WORKING SET > POOL vs FEW-CHUNK CYCLE vs still-loading). Caught
  a real fragmentation pattern (18 MB largest free run vs ~100 MB
  typical chunk) on a 51-model run where the dumb classifier
  would have called it a load-budget problem.

LOD1 firing counter
  "lod1 X/Y (saved Z tris, N no-lod1)" suffix on the [frame] log.
  X = LOD1-selected this frame, Y = LOD1-eligible, Z = tris not
  drawn vs always-LOD0, N = visible instances with no baked LOD1
  (mesh below IFC_LOD_MIN_TRIS). Confirmed LOD1 path is genuinely
  firing post the per-chunk LOD1-storage commit, and exposed that
  ~90% of instances in real scenes are no-lod1 meshes — relevant
  to the future LOD-tier-residency design.

Chunk.load_count + Chunk.lod0/1 layout bookkeeping
  Per-chunk reload counter for the thrash detector. lod0/1
  layout_count fields prep the data model for distance-tiered
  residency (Phase B of #31) but aren't acted on yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-29 12:17:03 +10:00
parent 6a3dd4a0eb
commit a63bc63439
3 changed files with 195 additions and 19 deletions
+18
View File
@@ -400,6 +400,12 @@ private:
float min_pixel_radius_ = 3.0f;
float motion_min_pixel_radius_ = 15.0f;
// Whether driveCull dispatches per-model work via std::async. ON by
// default; setting WGPU_CULL_THREADS=0 forces sequential cull for
// measurement (does std::async actually parallelize on this libstdc++?
// and is per-model the right granularity?).
bool cull_threads_enabled_ = true;
public:
// Master switch for HiZ occlusion. Set false to skip the depth resolve
// + readback + cull test entirely (matches IFC_NO_HIZ in the GL backend).
@@ -445,6 +451,16 @@ public:
int streaming_loads_this_frame_ = 0;
bool streaming_more_pending_ = false;
// Per-frame streaming counters for WGPU_STREAM_DEBUG. Mutated inside
// driveStreamingLoads, consumed by the per-frame debug print and the
// bench-warm timeout dump.
int streaming_candidates_this_frame_ = 0;
int streaming_evictions_lru_this_frame_ = 0;
int streaming_evictions_pri_this_frame_ = 0;
int streaming_drained_this_frame_ = 0;
int streaming_blocked_oom_this_frame_ = 0;
bool streaming_debug_ = false; // WGPU_STREAM_DEBUG=1
// Bench warm-phase counters. We wait until N consecutive frames with
// 0 loads (convergence) before starting the orbit sweep, capped by
// MAX_WARM_FRAMES so chronically thrashing scenes still produce
@@ -534,6 +550,8 @@ private:
// bench) so the periodic [frame] heartbeat log can show cull /
// stream cost without needing the bench averaging machinery.
double last_cull_ms_ = 0.0;
double last_cull_compute_ms_ = 0.0; // parallel per-model cull
double last_cull_upload_ms_ = 0.0; // sequential queueWriteBuffer pass
double last_stream_ms_ = 0.0;
// Tick count for the interactive (non-bench) [frame] heartbeat log.