mirror of
https://github.com/Andalu30/frame-unboundedMouse-vibed.git
synced 2026-10-06 04:04:06 +02:00
Side button now works as Back.
This commit is contained in:
1 parent
da48c37235
commit
12416e005d
6 files changed
+38
-11
No files matched your search
@@ -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.
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ 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.
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user