Side button now works as Back.

This commit is contained in:
Juan Arteaga Carmona committed 2026-10-04 13:18:26 +02:00
1 parent da48c37235
commit 12416e005d
6 files changed
+39 -12

No files matched your search

+4 -6
View File
@@ -33,6 +33,7 @@ The compositor then treats it like any other laser-pointing hand.
| Forward side button (BTN_EXTRA) | **Turns laser mode on** | **Turns laser mode off** |
| Movement | Normal desktop cursor | Aims the laser (world-locked: turning your head doesn't move it) |
| Left / right / middle button | Normal | Laser click / right click / middle click |
| Back side button (BTN_SIDE) | Normal | SteamVR's laser **Back**, the same action as the right Frame controller's B button. Works in Steam UI menus; it does not reach the KDE desktop (see Known limitations). |
| Wheel (and tilt wheel) | Normal | Acts like the controller **thumbstick**. One notch moves one item in Steam UI lists, and spinning the wheel scrolls continuously. It also scrolls the overlay under the laser. |
When laser mode turns on, the ray starts where you're looking. When it's off, the virtual device reports itself as disconnected, so your real controllers keep the laser.
@@ -55,7 +56,7 @@ cmake -S . -B build -G Ninja && cmake --build build
Check that it loaded:
```sh
grep -a 'mouselaser:' ~/.local/share/Steam/logs/vrserver.txt | tail
# expect: "version 0.3.1-experimental", "activated as device N, role 1", "using /dev/input/eventX (<your mouse>)"
# expect: "version 0.4.0-experimental", "activated as device N, role 1", "using /dev/input/eventX (<your mouse>)"
```
### Optional: offline wheel test
@@ -77,6 +78,7 @@ Add any of these to `~/.config/openvr/config/steamvr.vrsettings` under a `"drive
| `enable` | `true` | `false` loads the driver but adds no device. This is the kill switch. |
| `sensitivity` | `0.05` | Degrees of ray rotation per mouse count. |
| `toggleButton` | `276` | evdev key code of the toggle: 276 = BTN_EXTRA (forward), 275 = BTN_SIDE (back). |
| `backButton` | `275` | evdev key code that sends the laser Back action while laser mode is on (275 = BTN_SIDE). `-1` disables it. |
| `role` | `1` | 1 = left hand, 2 = right hand. |
| `deviceNameFilter` | `""` | Substring of the evdev mouse name. Empty means the first device with REL_X/REL_Y and BTN_LEFT. |
| `originOffsetY` | `-0.08` | Ray origin height relative to the HMD, in metres. |
@@ -96,11 +98,6 @@ Add any of these to `~/.config/openvr/config/steamvr.vrsettings` under a `"drive
- Scrolling stops abruptly: raise `wheelSmoothDecayMs`.
- Fast spins aren't fast enough: raise `wheelSmoothImpulse`.
## It shows up in SteamVR's "Manage Add-ons"
This is expected. SteamVR lists every driver that isn't resources-only under **Settings → Developer → Manage Add-ons** (exact menu names may differ). That entry is useful:
- **Its toggle is a kill switch.** It writes `"driver_mouselaser": {"enable": false}` to `steamvr.vrsettings`, which is the same `enable` setting the driver honours. Takes effect after a reboot.
- **Safe mode.** SteamVR can block add-ons in "safe mode", apparently when it crashes after they were added. The **Unblock** button on that page re-enables them, once you've fixed or updated the driver.
## Recovery
If SteamVR won't come up properly after installing, there are four options:
0. In VR, open **Manage Add-ons**, switch *mouselaser* off, then reboot.
@@ -123,6 +120,7 @@ If SteamVR won't come up properly after installing, there are four options:
- If a **real controller holds the same hand role** its own laser pointer seems to stop working.
- **Wheel feel is approximate.** A wheel isn't a stick: it only sends notches. Smooth mode only *simulates* a held stick, so it still won't feel exactly like a real thumbstick. Tune it with the `wheel*` settings.
- Settings are only read when SteamVR starts.
- **Back doesn't reach the KDE desktop.** SteamVR's laser Back goes to SteamVR overlays. Steam's UI handles it, but gamescope doesn't pass it on to the windows it hosts (a real controller's B behaves the same). With laser mode off, the side buttons don't work in KDE either, because the nested KWin's X11 backend drops X buttons 8 and up. See [docs/steam-frame-background.md](docs/steam-frame-background.md).
- Two harmless log lines:
- `steam.client (mouselaser) has no configured binding`: only the compositor bindings are provided, and Steam's own binding for the Frame controllers is haptics-only anyway.
- `Driver mouselaser has no suitable devices`: logged at load time, presumably because the driver provides no HMD. The device is added right after.
+3 -1
View File
@@ -56,7 +56,9 @@ The broader notes from that session (hardware, display pipeline, KDE scale, ultr
13. **Demo media** (2026-10-01). The owner recorded `~/Videos/mousedemo.mp4` (44.2 s, 1920×1080, 20 fps). It was trimmed (first 3 s and last 2 s removed, leaving 39.2 s) and converted to `docs/media/demo.webp`, which the README shows as the hero image.
14. **Back button, 0.4.0** (2026-10-04). The mouse's back side button (BTN_SIDE, setting `backButton`) is now bound to the compositor's `/actions/lasermouse/in/back`. That is the same action as the right Frame controller's B; right A is `home`. The owner reports Back works in the Steam UI and not on the KDE overlay. Investigating KDE separately (see `steam-frame-background.md`, input path) showed two causes: gamescope doesn't forward SteamVR Back to its windows, and KWin's X11 nested backend drops X buttons 8 and up even with laser mode off. The driver can't fix either one, and nothing was added for it.
## State at hand-off
- The driver is registered from this repo (`<repo>/driver/mouselaser`). The old prototype registration has been removed.
- Nothing is vendored. The build depends on SteamVR's bundled header.
- Licensed MIT (see `LICENSE`). SPDX tags are on the sources.
- Licensed MIT (see `LICENSE`). SPDX tags are on the sources.
+2
View File
@@ -23,6 +23,8 @@ DSI-1 panel (2× 2160x2160 @ 108/120/144 Hz)
```
- KWin on the X11 windowed backend never uses libinput, so KDE's mouse and keyboard settings pages are empty. `/org/kde/KWin/InputDevice` doesn't exist.
- Gamescope delivers the mouse only to its **focused** overlay, clamped to that surface. **This is the "bounded" mouse the project is named after.**
- **Back/forward side buttons never reach KDE apps.** Gamescope delivers them fine (X button 8/9 on `:0`, seen with `xinput test-xi2`). KWin 6.2.5's X11 windowed backend (`X11WindowedBackend::handleButtonPress`) then discards every X button above 7: its `default:` branch `return`s. Upstream KWin removed that backend in 2025 without fixing it. KWin's Wayland nested backend passes button codes through unchanged.
- SteamVR's laser **Back** action (a controller's B, or this driver's back button) only reaches SteamVR overlays such as Steam's UI. Gamescope doesn't translate it for the windows it hosts.
## The laser pointer
- The laser belongs to **vrcompositor**. It casts a ray from a *pose* and sends the overlay it hits `VREvent_MouseMove/ButtonDown/ButtonUp/...`. Gamescope turns those into pointer input for its clients.
@@ -12,6 +12,7 @@
"/input/b" : { "type" : "button", "click" : true, "binding_image_point" : [ 50, 50 ], "order" : 3 },
"/input/grip" : { "type" : "button", "click" : true, "binding_image_point" : [ 50, 50 ], "order" : 4 },
"/input/thumbstick" : { "type" : "joystick", "touch" : true, "binding_image_point" : [ 50, 50 ], "order" : 5 },
"/input/back" : { "type" : "button", "click" : true, "binding_image_point" : [ 50, 50 ], "order" : 6 },
"/pose/raw" : { "type" : "pose", "binding_image_point" : [ 50, 50 ] },
"/pose/tip" : { "type" : "pose", "binding_image_point" : [ 50, 50 ] }
},
@@ -4,7 +4,7 @@
"app_key": "openvr.component.vrcompositor",
"category": "steamvr_input",
"controller_type": "mouselaser",
"description": "Mouse aims the SteamVR laser. Buttons click, and the wheel acts as the thumbstick (lists and scrolling).",
"description": "Mouse aims the SteamVR laser. Buttons click, the back side button goes back, and the wheel acts as the thumbstick (lists and scrolling).",
"name": "Mouse Laser: dashboard and overlays",
"options": {},
"simulated_actions": [],
@@ -74,6 +74,24 @@
"output": "/actions/lasermouse/in/middleclick"
}
}
},
{
"path": "/user/hand/left/input/back",
"mode": "button",
"inputs": {
"click": {
"output": "/actions/lasermouse/in/back"
}
}
},
{
"path": "/user/hand/right/input/back",
"mode": "button",
"inputs": {
"click": {
"output": "/actions/lasermouse/in/back"
}
}
}
]
},
+10 -4
View File
@@ -31,7 +31,7 @@
using namespace vr;
static const char *k_section = "driver_mouselaser";
static const char *k_version = "0.3.1-experimental";
static const char *k_version = "0.4.0-experimental";
static void Log(const char *fmt, ...)
{
@@ -54,6 +54,7 @@ struct Settings
{
float sensitivityDeg = 0.05f; // degrees of ray rotation per mouse count
int toggleButton = BTN_EXTRA;
int backButton = BTN_SIDE; // sends the compositor's laser "back"; -1 = none
int role = TrackedControllerRole_LeftHand;
std::string nameFilter; // substring of the evdev name; empty = first mouse found
float originOffsetY = -0.08f; // metres, world space, relative to the HMD
@@ -79,6 +80,8 @@ struct Settings
if (err == VRSettingsError_None) sensitivityDeg = f;
int32_t i = s->GetInt32(k_section, "toggleButton", &err);
if (err == VRSettingsError_None) toggleButton = i;
i = s->GetInt32(k_section, "backButton", &err);
if (err == VRSettingsError_None) backButton = i;
i = s->GetInt32(k_section, "role", &err);
if (err == VRSettingsError_None) role = i;
char buf[256] = {};
@@ -194,7 +197,7 @@ public:
std::atomic<bool> toggled{false}; // set on every toggle; RunFrame clears it
std::atomic<int> dx{0}, dy{0};
std::atomic<int> wheel{0}, hwheel{0};
std::atomic<bool> left{false}, right{false}, middle{false};
std::atomic<bool> left{false}, right{false}, middle{false}, back{false};
// Destroying a joinable std::thread calls std::terminate (taking vrserver down),
// so make sure the thread is stopped even if Deactivate() was never called.
@@ -281,7 +284,7 @@ private:
Log("EVIOCGRAB failed (%s), staying inactive\n", strerror(errno));
return;
}
left = right = middle = false;
left = right = middle = back = false;
dx = dy = wheel = hwheel = 0;
active = on;
toggled = true;
@@ -338,6 +341,7 @@ private:
if (ev.code == BTN_LEFT) left = ev.value != 0;
else if (ev.code == BTN_RIGHT) right = ev.value != 0;
else if (ev.code == BTN_MIDDLE) middle = ev.value != 0;
else if (ev.code == m_settings.backButton) back = ev.value != 0;
}
}
};
@@ -364,6 +368,7 @@ public:
VRScalarType_Absolute, VRScalarUnits_NormalizedOneSided);
VRDriverInput()->CreateBooleanComponent(c, "/input/a/click", &m_a);
VRDriverInput()->CreateBooleanComponent(c, "/input/b/click", &m_b);
VRDriverInput()->CreateBooleanComponent(c, "/input/back/click", &m_back);
VRDriverInput()->CreateBooleanComponent(c, "/input/grip/click", &m_grip);
VRDriverInput()->CreateBooleanComponent(c, "/input/thumbstick/touch", &m_stickTouch);
VRDriverInput()->CreateScalarComponent(c, "/input/thumbstick/x", &m_stickX,
@@ -450,6 +455,7 @@ public:
VRDriverInput()->UpdateScalarComponent(m_triggerValue, l ? 1.f : 0.f, 0);
VRDriverInput()->UpdateBooleanComponent(m_a, active && m_mouse.right, 0);
VRDriverInput()->UpdateBooleanComponent(m_b, active && m_mouse.middle, 0);
VRDriverInput()->UpdateBooleanComponent(m_back, active && m_mouse.back, 0);
VRDriverInput()->UpdateBooleanComponent(m_grip, active, 0); // holds "quick mouse" on
VRDriverInput()->UpdateBooleanComponent(m_stickTouch, stickX != 0.f || stickY != 0.f, 0);
VRDriverInput()->UpdateScalarComponent(m_stickX, stickX, 0);
@@ -464,7 +470,7 @@ private:
DriverPose_t m_pose = {};
float m_yaw = 0, m_pitch = 0;
StickStepper m_stepX, m_stepY;
VRInputComponentHandle_t m_trigger = 0, m_triggerValue = 0, m_a = 0, m_b = 0, m_grip = 0;
VRInputComponentHandle_t m_trigger = 0, m_triggerValue = 0, m_a = 0, m_b = 0, m_back = 0, m_grip = 0;
VRInputComponentHandle_t m_stickTouch = 0, m_stickX = 0, m_stickY = 0;
};