---
status: accepted (shipped 2026-06-11, ebc1ff8; field-tested same day)
---

# SteamVR coexistence: yield on launch, prox-gated reclaim on exit

Sauna and SteamVR want the same exclusive DirectMode display. v1 ships to team machines
where people also play PC VR, so "quit sauna before starting SteamVR" is a daily paper cut
and "sauna wins" is a broken SteamVR. We chose automatic yield-and-reclaim: sauna watches
for the SteamVR process (`vrserver.exe` / `vrcompositor.exe`), releases the ENTIRE
DirectMode acquisition the moment it appears, and re-acquires after it exits — immediately
if the headset is worn, otherwise on the next don (no prox hardware: immediately).

## Considered options

- **Refuse to run / exit when SteamVR is detected** — rejected: turns a coexistence problem
  into a babysitting problem; sauna should come back on its own after a VR session.
- **Manual tray toggle** — rejected as the primary mechanism: the user is wearing the
  headset when SteamVR starts; reaching for a tray icon mid-transition is exactly the
  friction to remove. (The tray still exposes manual sleep/wake — ADR 0003.)
- **Yield scanout only (panel power off, keep the acquisition)** — rejected: a held
  acquisition blocks SteamVR's acquire outright. The whole acquisition must go.

## Consequences

- The presenter gains an ownership lifecycle: release destroys scanout surfaces and the
  acquisition; re-acquire retries while the other runtime lets go. The D3D12 device
  persists across cycles, so app render resources survive a SteamVR session untouched.
- DM surfaces can return in a different typeless format family per acquisition (S2
  finding); format-bound PSOs rebuild when that happens.
- Released/parked footprint is a commitment, not an accident: zero GPU submissions —
  present loop parked, capture paused with the OS-side duplication dropped. Mirroring a
  SteamVR session's desktop would steal GPU from its compositor.
- IMU + prox HID reads stay open through a SteamVR session (shared-access input reports;
  the prox is what arms the reclaim). No conflicts observed; revisit if SteamVR's
  lighthouse driver ever objects.
- Process polling (2 s owned / 0.5 s released) is the detection mechanism — no SteamVR
  API dependency, which keeps the SteamVR-free goal honest.

## Amendment 2026-06-12 (doze — ADR-0005)

The prox-gated reclaim's **not-worn** branch changes: instead of yielding the acquisition and
waiting released for the next don (`kAwaitDon`), sauna now **reclaims into doze** (ADR-0005) —
re-acquires, brings up video, and comes up *born dozing* (latch set before video, no
emission/fan/LED). The worn branch is unchanged (reclaim straight to active). One rule now
governs every pipeline bring-up/reclaim: **worn → active, not-worn → born dozing.**

- Consequence: sauna **greedily re-holds** the HMD in doze whenever SteamVR is not running,
  rather than leaving it released for another runtime. Accepted for a daily-driver default;
  sauna still re-yields the entire acquisition the instant SteamVR reappears (the `kReleased`
  transition is unchanged), so coexistence is preserved. A non-SteamVR runtime wanting the
  released headset is the edge case traded away.
- The reclaim-into-doze still pays one cold bringup (the reclaim itself re-acquires a
  torn-down pipeline); the *win* is that the subsequent don wakes from doze in ~340 ms instead
  of paying cold reclaim on the don.
- The "released/parked footprint is zero GPU submissions" commitment above still holds for the
  **released** state (SteamVR active). Doze is a distinct, warm state entered only after
  SteamVR exits.
