Perf: depth/normal prepass issues 3,880 draw calls where the main pass issues 206

Part of #158. Phase 2 — no fidelity change.

Problem

The depth/normal prepass and the main pass draw the same geometry. The main pass does it in 206 indirect multi-draws. The prepass does it in ~3,880 individual draw calls.

prepass.run (prepass.zig:260-282) loops submeshes, not material groups, and submits one SDL_DrawGPUIndexedPrimitives each — with a resolveMaterial call, a CPU frustum test, two uniform pushes and a sampler bind per submesh. root.zig:648-689 does the same work per material group off gm.indirect_buf.

The prepass never got the indirect path, and it can't use it as written: it runs before gpu_timing.runCullPhase (root.zig:417 vs root.zig:504), so gm.indirect_buf holds no valid commands for the current frame yet.

This arrived with the SSAO phase and is the most likely single largest CPU cost in the frame — to be confirmed against #159 (closed)'s per-pass draw-call attribution before it's treated as settled.

Scope

  • Move runCullPhase ahead of prepass.run. Both already force a pass break (compute and render passes can't overlap on one command buffer), so the reorder costs nothing.
  • Draw per material group off gm.indirect_buf, resolving each material once per group instead of once per submesh.
  • Split the prepass pipeline into opaque and alpha-masked permutations — it currently binds one pipeline for everything and branches in the shader on a per-draw uniform, which a per-group indirect draw can't express.
  • Keep the existing CULLMODE_NONE choice: back-face culling here would punch holes in the depth buffer for double-sided foliage, which SSAO would reproject as false contact AO (see the comment at prepass.zig:81-84).

Acceptance

  • Prepass draw calls drop from ~3,880 to ~206 on Bistro (per-pass counter from #159 (closed))
  • SSAO and SSR output visually unchanged — fixed-camera before/after pair from the terrace viewpoint
  • Alpha-masked foliage still discards correctly in the prepass depth
  • Before/after render.prepass CPU and GPU time recorded