vrOS v1.2.3 Patch Notes
Highlights
Hotfix for flaky space-drag on Oculus and Knuckles, plus the recurring "UI feels laggy" regression. The double-tap Y/B (Oculus) / B (Knuckles) bindings were pulsing drag for ~12ms on press instead of latching while held, so the playspace shifted on every double-tap. The UI hitch came from re-serializing the same broadcast payload once per popout recipient and a 250ms idle bridge poll that left VR keystrokes sitting in the JS outbox.
Bug Fixes
- Fixed flaky space-drag on Oculus Y/B and Knuckles B. SteamVR's button-mode state machine was gating the click signal while waiting on the double-tap window when click and double shared one binding source, so drag fired as a one-shot pulse on press instead of a held boolean. Split into separate click-only and double-only source entries per hand; drag now latches true while the button is held.
- Suppressed drag while rotate is active on the same frame. SteamVR 2.0 fires both click and double independently on a double-tap, which let drag and rotate engage in the same frame on a fresh double-tap of the playspace controls. Drag is now consumer-side suppressed when rotate is active on either hand.
- Killed the recurring UI-thread hitch (7th re-occurrence since v0.3.4). State-broadcast fan-out was re-serializing the same JSON payload once per recipient — with dashboard + 3-5 popouts that's 4-6 serializations per state change, and the audio-level push at ~10 Hz per WASAPI source compounded the cost. Broadcast paths now serialize the payload once and reuse the prebuilt string for every recipient. Audio level fan-out is globally gated at 10 Hz instead of 10 Hz × M sources, and skips entirely when every recipient is suspended. Unconditional repaint after broadcasts removed — JS subscribers trigger their own repaints.
- Fixed VR-keyboard typing lag on window-capture overlays. The idle bridge poll interval was 250ms, so slow typing (≥500ms between keys) left each keystroke sitting in the JS outbox until the next poll, producing the alternating fast/slow per-key latency. Dropped to 50ms and the keyboard now forces a poll on every iteration that has pending actions, eliminating the throttle race entirely.
- Fixed "char doesn't render until next keystroke" in Windows Terminal. inject_text was batching the whole UTF-16 string into one SendInput call, which hit Windows' input merge window and the terminal renderer didn't wake until a follow-up event arrived. Per-char dispatch now separates the merge windows so each character independently wakes the target's renderer.
- Populated scan codes on injected keys. Some targets (terminals, native input handlers) were ignoring or buffering VK-only events with no scan code. inject_key now populates wScan via MapVirtualKeyW and sets KEYEVENTF_EXTENDEDKEY for the high-byte VKs (right Ctrl/Alt, arrow keys, Home/End, Numpad Enter).
- Cached focus-target lookups for capture-keyboard input. Every keyboard drain on a captured window was running AttachThreadInput → SetForegroundWindow → EnumChildWindows → SetFocus → DetachThreadInput, where the EnumChildWindows walk over Electron HWND trees (Discord, VS Code, Slack, Claude Desktop) is 10-500ms wall-clock per call. UI-thread now caches the target and skips re-focusing when the same target is still foreground within 750ms; vros-graphics caches the Chromium renderer descendant per top-level HWND.
Improvements
- Class-aware SendInput dispatch. The per-char fix above is necessary for terminals (cmd.exe, Windows Terminal, PowerShell) but adds ~10µs × N overhead on Chromium-backed apps that don't need it. Targets are now classified by window class and Chromium apps get batched dispatch while terminals get per-char.
- Robust target classifier with per-target INFO log so the dispatch path is visible in logs without needing trace.
- Added per-substage UI-thread telemetry. The "slow UI loop iteration" warn now names the culprit substage (host_update_ms, state_push_ms, js_eval_ms, popout_lifecycle_ms, capture_focus_ms, capture_inject_ms) instead of needing a forensic dig. Debug builds also restart the supervisor after 5 consecutive 200ms+ iterations with the full per-substage breakdown in the failure message.