Graphics: per-feature rendering toggles + quality presets (wire up graphics.quality)

Part of #158.

Problem

The renderer has no quality dial at all.

ProjectSettings.graphics.quality (low/medium/high/ultra) is persisted, serialized, and editable in the Project Settings Inspector — and no code in subsystems/render/ ever reads it. The only references outside the settings struct are the Inspector editor and its own tests.

Meanwhile every expensive choice is hardcoded:

  • pipeline.pickSampleCount returns SAMPLECOUNT_4 whenever the device supports the format, with no override anywhere.
  • SSAO: 24 hemisphere taps at full resolution, plus a blur pass.
  • SSR: a 32-step march at full resolution.
  • 4 cascades at 2048², NUM_CASCADES fixed at comptime.
  • No render-scale option.

So there is no way for a user — or for us, while optimizing — to trade quality for speed, and no way to find out which feature is actually costing the frame.

Scope

A render.Features struct in state.zig with one toggle or value per feature, applied through render.setFeatures():

toggle today note
msaa forced 4× pipelines bake in the sample count, so changing it must rebuild the scene pipeline cache
render_scale 1.0 render at a fraction, upsample in the composite pass
ssao, ssao_half_res, ssao_samples on, full, 24
ssr, ssr_half_res, ssr_steps on, full, 32
shadows, shadow_dim, cascade_count on, 2048, 4 NUM_CASCADES is comptime today; needs a runtime bound, max 4 — FragUB packs the splits into one vec4
reflection_probes on
bloom on already per-volume; surface a global kill switch
lod_bias placeholder until #39 lands

Surfaced two ways:

1. A Studio "Rendering" control group, alongside the Profiler panel's existing vsync / fps-cap / detailed-GPU-timing controls: one checkbox or slider per feature, applied live with no restart. This is what makes the rest of #158 observable — flip one toggle, watch the frame chart move. It is also how we ground the presets in real hardware instead of guessing.

2. ProjectSettings.graphics.quality presets seeding the toggle set, finally giving that field meaning. The shipped game applies it at startup.

Suggested starting presets, to be re-grounded against #159 (closed)'s baseline numbers:

  • low — no MSAA, 0.75 render scale, half-res SSAO at 8 taps, SSR off, 1024 shadow map, 2 cascades
  • medium — no MSAA, 1.0 scale, half-res SSAO + SSR
  • high — today's behaviour
  • ultra — high + 4096 shadow map

Files

subsystems/render/{state,pipeline,ssao,ssr,shadow,postprocess,root}.zig, studio/main-window/ProfilerPanel.zig, engine/assets/ProjectSettings.zig, engine/Application.zig

Acceptance

  • Every feature above can be toggled live in Studio with no restart
  • graphics.quality seeds the toggles and is honoured by the shipped game
  • Changing MSAA correctly rebuilds the scene pipeline cache (pipelines bake in the sample count)
  • --features on #159 (closed)'s benchmark harness can drive a toggle set headlessly
  • Each toggle's measured fps delta on Bistro recorded in the baseline doc