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>
4.7 KiB
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 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.
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). 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.
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. 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.
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
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
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:
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.