mirror of
https://github.com/nestriness/nestri.git
synced 2026-10-01 07:02:25 +03:00
feat: controller/gamepad support (#350)
Adds `nesgamepad`, basically "diet-coke vimputti" for Nestri's microVM needs here. Comes with direct mapping for: - Sony - DualShock 4 (v2), DualSense - Microsoft - Xbox 360 pad, Xbox One S pad - Nintendo - Pro Controller - Generic pad for unknown controllers --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
DatCaptainHorse
Claude Opus 5.5
parent
291fabd145
commit
451b495de6
@@ -0,0 +1,106 @@
|
||||
# nesgamepad
|
||||
|
||||
Turns the controllers plugged into a client into devices inside the box, where
|
||||
a game finds them the way it finds real hardware.
|
||||
|
||||
[`neshub`](../neshub) forwards the client's messages about its controllers
|
||||
unread, tagged with which client sent them. The client only describes a
|
||||
controller -- vendor, product, name, and its whole state as a positional
|
||||
snapshot on every change, which is all any platform's gamepad API can say -- and
|
||||
nesgamepad decides what a game finds for it. Rumble a game asks for goes back
|
||||
the same way. The wire format is in
|
||||
[`nesprotocol::gamepad`](../../crates/nesprotocol/src/gamepad.rs).
|
||||
|
||||
A box with no controllers connected has no controller devices at all. That
|
||||
matters: some games stop listening to the keyboard and mouse as soon as one
|
||||
exists.
|
||||
|
||||
---
|
||||
|
||||
## What a controller becomes
|
||||
|
||||
Each controller is matched to a template by vendor and product
|
||||
([`template.rs`](src/template.rs)). One that matches no template, from a vendor
|
||||
the box knows, gets that vendor's default, so it still reads as its vendor's in
|
||||
a game; only one the box cannot tell at all is generic.
|
||||
|
||||
| Vendor | Templates | Default |
|
||||
| --- | --- | --- |
|
||||
| Sony | DualShock 4 (the original, v2, wireless adapter), DualSense | DualShock 4 |
|
||||
| Microsoft | Xbox 360 pad, Xbox One S pad | Xbox 360 pad |
|
||||
| Nintendo | Pro Controller | Pro Controller |
|
||||
| Anything else | | generic |
|
||||
|
||||
**The DualShock 4 is rebuilt as the device itself.** Some games read a
|
||||
controller as a HID device and parse its reports by hand, to tell one family
|
||||
from another and show the right buttons, and Proton hands them the raw device
|
||||
only when it has a hidraw node. So nesgamepad makes one through `/dev/uhid`,
|
||||
from the real controller's report descriptor, and writes the reports the real
|
||||
one would send for the state the client reports, at the rate it sends them.
|
||||
What a game asks of the device -- calibration, firmware, its address -- is
|
||||
answered from values read off real hardware, and rumble it writes goes back to
|
||||
the client. What the snapshot cannot carry, motion and touch, reads as a
|
||||
controller at rest. See [`replica.rs`](src/replica.rs).
|
||||
|
||||
Beside it goes **a gamepad under a neutral identity**, for games that read only
|
||||
XInput. Proton gives XInput only to controllers it reads through SDL, and drops
|
||||
one when a raw device with the same vendor and product exists, so the copy
|
||||
carries neither -- which also keeps a game that recognises the family by those
|
||||
numbers from mistaking the copy for the device. A game that can read a
|
||||
controller both ways uses one at a time, so no press is answered twice.
|
||||
|
||||
**Every other template is one uinput gamepad**, built the way the Linux driver
|
||||
for it builds its device (hid-playstation, xpad, hid-nintendo), from
|
||||
[`layout.rs`](src/layout.rs). A game rarely reads a controller by its raw
|
||||
codes -- SDL, Wine and Steam look the identity up in a mapping database written
|
||||
against the exact layout that driver produces -- and claiming a real device's
|
||||
identity with a different layout scrambles its buttons, which is worse than
|
||||
being unrecognised. So a fallback presents its template's identity, not the
|
||||
client's, and a generic controller gives up its identity altogether.
|
||||
|
||||
## Standing in for udev
|
||||
|
||||
A box has no udev, and games find controllers through libudev. nesgamepad
|
||||
writes udev's database entries for its own devices and sends the hotplug
|
||||
broadcast udev would have sent, for those devices and nothing else. The
|
||||
details libudev checks, each of which fails silently, are written down in
|
||||
[`udev.rs`](src/udev.rs).
|
||||
|
||||
It runs as root because libudev ignores a broadcast from anyone else, because it
|
||||
opens each new device node to the workload, and because `/dev/uhid` is
|
||||
root's alone.
|
||||
|
||||
## Running it
|
||||
|
||||
```bash
|
||||
cargo run --release --bin nesgamepad
|
||||
```
|
||||
|
||||
| Flag | Env | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `--ipc` | `NESTRI_GAMEPAD_IPC` | `/tmp/nestri-gamepad.sock` | The hub's gamepad socket, which this dials |
|
||||
|
||||
`RUST_LOG` takes a standard tracing filter, e.g. `RUST_LOG=nesgamepad=debug`.
|
||||
Every state change is logged at `trace`.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
cargo test -p nesgamepad
|
||||
```
|
||||
|
||||
Four more build real devices through `/dev/uinput` -- every template among them
|
||||
-- and read them back the way a game would, including a rumble upload, and four
|
||||
build one through `/dev/uhid` -- one of them a rebuilt DualShock 4 -- and check
|
||||
reports in both directions and a feature report a game asks for. They are
|
||||
ignored by default because they create a device on whatever machine runs them,
|
||||
and the uhid ones need root:
|
||||
|
||||
```bash
|
||||
cargo test -p nesgamepad kernel_tests -- --ignored
|
||||
```
|
||||
|
||||
One more sends a real udev broadcast. It needs to be uid 0 in a network
|
||||
namespace of its own, which `unshare -rn` gives without root, and is only
|
||||
meaningful with a libudev monitor listening in that namespace to report
|
||||
whether the broadcast was accepted.
|
||||
Reference in New Issue
Block a user