diff --git a/README.md b/README.md index 0b810ce..31cd4ed 100644 --- a/README.md +++ b/README.md @@ -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 ()" +# expect: "version 0.4.0-experimental", "activated as device N, role 1", "using /dev/input/eventX ()" ``` ### 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. diff --git a/docs/development-log.md b/docs/development-log.md index 53b0622..23e5feb 100644 --- a/docs/development-log.md +++ b/docs/development-log.md @@ -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 (`/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. \ No newline at end of file diff --git a/docs/steam-frame-background.md b/docs/steam-frame-background.md index 1f00536..b770091 100644 --- a/docs/steam-frame-background.md +++ b/docs/steam-frame-background.md @@ -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. diff --git a/driver/mouselaser/resources/input/mouselaser_profile.json b/driver/mouselaser/resources/input/mouselaser_profile.json index f3f4b72..6c5ef66 100644 --- a/driver/mouselaser/resources/input/mouselaser_profile.json +++ b/driver/mouselaser/resources/input/mouselaser_profile.json @@ -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 ] } }, diff --git a/driver/mouselaser/resources/input/vrcompositor_bindings_mouselaser.json b/driver/mouselaser/resources/input/vrcompositor_bindings_mouselaser.json index d1ad392..bf92394 100644 --- a/driver/mouselaser/resources/input/vrcompositor_bindings_mouselaser.json +++ b/driver/mouselaser/resources/input/vrcompositor_bindings_mouselaser.json @@ -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" + } + } } ] }, diff --git a/src/driver.cpp b/src/driver.cpp index dc1661d..e46f364 100644 --- a/src/driver.cpp +++ b/src/driver.cpp @@ -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 toggled{false}; // set on every toggle; RunFrame clears it std::atomic dx{0}, dy{0}; std::atomic wheel{0}, hwheel{0}; - std::atomic left{false}, right{false}, middle{false}; + std::atomic 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; };