// Sauna user settings — M3 step 5 (settings + persistence). // // One JSON file at %APPDATA%\sauna\config.json holds every user-facing // knob. spatial_light loads it at startup (CLI flags override for the // run), live-applies edits via a file watcher (the gate: change a // setting -> see it in-headset -> restart -> it stuck), and writes it // back on the 's' console key (persisting live-tuned values). // // Missing file = defaults (first run); missing keys = per-key defaults // (forward compatibility). Saves are atomic (tmp + rename) so a // half-written file never gets live-applied. #pragma once #include #include #include namespace sauna { struct Settings { // Virtual screen placement. Distance moves the plane (angular size // changes); scale multiplies physical size at any distance. scale=1 => // primary monitor width = kSceneQuadW (the M2 anchor, 2.1333 m). double screen_distance_m = 2.0; double screen_scale = 1.0; // Curved arrangement (M4 ergonomics): screens sit on a cylinder of // radius screen_distance_m around the head, each rotated so its normal // points at the user, adjacent edges touching in OS left-to-right // order. Every screen is equidistant — a side monitor no longer sits // farther (and smaller) than the primary the way the flat plane put // it. false = M3 flat plane, OS-faithful gaps. Live-applies ('c' key). bool screen_curved = true; // Render. supersample applies at startup only (eye render targets are // allocated once) and only affects the TWO-PASS path — warp-direct (the // default since the M4 step-2 gate) samples the desktop directly and // needs none, so the default dropped 1.5 -> 1.0. predict_ms live-applies. double supersample = 1.0; // Prediction (M4 step 3): predict_auto ON spans the gyro extrapolation // to the measured sample->photon time of each frame (vsync-derived; // ~latch margin + half a scanout). predict_ms is the manual span, used // when auto is off or no vsync reference exists (free-run); both // live-apply. Old configs lack the key and default to auto. bool predict_auto = true; double predict_ms = 20.0; // Neck model (eye midpoint relative to neck pivot, head frame). bool neck_enabled = true; double neck_forward_mm = 80.0; double neck_up_mm = 75.0; // 0 = use the headset flash config default. double ipd_mm = 0.0; // Global recenter binding, e.g. "ctrl+alt+home". See ParseHotkey. std::string recenter_hotkey = "ctrl+alt+home"; // DXGI output indices to skip in capture mode. Startup only. std::vector monitor_exclude; // Cursor overlay in captured screens: -1 auto (= on), 0 never (set on // a machine whose duplicated image already contains a software cursor // — accessibility pointer size/color, some HDR states — where the // overlay would double it), 1 always. Live-applies. int cursor_overlay = -1; // Display policy (M3 step 4). brightness 0–1023, or -1 = hands-off: the // firmware re-applies its own stored startup brightness (config tag // 0x0A, set by the Beyond Utility) every time the displays come on, so // sauna only sends a value when the user explicitly sets one — and then // must re-send it after every panel power cycle (idle wake, don). // Live-applies. idle_timeout_min: no head motion this long -> panels // off, motion/don wakes, worn vetoes (0 disables); live-applies. // refresh_hz 75 (native 5088x2544) or 90 (3840x1920) — startup only. int brightness = -1; double idle_timeout_min = 10.0; double refresh_hz = 75.0; // Wake a parked headset on head motion (legacy behavior). Default OFF: // every supported unit has a prox sensor, and the don is the // deliberate wake signal — motion wake fires from desk bumps and // cable tugs. Forced on internally when no prox hardware is present // (motion is then the only wake). Live-applies. bool wake_on_motion = false; }; // %APPDATA%\sauna\config.json ("" if APPDATA unset — caller treats as // no persistence). std::string SettingsPath(); // Loads SettingsPath() into *out. Missing file: *out = defaults, returns // true. Malformed JSON / unreadable: returns false with *err set and *out // = defaults (callers run on defaults rather than dying). bool LoadSettings(Settings* out, std::string* err); // Atomic save (tmp + MoveFileEx replace). Creates %APPDATA%\sauna. bool SaveSettings(const Settings& s, std::string* err); // Last-write time of the settings file (FILETIME ticks; 0 = missing). // The live-apply watcher polls this. uint64_t SettingsFileTime(); // Per-serial calibration store (M4 step 1 — instant-start gyro bias): // %APPDATA%\sauna\calib\.json ("" if APPDATA unset or serial // empty). Seeded into the AHRS at startup so tracking starts from the // first sample; background still-window refreshes keep the file current. std::string GyroBiasPath(const std::string& serial); // False if no stored bias (first run), unreadable, or malformed. bool LoadGyroBias(const std::string& serial, double biasLsb[3]); // Atomic save (tmp + rename), creates the calib dir. False with *err set // on failure. bool SaveGyroBias(const std::string& serial, const double biasLsb[3], std::string* err); // "ctrl+alt+home" -> RegisterHotKey (mods, vk). Tokens separated by '+': // modifiers ctrl/alt/shift/win, then one key: a-z 0-9, f1-f24, home, end, // insert, delete, pageup, pagedown, space, pause. False on parse failure // (caller keeps the previous binding). Caller adds MOD_NOREPEAT. bool ParseHotkey(const std::string& spec, uint32_t* mods, uint32_t* vk); } // namespace sauna