forked from 1b-agent/opencadstudio-upstream
`gpu-live` made this the obvious next one. Once idle slots started giving their memory back, the shadow maps were the largest thing that never did: 128 MiB across eight slots, 37 % of the 341 MiB floor, and none of it sampled — `shadow_enabled` defaults to false and most visual styles never turn it on. Every slot allocated `SHADOW_MAP_SIZE` squared at `Depth32Float` in `Pipeline::new` and held it for the life of the slot, because the frame bind group needs *something* at its shadow binding and the field was not optional. It is now `Option<(Texture, TextureView)>` alongside a 1x1 placeholder that satisfies the binding while shadows are off — nothing samples it then, so one texel is the whole requirement. `sync_shadow_target` allocates on the transition into shadows and drops on the way out, and only on the transition, so a viewport that keeps its setting does not rebuild a bind group every frame. The frame bind group had two inline builders — one in `Pipeline::new`, one in `upload_background_images` — and this adds a third caller, so they are now one `rebuild_frame_bind_group`. A shadow transition cannot forget the background image the slot already had. The shadow pass now tests for the target rather than the flag: wanting shadows and having somewhere to put them are different questions, and without the second one a failed 16 MiB allocation would have rendered the pass into the 1x1 placeholder. Releasing a cold slot hands the target back too, and `gpu-live` reports what is actually held rather than a constant. Covered by a GPU test that toggles the setting off/on/off/on against a real adapter with a validation scope armed, then releases the slot — the transition is the risky path and the other GPU tests never take it. Co-Authored-By: Claude Opus 5 <[email protected]>