Pointer: sleep on the command socket while the pointer is off

The main loop slept a fixed 8 ms, about 116 wakeups a second, whether the pointer was awake
or not, and every second it looked up every overlay's handle and read a string property from
all 64 device slots to find its own device.

With the pointer off and hand gestures off, the loop now waits in poll() on its command
socket for up to 250 ms, or 20 ms while mapped Frame controller buttons are being read
(SteamVR input has no event to wait for). A mouse command ends the wait at once. The
headset's activity level, the game check, and the "vrgame" and "gazeawake" repeats keep
going at that pace. The 50 ms visibility poll and the 1 s handle lookups run only while the
pointer is awake, and waking forces both. The device index is looked for only while it's
unknown, and again after SteamVR activates or deactivates a device. The HMD pose history is
kept only with hand gestures on, its one user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-03 09:12:33 -06:00
1 parent 19032f8963
commit 0680297efa
3 files changed
+41 -11

No files matched your search

+1 -1
View File
@@ -141,7 +141,7 @@ Replacing a loaded driver's files, as re-running the installer used to do, leave
The dashboard follows whichever device summoned it or last pressed its trigger. Frametop adds "last used wins": moving a real controller releases the pointer, and the next mouse movement takes the laser back. Moving means faster than 0.35 m/s or 2 rad/s (both times `POINTER_CONTROLLER_PICKUP`, 1 by default) for 100 ms in a row, while the controller is tracked normally. A single sample over the limit used to be enough, and controllers resting on a desk took the laser back on a knock or a tracking jump while the mouse was in use. Small movements don't count; waking needs `POINTER_WAKE_COUNTS` of mouse motion within a second, so desk jitter doesn't steal the laser. While the pointer is awake, a tiny transparent overlay with `MakeOverlaysInteractiveIfVisible` keeps SteamVR's laser mouse on, since otherwise the first click would only switch the laser on.
When the headset comes off, SteamVR reports its activity level as idle at once and turns the displays off 5 seconds later (`power.turnOffScreensTimeout`), unless something keeps it awake. An awake pointer did, and so did the helper's `vrcmd` runs: each is a new SteamVR client, and a new client every second kept SteamVR out of standby. The helper now releases the pointer as soon as the headset is idle, ignores the mouse until you're wearing it again, and pauses the overlay list whenever the pointer is off.
When the headset comes off, SteamVR reports its activity level as idle at once and turns the displays off 5 seconds later (`power.turnOffScreensTimeout`), unless something keeps it awake. An awake pointer did, and so did the helper's `vrcmd` runs: each is a new SteamVR client, and a new client every second kept SteamVR out of standby. The helper now releases the pointer as soon as the headset is idle, ignores the mouse until you're wearing it again, and pauses the overlay list whenever the pointer is off. With the pointer off it also stops running its loop every 8 ms, about 116 wakeups a second for nothing: it waits up to 250 ms for a command on its socket (20 ms while it reads mapped controller buttons, which SteamVR input only offers by polling), and leaves the overlay lookups until the pointer wakes.
### Moving floating windows
+32 -10
View File
@@ -36,6 +36,13 @@
// pointer is off the helper also stops listing overlays with vrcmd, whose connection every
// second kept SteamVR from going to standby.
//
// Idle: while the pointer is off (and hand gestures are off), the main loop waits for a command
// on its socket for up to 250 ms instead of running every 8 ms, or 20 ms while it reads mapped
// Frame controller buttons (vrbuttons.h), which have no event to wait for. What has to go on
// meanwhile still does: the headset's activity level, the game check and the relay's "vrgame"
// and "gazeawake" repeats, and a mouse command wakes it at once. The overlay lookups (the 50 ms
// visibility poll, the once-a-second handles) wait for the pointer to wake, and run right then.
//
// Last used wins: when a real controller moves (picked up), the pointer is released
// (driver "hide", which also drops its hand role hint), so the controller gets
// its role and laser back. The next mouse input reconnects and claims the laser again.
@@ -329,6 +336,7 @@ extern "C" {
#include <fcntl.h>
#include <fnmatch.h>
#include <poll.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
@@ -859,6 +867,8 @@ int main() {
std::map<std::string, bool> sceneGraph; // no texture: plane test instead of ComputeOverlayIntersection
std::map<std::string, bool> visible; // refreshed every 50 ms
auto lastVisible = std::chrono::steady_clock::now();
auto lastSlow = std::chrono::steady_clock::now() - std::chrono::seconds(10);
unsigned listGeneration = 0; // the overlay list the handles were looked up from
// The plane of the last panel the cursor was on, and the last point on it (panel edges).
Vec3 edgePoint, edgeNormal, edgeLast;
std::string edgeKey;
@@ -901,6 +911,8 @@ int main() {
wokeAt = t;
active = true;
recenter = true;
// Overlay handles and visibility weren't looked at while it was off (see "Idle" at the top).
lastSlow = lastVisible = t - std::chrono::seconds(10);
SendTo(out, "ft_pointer", "role " + role); // POINTER_ROLE, before it takes it
SendTo(out, "ft_pointer", "show");
claimPending = true; // take the laser without clicking, once SteamVR has bound the device
@@ -1127,8 +1139,6 @@ int main() {
return gazeOn && gazeMouseHeld && !inGame && !headsetOff && Clock::now() - gz.at < std::chrono::seconds(1) &&
!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.
@@ -1669,7 +1679,7 @@ int main() {
}
// Hands (see the top): pinches and grips from ft-hands.
{
if (handsOn) {
vr::TrackedDevicePose_t h0;
sys->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, &h0, 1);
if (h0.bPoseIsValid) poses.Add(tnow, h0.mDeviceToAbsoluteTracking);
@@ -1968,9 +1978,10 @@ int main() {
}
followAt = tnow;
// Slow work, once a second and when the overlay list is new: overlay handles, our device index.
// Slow work, once a second and when the overlay list is new: overlay handles, our device
// index. Only while the pointer is awake (see "Idle" at the top).
const auto now = std::chrono::steady_clock::now();
if (now - lastSlow > std::chrono::seconds(1) || overlays.Generation() != listGeneration) {
if (active && (now - lastSlow > std::chrono::seconds(1) || overlays.Generation() != listGeneration)) {
lastSlow = now;
listGeneration = overlays.Generation();
handles.clear();
@@ -1992,16 +2003,16 @@ int main() {
sceneGraph[key] = (tw == 0 || th == 0) && tt == vr::VROverlayTransform_Absolute &&
key.rfind("frametop.", 0) != 0;
}
ours = vr::k_unTrackedDeviceIndexInvalid;
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
// Our device keeps its index; it's looked for again when a device is activated or
// deactivated (the events below).
for (vr::TrackedDeviceIndex_t i = 0; ours == vr::k_unTrackedDeviceIndexInvalid && i < vr::k_unMaxTrackedDeviceCount; ++i) {
char type[64] = "";
sys->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
if (std::strcmp(type, "ft_pointer") == 0) ours = i;
}
}
if (now - lastVisible > std::chrono::milliseconds(50) || visible.size() != handles.size()) {
if (active && (now - lastVisible > std::chrono::milliseconds(50) || visible.size() != handles.size())) {
lastVisible = now;
visible.clear();
for (const auto &[key, h] : handles) visible[key] = overlay->IsOverlayVisible(h);
@@ -2320,6 +2331,8 @@ int main() {
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_TrackedDeviceActivated || ev.eventType == vr::VREvent_TrackedDeviceDeactivated)
ours = vr::k_unTrackedDeviceIndexInvalid;
if (ev.eventType == vr::VREvent_Quit) {
sys->AcknowledgeQuit_Exiting();
@@ -2328,6 +2341,15 @@ int main() {
return 0;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(8));
// Idle (see the top): wait for a command instead. Not with anything still to finish (the
// claim pulse, a click's release, a press).
const bool idle = !active && !handsOn && !claimPending && !claimHeld && !clickRelease && !leftHeld &&
hold.src == Src::None;
if (idle) {
pollfd p{in, POLLIN, 0};
poll(&p, 1, controllerButtons.Watching(inGame) ? 20 : 250);
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(8));
}
}
}
+8
View File
@@ -111,6 +111,14 @@ class ControllerButtons {
}
}
// Whether Poll reads any button now (the main loop can't sleep long while it does).
bool Watching(bool inGame) const {
if (!ok_ || (inGame && !games_ && !capture_)) return false;
for (int i = 0; i < kCount; ++i)
if (bound_[i]) return true;
return false;
}
// {"manifest":true,"global":false,"bound":[...],"active":[...]}: active = bound and
// delivered (a controller that has the button is on, and SteamVR lets us have it).
std::string Status() const {