mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-30 22:52:25 +03:00
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>
107 lines
4.7 KiB
Markdown
107 lines
4.7 KiB
Markdown
# 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.
|