Settings never polled OpenVR events. When vrmonitor crashed at 21:12:01,
vrserver sent VREvent_Quit to all clients; Steam and the web helpers
disconnected, Settings stayed connected and vrserver SIGKILLed it at
21:12:06 ("didn't quit in time"), holding up SteamVR shutdown 5 s.
The preview thread now drains the event queue every 250 ms (also while
the preview is hidden). On VREvent_Quit it shuts the OpenVR client down
and stays disconnected from that vrserver; the existing PID-change guard
reconnects to the next one. The window keeps running.
Compare layout changes against the size the swapchain was requested
for, not the extent the surface caps may clamp it to, so a clamped
surface cannot recreate the swapchain every frame. Add a focused test
for the /proc process lookup that replaced pgrep.
Everything below lost its last caller when the GPU preview replaced the
CPU path; each was grepped for references, then the app was rebuilt
(--locked, no Rust warnings) and cargo test passed.
- native/overlay_preview.{cpp,hpp,comp}, bridge rifts_preview() and
the Rust `preview` command: the CPU readback, compute downscale,
JPEG q80 encode and Tauri IPC frame path.
- jpeg-encoder dependency: only used by that command.
- build.rs compute-shader step and the overlay_preview sources.
- PID_CACHE/server_pid_fast(): a 1 s TTL cache that hid the ~9 ms
pgrep spawn from the old per-frame path; process lookup now reads
/proc and the preview no longer resolves the PID per frame.
- style.css body.mirror/#mirror-only (standalone mirror window, no
markup sets them) and .seg (segmented control, no markup uses it).
- main.tsx Snapshot.proximity/idle: never read by the frontend. The
backend still emits them for --snapshot evidence and the restart guard.
The headset view is drawn natively instead of streamed through the
webview. A preview thread re-acquires SteamVR's system.HeadsetView
overlay once per compositor vsync with the same VROverlayView_t (as
the OpenVR header advises), samples it on SteamVR's GPU and presents
into a Vulkan swapchain on a Wayland subsurface placed over the
preview box. No readback, encode, IPC or decode.
- Re-acquire without release: ~25 us, no vrclient log line per frame
(acquire+release cost ~1 ms and logged one line each time).
- Paced from IVRSystem::GetTimeSinceLastVsync, one present per
compositor frame, MAILBOX/IMMEDIATE so the thread never blocks.
- In HMD Standby with no new compositor frame, refresh once a second:
any display-rate GPU submit costs vrcompositor CPU on NVIDIA.
- The subsurface has an empty input region; double-click, F11, Esc and
the Fullscreen button keep working, aspect kept with black bars.
- Snapshot holds the OpenVR lock only for OpenVR calls, so its process
spawns cannot stall the preview; process lookup reads /proc instead
of spawning pgrep.
Poll the headset preview at 30 fps instead of 5 Hz, encode frames
as JPEG to keep the Tauri IPC round-trip small, cache the vrserver
PID and the overlay handle on the hot path, and retain a ready
Vulkan device across transient frame errors. Add a fullscreen
preview (button, double-click, F11, Esc) with aspect-preserving
fill, plus a small fps/size/latency readout.
Fake vrcompositor/vrserver/vrmonitor daemons let DisplayRecovery pin,
lose and re-pin generations. A respawned compositor must not trigger a
session restart; a dead one with no replacement still must.
Activate can run again on the same driver object without an intervening
Deactivate. Spawning a second pose thread orphaned the first (leaked,
never joined) and left two threads racing on m_pose via
TrackedDevicePoseUpdated. Keep the running thread instead.
The helper SIGTERMed only the monitor and then waited 10s for a server
and compositor it never asked to exit. Signal all three pinned
generations up front; give up only if the server itself is already gone.
DisplayRecovery froze its compositor handle while the loss latch was set,
so a compositor SteamVR had already respawned still looked dead and the
guard SIGTERMed a self-healed session. Re-pin whenever the tracked
generation exits and a unique replacement exists; keep the dead handle
only while nothing replaced it.
A SLAM restart between the last recorded keyframe and Save clears
world_valid; confirming then publishes saved bounds in a
session-derived frame until the 1 Hz verifier revokes them. Stay
unconfirmed and relocalize from fresh votes instead.
Hold world confirmations across invalid SLAM samples instead of
resetting on every miss; a candidate drops only when stale (>3 s) or
from an older SLAM session. The 3-vote bar, 4 cm / 3 deg agreement and
the chained 3 s window are unchanged, so transient wrong matches still
cannot accumulate votes.
Wait up to 250 ms for the exposure's SLAM sample and accept the same
250 ms gap the display pose path tolerates, so a loaded backend does
not make recognition samples unusable while published poses stay valid.
Skip feature matching while proximity reports off-head standby, and
verify at 0.2 Hz once the room is localized; match every queued pair
while searching or recording. Attempts and diagnostics freeze visibly
while skipping.
Count the first failing usability gate per unusable attempt
(paused/sample/gap/flags/generation/guard) in diagnostics, the
per-second world log, the pending-room warning and the status file, so
the next worn session shows which condition dominates.
Triangulate at most one landmark per second-view pixel, with separate
live and reference-image sets so temporal pairs keep their own domain.
Replay: same-room recognition still confirms in ~0.5 s with tighter
votes on fewer map points; sparse valid samples confirm where the old
window reset could not.
Refcount frameserver storage so destroy defers munmap/free/close
until outstanding ROI frames return; always join the stream thread
via a joinable flag instead of the running flag; wake blocked DQBUF
with poll timeout plus STREAMOFF; make running/used state atomic.
Add per-command monotonic deadline (1500 ms, beyond the device 1000 ms
timeout) so a missing response fails the head instead of wedging the
queue. Bound send and poll transport errors (8 each) with 5 ms doubling
to 100 ms backoff so a persistent STALL cannot spin HID ioctls.
Retries resend only the unsent head with identical bytes; once sent the
head is polled, never resent. The response carries no command echo
(seqnum/busy only), so a late reply after a timeout can be misattributed
to the next command; last seq is retained to ignore duplicates and
callers must validate payload status/length.
Fail callbacks once via a shared helper; clear stays safe after timeouts.
HID -1/errno semantics unchanged; valid busy polls every tick.
Add fake-HID/fake-time tests for success pipelining, busy timeout
advance, send/poll/short backoff bounds, clear-once safety, stale seq,
and null HID expiry.
USB thread calls rift_s_tracker_device_lost before camera frameserver stop.
Retains tracker, passthrough pointer, and SLAM/constellation xfctx; marks
ready_for_data=false, pauses via existing restart path with forced pending
restart, resets hw clock/IMU/frame epochs and fusion history for counter
reset, bumps slam_generation and invalidates world candidate so stale worker
results cannot publish, preserves saved map/anchor, joins world worker.
tracker_start restarts worker after reconnect. Controller/SLAM/IMU paths
already drop while !ready; no xfctx teardown here.