From 19032f8963f3fad63167067ea5fe0a58714026c5 Mon Sep 17 00:00:00 2001 From: DeeJanuz <45082401+DeeJanuz@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:10:30 -0600 Subject: [PATCH] Pointer: read the overlay list every 20 s, not every second The helper ran `vrcmd --overlays` once a second while the pointer was awake, and in gaze mode the pointer never sleeps. Each run is a shell plus vrcmd, a new SteamVR client, about 26 to 30 ms of CPU, so about 3% of a core all the time. The list is now read every 20 seconds, and at once (at most once a second) when it may have changed: the pointer waking, the dashboard opening or closing or creating an overlay, the scene app changing, an "overlays" request, and a left click that hit nothing, which may be on a panel that came up since. The thread waits on a condition variable instead of waking every 100 ms, so it sleeps while paused. The main loop looks the keys up again as soon as a new list is in, rather than at its next 1 s tick. Overlays already on the list still show and hide within 50 ms, from the IsOverlayVisible poll. Co-Authored-By: Claude Opus 5.5 --- docs/design.md | 2 +- pointer/helper/ft-pointer.cpp | 77 +++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/docs/design.md b/docs/design.md index 4725296..fb29cbf 100644 --- a/docs/design.md +++ b/docs/design.md @@ -118,7 +118,7 @@ The driver starts disconnected, because holding the right-hand role while SteamV Mouse motion turns into yaw and pitch around an anchor, the head position at the last recenter. A ray from the anchor is tested against every visible overlay with `ComputeOverlayIntersection`. On a hit, the cursor sits on that surface; otherwise it floats at `POINTER_DISTANCE`. Since the anchor isn't your current eye position, a second test runs along your line of sight to the cursor point, and anything nearer wins, so the cursor always lands on what you see under it. Overlays in `POINTER_IGNORE` are left out of both tests. A display-only panel, like a performance overlay locked to your view, has no input method, so SteamVR's laser passes through it, but `ComputeOverlayIntersection` still hits it, and the cursor stuck to it. The laser starts just before the cursor point, so an ignored panel nearer to you doesn't catch it either. -OpenVR has no call to list other programs' overlays, so the helper runs `vrcmd --overlays` in the background. It includes hidden overlays, because a floating window's controls only appear while something hovers the window, and the cursor has to find them immediately. +OpenVR has no call to list other programs' overlays, so the helper runs `vrcmd --overlays` in the background. It includes hidden overlays, because a floating window's controls only appear while something hovers the window, and the cursor has to find them immediately. Each run is a shell and a new SteamVR client, about 30 ms of CPU, and it ran every second while the pointer was awake, which in gaze mode is all the time. Now it runs every 20 seconds, and at once when the pointer wakes, when the dashboard opens or closes, when a game starts or ends, and when a left click hits nothing (a panel that came up since). An overlay already on the list showing or hiding needs no new list: the helper checks the visibility of the ones it knows every 50 ms. The laser starts partway along your line of sight to the cursor rather than at your eye. SteamVR sizes its hit dot by distance from the laser's origin, and a laser from the eye still shows a beam in each eye. Starting it close to the target makes the beam and the dot tiny, while `POINTER_ORIGIN_MARGIN` keeps the origin in front of the small window controls, which float a few centimetres in front of their panels. The helper's own white dot is the visible cursor. In empty space it's an interactive overlay that the laser lands on, so SteamVR never draws a laser into nothing. diff --git a/pointer/helper/ft-pointer.cpp b/pointer/helper/ft-pointer.cpp index a06a5fc..4820c2c 100644 --- a/pointer/helper/ft-pointer.cpp +++ b/pointer/helper/ft-pointer.cpp @@ -311,6 +311,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -500,29 +501,62 @@ std::string JsonQuote(const std::string &s) { // public call to enumerate other apps' overlays). Hidden ones are listed too: the // window controls under a floating panel only appear while something hovers the // panel, and the cursor has to find them the moment they do, not a second later. +// Each vrcmd run is a shell and a new SteamVR client, about 30 ms of CPU, so the list is read +// every 20 seconds (kEvery) while the pointer is awake, and at once when something says it may +// have changed (Kick: the pointer waking, the dashboard opening or closing, a click that hit +// nothing), at most once a second. Showing and hiding the overlays it knows doesn't need a new +// list: the main loop polls their visibility (IsOverlayVisible) every 50 ms. // Paused while the pointer is off: each vrcmd run connects to SteamVR as a new app, and a new // app every second kept SteamVR (and the headset's displays) from going to standby. // "overlays" requests (Frametop Input Settings' Ignored panels page) refresh the list even // while paused, and are answered from this thread once it's fresh. class OverlayList { public: + static constexpr auto kEvery = std::chrono::seconds(20), kGap = std::chrono::seconds(1); void Start() { out_ = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); thread_ = std::thread([this] { + std::unique_lock lk(lock_); while (running_) { - if (!paused_ || requested_) Refresh(); - // Wait a second, or less when the pointer wakes (refresh right away then). - for (int i = 0; i < 10 && running_; ++i) { - const bool wasPaused = paused_; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if ((wasPaused && !paused_) || requested_) break; + // Sleeps while paused (until a request or the wake); otherwise until it's time, or + // a kick once kGap has passed since the last read. + const auto due = std::max(last_ + kGap, kicked_ ? last_ : last_ + kEvery); + if (!waiting_.empty()) { + } else if (paused_) { + cv_.wait(lk); + continue; + } else if (std::chrono::steady_clock::now() < due) { + cv_.wait_until(lk, due); + continue; } + kicked_ = false; + lk.unlock(); + Refresh(); + lk.lock(); + last_ = std::chrono::steady_clock::now(); } }); } - void SetPaused(bool paused) { paused_ = paused; } + // Unpausing (the pointer woke) reads the list again at once. + void SetPaused(bool paused) { + std::lock_guard guard(lock_); + if (paused == paused_) return; + if (!paused) kicked_ = true, last_ = {}; + paused_ = paused; + cv_.notify_one(); + } + // The list may have changed: read it again soon (not while paused). + void Kick() { + std::lock_guard guard(lock_); + kicked_ = true; + cv_.notify_one(); + } void Stop() { - running_ = false; + { + std::lock_guard guard(lock_); + running_ = false; + cv_.notify_one(); + } if (thread_.joinable()) thread_.join(); } std::vector Keys() { @@ -531,12 +565,14 @@ public: for (const auto &e : entries_) keys.push_back(e.key); return keys; } + // Goes up by one each time the list is read, so the main loop knows to look the keys up again. + unsigned Generation() const { return generation_; } // Answer `to` with {"t":"overlays","list":[{"key","name","visible"}...]} after the next refresh. void Request(const sockaddr_un &to, socklen_t len) { if (len <= offsetof(sockaddr_un, sun_path)) return; std::lock_guard guard(lock_); if (waiting_.size() < 8) waiting_.push_back({to, len}); - requested_ = true; + cv_.notify_one(); } private: @@ -545,7 +581,6 @@ private: bool visible; }; void Refresh() { - requested_ = false; FILE *p = popen("LD_LIBRARY_PATH=/opt/steamvr/bin/linuxarm64 /opt/steamvr/bin/linuxarm64/vrcmd --overlays 2>/dev/null", "r"); if (!p) return; std::vector entries; @@ -577,6 +612,7 @@ private: entries_ = std::move(entries); waiting.swap(waiting_); } + ++generation_; if (waiting.empty()) return; std::string msg = "{\"t\":\"overlays\",\"list\":["; for (size_t i = 0; i < entries_.size(); ++i) @@ -589,10 +625,11 @@ private: std::thread thread_; int out_ = -1; - std::atomic paused_{false}; - std::atomic running_{true}; - std::atomic requested_{false}; - std::mutex lock_; + std::mutex lock_; // guards everything below + std::condition_variable cv_; + bool paused_ = false, running_ = true, kicked_ = false; + std::chrono::steady_clock::time_point last_{}; // the last read + std::atomic generation_{0}; std::vector entries_; // written only by the thread; the lock guards readers std::vector> waiting_; }; @@ -965,6 +1002,8 @@ int main() { if (debug) std::fflush(stdout); } nudging = confirmLesson = false; + // A click on nothing: maybe on an overlay that came up since the list was read. + if (lastHit.empty()) overlays.Kick(); leftHeld = true; dragDistance = lastDistance; pressKey.clear(); @@ -1089,6 +1128,7 @@ int main() { !aimHeld && !leftHeld && !tilting && hold.src == Src::None && !clickPress && !clickRelease; }; auto lastSlow = std::chrono::steady_clock::now() - std::chrono::seconds(10); + unsigned listGeneration = 0; // the overlay list the handles were looked up from // --- Panel placement (see "Placement" at the top of the file) --- // Device pose, given in the standing universe, sent to the driver in raw space. @@ -1928,10 +1968,11 @@ int main() { } followAt = tnow; - // Slow work, once a second: overlay handles, our device index, laser width. + // Slow work, once a second and when the overlay list is new: overlay handles, our device index. const auto now = std::chrono::steady_clock::now(); - if (now - lastSlow > std::chrono::seconds(1)) { + if (now - lastSlow > std::chrono::seconds(1) || overlays.Generation() != listGeneration) { lastSlow = now; + listGeneration = overlays.Generation(); handles.clear(); for (const auto &key : overlays.Keys()) { if (Ignored(ignore, key)) continue; @@ -2275,6 +2316,10 @@ int main() { vr::VREvent_t ev; while (sys->PollNextEvent(&ev, sizeof ev)) { + // The dashboard's overlays come and go with it, and a game brings its own. + if (ev.eventType == vr::VREvent_DashboardActivated || ev.eventType == vr::VREvent_DashboardDeactivated || + ev.eventType == vr::VREvent_DashboardOverlayCreated || ev.eventType == vr::VREvent_SceneApplicationChanged) + overlays.Kick(); if (ev.eventType == vr::VREvent_Quit) { sys->AcknowledgeQuit_Exiting();