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** | | 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) | | 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 | | 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. | | 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. 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: Check that it loaded:
```sh ```sh
grep -a 'mouselaser:' ~/.local/share/Steam/logs/vrserver.txt | tail 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 ### 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. | | `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. | | `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). | | `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. | | `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. | | `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. | | `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`. - Scrolling stops abruptly: raise `wheelSmoothDecayMs`.
- Fast spins aren't fast enough: raise `wheelSmoothImpulse`. - 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 ## Recovery
If SteamVR won't come up properly after installing, there are four options: 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. 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. - 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. - **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. - 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: - 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. - `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. - `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. 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 ## State at hand-off
- The driver is registered from this repo (`<repo>/driver/mouselaser`). The old prototype registration has been removed. - 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. - 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. - 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.** - 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 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. - 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/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/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/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/raw" : { "type" : "pose", "binding_image_point" : [ 50, 50 ] },
"/pose/tip" : { "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", "app_key": "openvr.component.vrcompositor",
"category": "steamvr_input", "category": "steamvr_input",
"controller_type": "mouselaser", "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", "name": "Mouse Laser: dashboard and overlays",
"options": {}, "options": {},
"simulated_actions": [], "simulated_actions": [],
@@ -74,6 +74,24 @@
"output": "/actions/lasermouse/in/middleclick" "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; using namespace vr;
static const char *k_section = "driver_mouselaser"; 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, ...) static void Log(const char *fmt, ...)
{ {
@@ -54,6 +54,7 @@ struct Settings
{ {
float sensitivityDeg = 0.05f; // degrees of ray rotation per mouse count float sensitivityDeg = 0.05f; // degrees of ray rotation per mouse count
int toggleButton = BTN_EXTRA; int toggleButton = BTN_EXTRA;
int backButton = BTN_SIDE; // sends the compositor's laser "back"; -1 = none
int role = TrackedControllerRole_LeftHand; int role = TrackedControllerRole_LeftHand;
std::string nameFilter; // substring of the evdev name; empty = first mouse found std::string nameFilter; // substring of the evdev name; empty = first mouse found
float originOffsetY = -0.08f; // metres, world space, relative to the HMD float originOffsetY = -0.08f; // metres, world space, relative to the HMD
@@ -79,6 +80,8 @@ struct Settings
if (err == VRSettingsError_None) sensitivityDeg = f; if (err == VRSettingsError_None) sensitivityDeg = f;
int32_t i = s->GetInt32(k_section, "toggleButton", &err); int32_t i = s->GetInt32(k_section, "toggleButton", &err);
if (err == VRSettingsError_None) toggleButton = i; 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); i = s->GetInt32(k_section, "role", &err);
if (err == VRSettingsError_None) role = i; if (err == VRSettingsError_None) role = i;
char buf[256] = {}; char buf[256] = {};
@@ -194,7 +197,7 @@ public:
std::atomic<bool> toggled{false}; // set on every toggle; RunFrame clears it std::atomic<bool> toggled{false}; // set on every toggle; RunFrame clears it
std::atomic<int> dx{0}, dy{0}; std::atomic<int> dx{0}, dy{0};
std::atomic<int> wheel{0}, hwheel{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), // Destroying a joinable std::thread calls std::terminate (taking vrserver down),
// so make sure the thread is stopped even if Deactivate() was never called. // 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)); Log("EVIOCGRAB failed (%s), staying inactive\n", strerror(errno));
return; return;
} }
left = right = middle = false; left = right = middle = back = false;
dx = dy = wheel = hwheel = 0; dx = dy = wheel = hwheel = 0;
active = on; active = on;
toggled = true; toggled = true;
@@ -338,6 +341,7 @@ private:
if (ev.code == BTN_LEFT) left = ev.value != 0; if (ev.code == BTN_LEFT) left = ev.value != 0;
else if (ev.code == BTN_RIGHT) right = 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 == 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); VRScalarType_Absolute, VRScalarUnits_NormalizedOneSided);
VRDriverInput()->CreateBooleanComponent(c, "/input/a/click", &m_a); VRDriverInput()->CreateBooleanComponent(c, "/input/a/click", &m_a);
VRDriverInput()->CreateBooleanComponent(c, "/input/b/click", &m_b); 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/grip/click", &m_grip);
VRDriverInput()->CreateBooleanComponent(c, "/input/thumbstick/touch", &m_stickTouch); VRDriverInput()->CreateBooleanComponent(c, "/input/thumbstick/touch", &m_stickTouch);
VRDriverInput()->CreateScalarComponent(c, "/input/thumbstick/x", &m_stickX, VRDriverInput()->CreateScalarComponent(c, "/input/thumbstick/x", &m_stickX,
@@ -450,6 +455,7 @@ public:
VRDriverInput()->UpdateScalarComponent(m_triggerValue, l ? 1.f : 0.f, 0); VRDriverInput()->UpdateScalarComponent(m_triggerValue, l ? 1.f : 0.f, 0);
VRDriverInput()->UpdateBooleanComponent(m_a, active && m_mouse.right, 0); VRDriverInput()->UpdateBooleanComponent(m_a, active && m_mouse.right, 0);
VRDriverInput()->UpdateBooleanComponent(m_b, active && m_mouse.middle, 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_grip, active, 0); // holds "quick mouse" on
VRDriverInput()->UpdateBooleanComponent(m_stickTouch, stickX != 0.f || stickY != 0.f, 0); VRDriverInput()->UpdateBooleanComponent(m_stickTouch, stickX != 0.f || stickY != 0.f, 0);
VRDriverInput()->UpdateScalarComponent(m_stickX, stickX, 0); VRDriverInput()->UpdateScalarComponent(m_stickX, stickX, 0);
@@ -464,7 +470,7 @@ private:
DriverPose_t m_pose = {}; DriverPose_t m_pose = {};
float m_yaw = 0, m_pitch = 0; float m_yaw = 0, m_pitch = 0;
StickStepper m_stepX, m_stepY; 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; VRInputComponentHandle_t m_stickTouch = 0, m_stickX = 0, m_stickY = 0;
}; };