Files
Andalu30--frame-unboundedMo…/docs/how-it-works.md
T

91 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# How the driver works
All code is in [`../src/driver.cpp`](../src/driver.cpp). Everything SteamVR loads lives in [`../driver/mouselaser/`](../driver/mouselaser/).
## Pieces
```
vrserver
└─ loads driver/mouselaser/bin/linuxarm64/driver_mouselaser.so (driver.vrdrivermanifest: alwaysActivate=true)
HmdDriverFactory() → MouseLaserProvider (IServerTrackedDeviceProvider)
└─ MouseLaserDevice (ITrackedDeviceServerDriver), serial "mouselaser-0", class Controller
├─ MouseReader thread ── reads /dev/input/eventX (evdev), EVIOCGRAB when active
└─ RunFrame() ── pose + input components, every vrserver frame
vrcompositor
└─ uses resources/input/vrcompositor_bindings_mouselaser.json (via the profile's default_bindings)
/actions/lasermouse/in/Pointer ← /user/stylus/pose/raw (or /user/hand/{left,right} for hand roles) → the laser
```
### Why `alwaysActivate: true`
`steamvr.vrsettings` has `activateMultipleDrivers: false` by default. That setting only limits which **HMD** driver is picked. Drivers whose manifest sets `alwaysActivate` are loaded in addition. This is the standard route for add-on trackers.
### MouseReader (evdev thread)
- Scans `/dev/input/event*` for the first device with `EV_REL` (REL_X, REL_Y) and `EV_KEY` (BTN_LEFT), optionally filtered by `deviceNameFilter`. A keyboard's "Consumer Control" node doesn't match.
- `poll()`s with a 200 ms timeout. On `POLLERR`/`POLLHUP` or `ENODEV` (mouse asleep or unplugged) it closes the device and rescans every 2 s.
- Pressing the toggle key (`toggleButton`, default BTN_EXTRA = 276) flips `active`:
- **on:** `ioctl(EVIOCGRAB, 1)` gives exclusive access, so gamescope stops receiving the mouse.
- **off:** `ioctl(EVIOCGRAB, 0)` releases the mouse; button state and deltas are cleared.
- While active it accumulates `REL_X`/`REL_Y`/`REL_WHEEL` into atomics and tracks BTN_LEFT, BTN_RIGHT and BTN_MIDDLE. `RunFrame` consumes them with `exchange(0)`.
### RunFrame (pose)
- HMD pose: `VRServerDriverHost()->GetRawTrackedDevicePoses(0, &hmd, 1)`. Device 0 is the HMD.
- On each OFF→ON toggle, yaw and pitch are initialised from the HMD's forward vector (−Z column of `mDeviceToAbsoluteTracking`):
`yaw = atan2(-fx, -fz)`, `pitch = asin(fy)`.
- Mouse deltas: `yaw -= dx·k`, `pitch -= dy·k` (sign flipped if `invertY`), where `k = sensitivity·π/180`. Pitch is clamped to ±89°.
- Orientation is **world-locked**: `q = yaw(Y) · pitch(X)` = `(cy·cp, cy·sp, sy·cp, −sy·sp)` with half-angle sines and cosines. OpenVR controllers point along −Z, so this aims the ray.
- Position = HMD position + `(0, originOffsetY, 0)` in world space.
- When inactive, it depends on `role`:
- **stylus (5, default since 0.5.0):** the device stays connected with a valid pose, but its ray is parked pointing straight up (yaw 0, pitch 90°) and every button is released. This matters because a device that goes invalid or disconnects is dropped as the laser pointer until SteamVR sees a new "user interaction". That took about 10 s of quiet in earlier captures, so a quick off/on left the mouse grabbed with no laser.
- **hand (1/2):** `deviceIsConnected = false` and `poseIsValid = false`, so the real controller of that hand gets its slot back. A connected device with a hand role would compete with it.
### Role and user path
SteamVR gives each controller a `/user/...` path based on its role. Hand roles share `/user/hand/left|right` with the real controllers. `TrackedControllerRole_Stylus` (5) maps to `/user/stylus`, which `IsRoleAllowedAsHand()` rules out of hand selection. Treadmill (4) maps to `/user/treadmill`. OptOut (3) has no path unless a tracker role is assigned in SteamVR. The compositor's laser accepts pose sources that aren't hands (the Frame HMD binds `/user/head/pose/raw` to `lasermouse/in/pointer`), so the bindings repeat every hand entry for `/user/stylus`.
### Event diagnostics
The provider logs SteamVR events about our device, plus user-interaction, role-change and dashboard events (`event N device M (active|off)`). There is no driver-side signal for "the laser is up", and these events are what we're watching to look for one.
### RunFrame (inputs)
| component | driven by | bound to (compositor) |
|---|---|---|
| `/input/trigger/click`, `/value` | left button | `lasermouse/LeftClick`, `lasermouse_secondary/SwitchLaserHand` |
| `/input/a/click` | right button | `lasermouse/RightClick` |
| `/input/b/click` | middle button | `lasermouse/MiddleClick` |
| `/input/grip/click` | held **true** while active | `quickmouse/ActivateQuickMouse` (keeps the laser up, as squeezing a grip does on Frame controllers) |
| `/input/thumbstick/y`, `/x` (+ `/touch`) | wheel → Y, tilt wheel → X, via `StickStepper` (see below) | `dualanalog/LeftValue`/`RightValue` (+ touch, mode `joystick`), `scroll_discrete` and `scroll_smooth` (mode `scroll`), the same as the Frame controller's stick |
### Wheel → thumbstick (`StickStepper`)
Steam UI lists are navigated through the compositor's `/actions/dualanalog` stick values, not through scroll events. The Frame controllers' `steam.client` binding only carries haptics. A wheel only produces discrete notches, so `StickStepper` turns them into stick deflection. It is timed with `steady_clock`, independently of the frame rate, and has two modes, chosen with `wheelMode`.
**smooth** (default, 0.3.0):
- On a notch: if the stick is at rest, it jumps to `wheelSmoothMin` (+ `wheelSmoothImpulse` for each extra notch in the same event). Otherwise `wheelSmoothImpulse` is added to its current value. Either way it is capped at `wheelDeflection`.
- For `wheelSmoothHoldMs` after a notch the value holds. After that it decays exponentially with time constant `wheelSmoothDecayMs`, and snaps to 0 below 0.1.
- The result:
- A single notch is about 130 ms above 0.5, which is one list step, followed by a short glide.
- Spinning at 10 notches/s ramps up to a steady 0.8–1.0 hold, which scrolls continuously without dips.
- Slow notches (3/s) remain separate steps.
- These figures come from an offline 120 Hz simulation that drives `StickStepper` directly.
**step** (0.2.0 behaviour):
- The stick is pushed to `±wheelDeflection` for `wheelPressMs`, then held at centre for `wheelReleaseMs`.
- Further notches queue up, to at most `wheelMaxQueued`.
- Reversing direction drops the queue and switches immediately.
- When laser mode turns off, the queue is cleared.
### Render model
`{mouselaser}mouselaser_none` is a single 0.1 mm triangle with a transparent 1×1 texture. Without a model of its own, SteamVR would draw a generic controller at your face.
## Files
| file | purpose |
|---|---|
| `driver.vrdrivermanifest` | Driver name and `alwaysActivate`. |
| `resources/settings/default.vrsettings` | Defaults for the `driver_mouselaser` section. |
| `resources/input/mouselaser_profile.json` | Input profile. Controller type `mouselaser`, declares components and points to the default bindings. |
| `resources/input/vrcompositor_bindings_mouselaser.json` | Bindings for app key `openvr.component.vrcompositor`, the laser and dashboard. |
| `resources/rendermodels/mouselaser_none/` | The invisible model. |
| `resources/localization/localization.json` | Display names (en_US, es_ES) for the controller type and its inputs, as shown in SteamVR's binding screens: "Left Click", "Mouse Wheel" and so on. |
## Changing things safely
- After editing code: `cmake --build build`, then reboot. The `.so` is only loaded when SteamVR starts.
- After editing bindings or the profile: reboot. SteamVR may also cache bindings per controller type under `~/.local/share/Steam/config/` or `~/.config/openvr/`.
- To add a setting: add it to `Settings::Load()`, add the default to `default.vrsettings`, and document it in the README table.