# M5 — Doze: instant wake from sleep

**Goal:** resume-from-sleep in well under 1 s (target ~340 ms), by keeping the video pipeline
warm during sleep and darkening via firmware instead of tearing the pipeline down.

Design rationale and the firmware contract live in **ADR-0005**; this is the implementation
breakdown. Domain terms (**doze**, **parked**, **asleep**, **emission gating**, **DISPLAYS_ON**,
**photon latency**) are in CONTEXT.md. Measurement method is docs/photon-latency-bench.md.

Coordinated firmware (`beyond_synaptics`, MCU) + host (`sauna`) milestone. Cold-boot speed-up is
a **separate follow-on milestone** (deferred). VXR7200 firmware is **out of scope**.

## The one-line model

Today every sleep is **parked**: host `POWER_OFF` → video signal dies → VXR de-locks →
`video_enabled` false → full 3–5 s cold bringup on wake. Doze keeps `video_enabled` true (host
presents black, VXR stays locked) and darkens with a firmware brightness sweep + true display-off.
Wake = re-enable display (~40 ms) + brightness sweep (~300 ms) ≈ **340 ms**.

## Firmware changes (beyond_synaptics, ATSAMG55 MCU)

1. **`display-sleep` / `display-wake` HID command pair** (control interface 35BD:0101, single
   byte each; pick unused codes in `usbhid_interface.h`). `display-sleep` runs the sleep
   choreography; `display-wake` reverses it. Reply `$`/`E` like the other control commands.
2. **Doze-latch** — a firmware bool, distinct from `displays_on`/`video_enabled`.
   - While set: overrides the prox gate — `video_displays_on_prox()` is suppressed, emission
     stays at floor/off regardless of `prox_hmd_on_person()`.
   - Honored at video-bringup (`video_proc.c` ~553-676): if the latch is set when video is
     detected, **skip** the OLED-on / fan-on / LED-on enables — come up straight into doze-dark
     (the "born dozing" path; also kills the "spurious screens on" flash the existing comment
     at ~574 worries about).
   - **Cleared on video loss** — add to the `video_enabled → false` teardown (~533-551). This is
     the force-close/crash/SteamVR-release safety: the latch never survives a pipeline drop.
3. **Brightness sweep** — target-seeking ramp modeled on `task_fan` (`++/--` toward a target per
   tick). `display-sleep` target = floor; `display-wake` target = `oled_current_brightness`.
   - Floor = **register 0** — lowest possible OLED brightness register value (the same value
     `video_displays_off_prox` already writes). Raw 0–1023 register scale, not the HMDUtility UI
     scale (whose min is -20).
   - ~300 ms sweep → tick/step sized so floor↔active spans ~300 ms.
   - `OLED_display_on_off(false)` gated on *reaching* the floor; wake's `OLED_display_on_off(true)`
     gated on it having been turned off. Mid-sweep reversal (interrupting don) chases the new
     target from the current value — free with target-seeking.
4. **Decouple fan + LED from `video_enabled`.** `display-sleep` drives fan → `FS_Idle`
   (`FAN_TASK_CODE_VIDEO_STATE` false — keeps the overtemp net live) and LED → breathing, while
   `video_enabled` stays true. `display-wake` restores `FS_Video` + static LED. Today these are
   hard-tied to `video_enabled` (fan VIDEO_ON/OFF at ~442/540/590, LED likewise).
5. **Fan kickstart floor** (wake-UX, also helps cold boot later). When ramping up **from a stop**
   (`current==0`, `next>0`), seed `current_fan_speed` to a fixed min-stable-PWM **just under
   HMDUtility's `fan_speed_min = 40`** (≈35, with the gen-2 ÷2 halving the firmware already
   applies) before the ramp loop — skip the ~100–200 ms low-RPM dwell that makes the abrupt
   startup noise. Config-gate via a `SigTag` so it is tunable/reversible. **Check the
   `fan-sticktion` remote branch first** — likely prior art. By-ear A/B (not bench-instrumentable).
6. **Version bump** so the host capability check (`HID_CODE_FOR_SW_VER`) can detect doze support.

## Host changes (sauna)

1. **Doze state** alongside `parked` in `spatial_light.cpp`. Wake must know which depth it leaves
   (`display-wake` ~340 ms vs full re-acquire 3–5 s).
2. **Capability check at startup** — read firmware version; use doze only if ≥ doze-capable,
   else fall back to parked. Doze is a *detected capability*, never assumed.
3. **Universal bring-up/reclaim rule** — on any pipeline bring-up/reclaim, check prox:
   **worn → active** (capturing, streaming); **not-worn → born dozing** (send `display-sleep`
   over HID to set the latch **before** starting video, then bring up DirectMode/scanout).
   Wire into: launch-not-worn (replaces today's parked path ~1099-1106) and SteamVR-exit reclaim
   (the not-worn branch ~703-707, replacing `kAwaitDon`-released with reclaim-into-doze — ADR-0002).
4. **All sleep entries → doze**: idle timeout, tray "Sleep now", launch-not-worn. Replace the
   `requestPanelPower(false)` parked path with a doze entry (`display-sleep` + keep presenting
   black). Parked stays for escalation/SteamVR/fallback.
5. **Present-loop liteness during doze** — strip to a bare black present (skip capture, warp,
   pose prediction, late-latch). Keep `video_enabled` alive.
6. **Doze→park escalation** — optional, **default off**, 30 min in-doze timeout → `display-wake`
   not needed; instead transition doze→parked (sweep handled, then `POWER_OFF`). Configurable knob.
7. **Wake path** — don out of doze sends `display-wake` through the existing ADR-0003 grace/
   override logic. Inherits the prox-dead motion failsafe and sleep-while-worn grace.

## Spikes / unknowns

- **No-present-free-run** (host): does NVAPI DirectMode hold the last surface with an idle present
  queue (link alive), or TDR? Holds → present black once, idle to a heartbeat, doze host cost ≈ 0.
  Doesn't → present black at mode rate (still cheap). Gate the liteness design on this.
- **BS1 vs BS2/BS2E sweep+floor parity** — validate the floor (register 7) and display-off look
  identical on both panel generations. Design-wise doze never cuts panel power, so no BS1-only
  rail difference applies; this is a bench check, not a blocker.

## Validation (camera bench, docs/photon-latency-bench.md)

1. **First: measure today's warm parked→wake baseline.** Add one panel-power toggle to
   `panel_bench` (it currently measures cold init only). Establishes the number doze must beat.
2. **Doze-wake latency** — `display-wake` → `DISPLAYS_ON` + camera; confirm ~340 ms, < 1 s.
3. **Doze darkness** — confirm true black (optic-cavity luminance floor) with fan off, LED breathing.
4. **Force-close-during-doze** — kill sauna, confirm latch clears (HMD goes dark/asleep, not stuck),
   then SteamVR/sauna restart lights normally.
5. **Born-dozing** — launch not-worn and SteamVR-exit not-worn both come up dark (no emission/fan/
   LED flash), don wakes in ~340 ms.
6. **Fan kickstart** — by-ear A/B of wake fan spin-up, both generations.

## Sequencing

Firmware and host land behind the capability gate, so order is flexible (old-host+new-fw and
new-host+old-fw both degrade safely). Suggested: firmware command + latch + sweep + fan/LED
decouple → host capability check + doze state → born-dozing wiring → escalation → fan kickstart
experiment. Measure the warm baseline before starting; measure doze-wake after the host wiring.
