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:
Kristian Ollikainen
2026-09-27 17:50:02 +03:00
committed by GitHub
co-authored by DatCaptainHorse Claude Opus 5.5
parent 291fabd145
commit 451b495de6
24 changed files with 4528 additions and 41 deletions
Generated
+13
View File
@@ -2361,6 +2361,19 @@ dependencies = [
"ureq", "ureq",
] ]
[[package]]
name = "nesgamepad"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"libc",
"nesprotocol",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "neshub" name = "neshub"
version = "0.2.0" version = "0.2.0"
+1
View File
@@ -16,6 +16,7 @@ members = [
"apps/nescapture", "apps/nescapture",
"apps/nescope", "apps/nescope",
"apps/nesdoctor", "apps/nesdoctor",
"apps/nesgamepad",
"apps/neshub", "apps/neshub",
"apps/nesinit", "apps/nesinit",
"apps/neswire", "apps/neswire",
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "nesgamepad"
version = "0.1.0"
description = "Turns the controllers plugged into a client into input devices inside the box"
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
libc.workspace = true
tokio.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
nesprotocol = { path = "../../crates/nesprotocol" }
+106
View File
@@ -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.
+373
View File
@@ -0,0 +1,373 @@
//! Against the real kernel: build a device through `/dev/uinput` and read it
//! back the way a game would.
//!
//! Ignored by default, because they need write access to `/dev/uinput` and
//! create a real (short-lived) input device on whatever runs them. Run with
//! `cargo test -p nesgamepad -- --ignored` on a machine where that is fine.
//! Nothing here touches udev's database: that is the host's, not ours.
use std::fs::OpenOptions;
use std::os::fd::AsRawFd;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use nesprotocol::gamepad::{PadState, button};
use crate::layout;
use crate::uinput::code::{self, BUS_USB};
use crate::uinput::{Device, Request, Spec};
fn build(layout: layout::Layout) -> (Device, layout::Layout) {
let keys: Vec<u16> = layout.buttons.iter().map(|&(_, key)| key).collect();
let device = Device::create(&Spec {
name: &layout.name,
bus: layout.bus,
vendor: layout.vendor,
product: layout.product,
version: layout.version,
keys: &keys,
axes: &layout.axes(),
})
.expect("create a device; is /dev/uinput writable?");
(device, layout)
}
/// Open a new device's node, waiting out the host's udev granting access to
/// it, which happens a moment after the node itself appears.
fn open(path: &Path, write: bool) -> std::fs::File {
let deadline = Instant::now() + Duration::from_secs(3);
loop {
match OpenOptions::new().read(true).write(write).open(path) {
Ok(file) => return file,
Err(e) if Instant::now() > deadline => panic!("{}: {e}", path.display()),
Err(_) => std::thread::sleep(Duration::from_millis(20)),
}
}
}
fn read(path: impl AsRef<Path>) -> String {
std::fs::read_to_string(path).unwrap().trim().to_owned()
}
fn event_node(device: &Device) -> std::path::PathBuf {
let dir = device.syspath();
let name = std::fs::read_dir(&dir)
.unwrap()
.filter_map(Result::ok)
.map(|e| e.file_name().to_string_lossy().into_owned())
.find(|n| n.starts_with("event"))
.expect("an event node");
Path::new("/dev/input").join(name)
}
#[test]
#[ignore = "creates a real input device through /dev/uinput"]
fn a_hid_playstation_pad_reads_like_the_real_one() {
let (device, _) = build(layout::dualsense());
let sys = device.syspath();
// Every capability below was read from a DualShock 4 v2 on USB through
// hid-playstation, which builds the gamepad node of every controller it
// drives with the same function. FF differs on purpose: the real one also
// lists the periodic effects ff-memless emulates, which this does not
// offer.
assert_eq!(
read(sys.join("name")),
"Sony Interactive Entertainment DualSense Wireless Controller"
);
assert_eq!(read(sys.join("id/bustype")), "0003");
assert_eq!(read(sys.join("id/vendor")), "054c");
assert_eq!(read(sys.join("id/product")), "0ce6");
assert_eq!(read(sys.join("id/version")), "8111");
assert_eq!(read(sys.join("capabilities/ev")), "20000b");
assert_eq!(
read(sys.join("capabilities/key")),
"7fdb000000000000 0 0 0 0"
);
assert_eq!(read(sys.join("capabilities/abs")), "3003f");
assert_eq!(read(sys.join("capabilities/ff")), "10000 0");
}
#[test]
#[ignore = "creates a real input device through /dev/uinput"]
fn every_template_has_its_drivers_capabilities() {
// As each driver's tables add up: xpad's with the d-pad as a hat and the
// triggers as axes, hid-nintendo's for a Pro Controller.
for (layout, key, abs) in [
(layout::xbox_360(), "7cdb000000000000 0 0 0 0", "3003f"),
(layout::xbox_one(), "7cdb000000000000 0 0 0 0", "3003f"),
(layout::switch_pro(), "7ffb000000000000 0 0 0 0", "3001b"),
] {
let (device, _) = build(layout.clone());
let sys = device.syspath();
assert_eq!(read(sys.join("name")), layout.name);
assert_eq!(read(sys.join("capabilities/key")), key, "{}", layout.name);
assert_eq!(read(sys.join("capabilities/abs")), abs, "{}", layout.name);
}
}
/// `EVIOCGABS(code)`: an axis's current value and range.
fn abs(fd: i32, code: u16) -> [i32; 6] {
let mut info = [0i32; 6];
let request = (2u64 << 30) | (24u64 << 16) | ((b'E' as u64) << 8) | (0x40 + code as u64);
assert!(unsafe { libc::ioctl(fd, request as _, info.as_mut_ptr()) } >= 0);
info
}
#[test]
#[ignore = "creates a real input device through /dev/uinput"]
fn state_written_is_state_a_game_reads() {
let (device, layout) = build(layout::dualsense());
let node = open(&event_node(&device), false);
let fd = node.as_raw_fd();
let resting = abs(fd, code::ABS_X);
assert_eq!(&resting[1..3], &[0, 255]);
let state = PadState {
left_x: i16::MAX,
right_trigger: u16::MAX,
buttons: button::DPAD_DOWN,
..PadState::default()
};
device.write(&layout.events(&state)).unwrap();
assert_eq!(abs(fd, code::ABS_X)[0], 255);
assert_eq!(abs(fd, code::ABS_RZ)[0], 255);
assert_eq!(abs(fd, code::ABS_HAT0Y)[0], 1);
}
/// `struct ff_effect` for a rumble, as a game uploads it.
#[repr(C)]
#[allow(dead_code)]
struct Rumble {
kind: u16,
id: i16,
direction: u16,
trigger: [u16; 2],
replay: [u16; 2],
/// The union starts eight-aligned.
gap: u16,
strong: u16,
weak: u16,
pad: [u8; 28],
}
const _: () = assert!(std::mem::size_of::<Rumble>() == 48);
#[test]
#[ignore = "creates a real input device through /dev/uinput"]
fn a_game_uploading_rumble_is_answered_and_heard() {
let (device, _) = build(layout::dualsense());
let node = open(&event_node(&device), true);
// The upload blocks in the kernel until the device's owner answers it, so
// the game's side runs on a thread of its own, exactly as it would be a
// process of its own.
let game = std::thread::spawn(move || {
let mut effect = Rumble {
kind: code::FF_RUMBLE,
id: -1,
direction: 0,
trigger: [0; 2],
replay: [300, 0],
gap: 0,
strong: 0xc000,
weak: 0x4000,
pad: [0; 28],
};
const EVIOCSFF: u64 = (1u64 << 30) | (48u64 << 16) | ((b'E' as u64) << 8) | 0x80;
let rc = unsafe { libc::ioctl(node.as_raw_fd(), EVIOCSFF as _, &mut effect) };
assert!(
rc >= 0,
"upload refused: {}",
std::io::Error::last_os_error()
);
// Play it once.
let play = crate::uinput::InputEvent::default();
let mut raw: [u8; 24] = unsafe { std::mem::transmute(play) };
raw[16..18].copy_from_slice(&code::EV_FF.to_ne_bytes());
raw[18..20].copy_from_slice(&(effect.id as u16).to_ne_bytes());
raw[20..24].copy_from_slice(&1i32.to_ne_bytes());
use std::io::Write;
(&node).write_all(&raw).unwrap();
effect.id
});
let deadline = Instant::now() + Duration::from_secs(5);
let mut seen = Vec::new();
while Instant::now() < deadline && !seen.iter().any(|r| matches!(r, Request::Play { .. })) {
seen.extend(device.drain().unwrap());
std::thread::sleep(Duration::from_millis(5));
}
let id = game.join().unwrap();
let uploaded = seen.iter().find_map(|r| match r {
Request::Upload(effect) => Some(*effect),
_ => None,
});
let uploaded = uploaded.expect("the upload reached the device");
assert_eq!(uploaded.id, id);
assert_eq!(uploaded.rumble(), Some((0xc000, 0x4000)));
assert_eq!(uploaded.length_ms(), 300);
assert!(
seen.iter()
.any(|r| matches!(r, Request::Play { id: played, on: true } if *played == id)),
"{seen:?}"
);
}
/// A small gamepad, written by hand: report 1 is eight buttons and two axes,
/// report 2 a four-byte feature, report 3 a one-byte output.
#[rustfmt::skip]
const GAMEPAD_DESCRIPTOR: &[u8] = &[
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01,
0x85, 0x01,
0x05, 0x09, 0x19, 0x01, 0x29, 0x08, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x08, 0x81, 0x02,
0x05, 0x01, 0x09, 0x30, 0x09, 0x31, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02,
0x85, 0x02,
0x06, 0x00, 0xff, 0x09, 0x01, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x04, 0xb1, 0x02,
0x85, 0x03,
0x09, 0x02, 0x75, 0x08, 0x95, 0x01, 0x91, 0x02,
0xc0,
];
/// The device, started and with its hidraw node found.
fn build_hid() -> (Arc<crate::uhid::Device>, std::path::PathBuf) {
use crate::uhid::{Device, Event, Spec};
let device = Arc::new(
Device::create(&Spec {
name: "nesgamepad test pad",
uniq: "test",
bus: BUS_USB,
vendor: 0x1234,
product: 0x5678,
version: 0x0100,
country: 0,
descriptor: GAMEPAD_DESCRIPTOR,
})
.expect("create a HID device; this needs root for /dev/uhid"),
);
let deadline = Instant::now() + Duration::from_secs(3);
while !device.drain().unwrap().contains(&Event::Start) {
assert!(Instant::now() < deadline, "the kernel never started it");
std::thread::sleep(Duration::from_millis(5));
}
loop {
if let Some((_, nodes)) =
crate::pads::discover(BUS_USB, 0x1234, 0x5678, &Default::default())
{
let name = nodes.hidraw[0].file_name().unwrap().to_owned();
return (device, Path::new("/dev").join(name));
}
assert!(Instant::now() < deadline, "no hidraw node appeared");
std::thread::sleep(Duration::from_millis(5));
}
}
#[test]
#[ignore = "needs root: creates a device through /dev/uhid"]
fn a_report_written_is_the_report_a_game_reads() {
use std::io::Read;
let (device, node) = build_hid();
let mut hidraw = open(&node, false);
device.input(&[0x01, 0b1000_0001, 0x10, 0xf0]).unwrap();
let mut buf = [0u8; 64];
let n = hidraw.read(&mut buf).unwrap();
assert_eq!(&buf[..n], &[0x01, 0b1000_0001, 0x10, 0xf0]);
}
#[test]
#[ignore = "needs root: creates a device through /dev/uhid"]
fn a_game_reading_a_feature_report_gets_the_answer_given() {
use crate::uhid::Event;
let (device, node) = build_hid();
let hidraw = open(&node, true);
let game = std::thread::spawn(move || {
// HIDIOCGFEATURE(5): report id in, report out.
let mut buf = [0x02u8, 0, 0, 0, 0];
let request = (3u64 << 30) | (5u64 << 16) | ((b'H' as u64) << 8) | 0x07;
let n = unsafe { libc::ioctl(hidraw.as_raw_fd(), request as _, buf.as_mut_ptr()) };
assert!(n >= 0, "{}", std::io::Error::last_os_error());
buf
});
let deadline = Instant::now() + Duration::from_secs(3);
loop {
let events = device.drain().unwrap();
if let Some(Event::GetReport { id, number, kind }) = events
.into_iter()
.find(|e| matches!(e, Event::GetReport { .. }))
{
assert_eq!((number, kind), (0x02, crate::uhid::REPORT_FEATURE));
device
.get_report_reply(id, 0, &[0x02, 0xde, 0xad, 0xbe, 0xef])
.unwrap();
break;
}
assert!(Instant::now() < deadline, "the request never arrived");
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(game.join().unwrap(), [0x02, 0xde, 0xad, 0xbe, 0xef]);
}
#[test]
#[ignore = "needs root: creates a device through /dev/uhid"]
fn a_report_a_game_writes_comes_out_of_the_device() {
use crate::uhid::Event;
use std::io::Write;
let (device, node) = build_hid();
let mut hidraw = open(&node, true);
hidraw.write_all(&[0x03, 0xaa]).unwrap();
let deadline = Instant::now() + Duration::from_secs(3);
loop {
let events = device.drain().unwrap();
if let Some(Event::Output { kind, data }) = events
.into_iter()
.find(|e| matches!(e, Event::Output { .. }))
{
assert_eq!(kind, crate::uhid::REPORT_OUTPUT);
assert_eq!(data, [0x03, 0xaa]);
return;
}
assert!(Instant::now() < deadline, "the output never arrived");
std::thread::sleep(Duration::from_millis(5));
}
}
#[test]
#[ignore = "needs root: creates a device through /dev/uhid"]
fn a_rebuilt_dualshock_4_is_taken_by_the_kernel_and_read_as_sent() {
use crate::replica::{Model, Reporter};
use crate::uhid::{Device, Event};
use std::io::Read;
let model = Model::DualShock4;
let device = Device::create(&model.spec(0x09cc, "02:00:00:00:00:01"))
.expect("create a HID device; this needs root for /dev/uhid");
let deadline = Instant::now() + Duration::from_secs(3);
while !device.drain().unwrap().contains(&Event::Start) {
assert!(
Instant::now() < deadline,
"the kernel refused the descriptor"
);
std::thread::sleep(Duration::from_millis(5));
}
let node = loop {
if let Some((_, nodes)) =
crate::pads::discover(BUS_USB, 0x054c, 0x09cc, &Default::default())
{
break Path::new("/dev").join(nodes.hidraw[0].file_name().unwrap());
}
assert!(Instant::now() < deadline, "no hidraw node appeared");
std::thread::sleep(Duration::from_millis(5));
};
let mut hidraw = open(&node, false);
let mut reporter = Reporter::new(model);
reporter.set(PadState {
buttons: button::SOUTH,
..PadState::default()
});
let report = reporter.next();
device.input(&report).unwrap();
let mut buf = [0u8; 128];
let n = hidraw.read(&mut buf).unwrap();
assert_eq!(&buf[..n], &report[..]);
}
+418
View File
@@ -0,0 +1,418 @@
//! What a controller looks like to a game as an evdev device: its identity and
//! its layout.
//!
//! # Why the layout follows the identity
//!
//! A game rarely reads a controller's buttons by code. SDL, Wine and Steam each
//! build an id from the device's bus, vendor, product and version, look it up
//! in a mapping database, and read the device through that mapping -- which
//! was written against the exact set of axes and buttons the *Linux driver*
//! for that device exposes, in the order the driver exposes them. A device
//! claiming a real controller's identity with any other layout gets its
//! buttons scrambled, which is worse than being unrecognised.
//!
//! So each template here is built the way the driver for its identity builds
//! it, read from that driver's source, or it is [`generic`]: the kernel's own
//! gamepad layout under a neutral identity, which every mapping layer knows how
//! to read without a database entry. Which template a controller gets is
//! `crate::template`'s to decide.
use nesprotocol::gamepad::button;
use nesprotocol::gamepad::{PadIdentity, PadState};
use crate::uinput::code::{self, BUS_USB, BUS_VIRTUAL};
use crate::uinput::{AbsAxis, InputEvent};
/// A controller as the box will present it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Layout {
pub name: String,
pub bus: u16,
pub vendor: u16,
pub product: u16,
pub version: u16,
/// Wire button bit, and the key code it becomes. A bit of zero is a key
/// the real device has and the wire cannot carry: registered, because a
/// mapping counts the device's keys to number them, and never pressed.
pub buttons: &'static [(u32, u16)],
/// Left x, left y, right x, right y.
pub sticks: [AbsAxis; 4],
/// Left, right. `None` for a controller whose triggers are only buttons.
pub triggers: Option<[AbsAxis; 2]>,
/// Which driver this imitates, for the log.
pub driver: &'static str,
}
/// The kernel's generic gamepad buttons, by position.
///
/// Also exactly what hid-playstation registers, which is why its template
/// shares it. The d-pad is not here: every layout below reports it as a hat.
const GAMEPAD_BUTTONS: &[(u32, u16)] = &[
(button::SOUTH, code::BTN_SOUTH),
(button::EAST, code::BTN_EAST),
(button::NORTH, code::BTN_NORTH),
(button::WEST, code::BTN_WEST),
(button::LEFT_SHOULDER, code::BTN_TL),
(button::RIGHT_SHOULDER, code::BTN_TR),
(button::LEFT_TRIGGER, code::BTN_TL2),
(button::RIGHT_TRIGGER, code::BTN_TR2),
(button::SELECT, code::BTN_SELECT),
(button::START, code::BTN_START),
(button::MODE, code::BTN_MODE),
(button::LEFT_STICK, code::BTN_THUMBL),
(button::RIGHT_STICK, code::BTN_THUMBR),
];
/// What xpad registers for the 360 and One pads it drives with the d-pad as
/// a hat and the triggers as axes.
///
/// xpad names its face buttons by letter, not position: the left one, X, is
/// reported as `BTN_X`, which is the same code as `BTN_NORTH`, and the top
/// one, Y, as `BTN_Y`, the code of `BTN_WEST`. Mappings written against xpad
/// expect exactly that, so it is kept.
const XPAD_BUTTONS: &[(u32, u16)] = &[
(button::SOUTH, code::BTN_SOUTH),
(button::EAST, code::BTN_EAST),
(button::WEST, code::BTN_X),
(button::NORTH, code::BTN_Y),
(button::LEFT_SHOULDER, code::BTN_TL),
(button::RIGHT_SHOULDER, code::BTN_TR),
(button::SELECT, code::BTN_SELECT),
(button::START, code::BTN_START),
(button::MODE, code::BTN_MODE),
(button::LEFT_STICK, code::BTN_THUMBL),
(button::RIGHT_STICK, code::BTN_THUMBR),
];
/// What hid-nintendo registers for a Pro Controller: positional, with ZL and
/// ZR as buttons only, and Capture, which the wire does not carry.
const PRO_CONTROLLER_BUTTONS: &[(u32, u16)] = &[
(button::SOUTH, code::BTN_SOUTH),
(button::EAST, code::BTN_EAST),
(button::NORTH, code::BTN_NORTH),
(button::WEST, code::BTN_WEST),
(button::LEFT_SHOULDER, code::BTN_TL),
(button::RIGHT_SHOULDER, code::BTN_TR),
(button::LEFT_TRIGGER, code::BTN_TL2),
(button::RIGHT_TRIGGER, code::BTN_TR2),
(button::SELECT, code::BTN_SELECT),
(button::START, code::BTN_START),
(button::MODE, code::BTN_MODE),
(button::LEFT_STICK, code::BTN_THUMBL),
(button::RIGHT_STICK, code::BTN_THUMBR),
(0, code::BTN_Z),
];
pub const SONY: u16 = 0x054c;
pub const MICROSOFT: u16 = 0x045e;
pub const NINTENDO: u16 = 0x057e;
fn axis(code: u16, min: i32, max: i32, fuzz: i32, flat: i32) -> AbsAxis {
AbsAxis {
code,
min,
max,
fuzz,
flat,
}
}
fn sticks(min: i32, max: i32, fuzz: i32, flat: i32) -> [AbsAxis; 4] {
[code::ABS_X, code::ABS_Y, code::ABS_RX, code::ABS_RY].map(|c| axis(c, min, max, fuzz, flat))
}
fn triggers(max: i32) -> Option<[AbsAxis; 2]> {
Some([code::ABS_Z, code::ABS_RZ].map(|c| axis(c, 0, max, 0, 0)))
}
/// A DualSense on a cable, as hid-playstation makes it.
///
/// One byte per stick and trigger, no fuzz, no flat, as the driver's
/// `ps_gamepad_create` sets them for every controller it drives. The version
/// is the driver's patch bit, which tells userspace the mapping is the
/// driver's and not hid-generic's, on the HID version; read from a
/// DualShock 4 v2 under the driver, as is the rest.
pub fn dualsense() -> Layout {
Layout {
name: "Sony Interactive Entertainment DualSense Wireless Controller".into(),
bus: BUS_USB,
vendor: SONY,
product: 0x0ce6,
version: 0x8111,
buttons: GAMEPAD_BUTTONS,
sticks: sticks(0, 255, 0, 0),
triggers: triggers(255),
driver: "hid-playstation",
}
}
/// A wired Xbox 360 pad, as xpad makes it. The version is the device's own
/// release number, which xpad passes on.
pub fn xbox_360() -> Layout {
Layout {
name: "Microsoft X-Box 360 pad".into(),
bus: BUS_USB,
vendor: MICROSOFT,
product: 0x028e,
version: 0x0114,
buttons: XPAD_BUTTONS,
sticks: sticks(-32768, 32767, 16, 128),
triggers: triggers(255),
driver: "xpad",
}
}
/// An Xbox One S pad on a cable, as xpad makes it: the 360's layout, with ten
/// bits of trigger.
pub fn xbox_one() -> Layout {
Layout {
name: "Microsoft X-Box One S pad".into(),
bus: BUS_USB,
vendor: MICROSOFT,
product: 0x02ea,
version: 0x0408,
buttons: XPAD_BUTTONS,
sticks: sticks(-32768, 32767, 16, 128),
triggers: triggers(1023),
driver: "xpad",
}
}
/// A Pro Controller on a cable, as hid-nintendo makes it. The name is what
/// the device calls itself over USB, manufacturer first, which the driver
/// passes on; the version is the HID version, which it passes on too.
pub fn switch_pro() -> Layout {
Layout {
name: "Nintendo Co., Ltd. Pro Controller".into(),
bus: BUS_USB,
vendor: NINTENDO,
product: 0x2009,
version: 0x0111,
buttons: PRO_CONTROLLER_BUTTONS,
sticks: sticks(-32767, 32767, 250, 500),
triggers: None,
driver: "hid-nintendo",
}
}
/// A controller the box cannot tell.
///
/// The identity is dropped on purpose (see the module docs): vendor and
/// product zero on the virtual bus, so no mapping database can match it to a
/// real device with a different layout. The name stays, because that is what a
/// person sees in a game's settings.
pub fn generic(identity: &PadIdentity) -> Layout {
let name = if identity.name.trim().is_empty() {
"Gamepad".to_owned()
} else {
identity.name.clone()
};
Layout {
name,
bus: BUS_VIRTUAL,
vendor: 0,
product: 0,
version: 0,
buttons: GAMEPAD_BUTTONS,
sticks: sticks(-32768, 32767, 16, 128),
triggers: triggers(1023),
driver: "generic",
}
}
/// Map a full-range wire value onto an axis's own range.
fn scale(axis: &AbsAxis, from_min: i64, from_max: i64, value: i64) -> i32 {
let span = i64::from(axis.max) - i64::from(axis.min);
let offset = (value - from_min) * span / (from_max - from_min);
(i64::from(axis.min) + offset) as i32
}
impl Layout {
/// Every event that makes the device read as `state`, ending in a report.
///
/// All of them every time: the kernel drops a value that did not change
/// before any reader sees it, so repeating one costs nothing downstream,
/// and a device built from a snapshot can never drift from it.
pub fn events(&self, state: &PadState) -> Vec<InputEvent> {
let mut events = Vec::with_capacity(self.buttons.len() + 9);
for &(bit, key) in self.buttons {
events.push(InputEvent::key(key, state.buttons & bit != 0));
}
let sticks = [state.left_x, state.left_y, state.right_x, state.right_y];
for (axis, value) in self.sticks.iter().zip(sticks) {
let value = scale(axis, i16::MIN.into(), i16::MAX.into(), value.into());
events.push(InputEvent::abs(axis.code, value));
}
for (axis, value) in self
.triggers
.iter()
.flatten()
.zip([state.left_trigger, state.right_trigger])
{
let value = scale(axis, 0, u16::MAX.into(), value.into());
events.push(InputEvent::abs(axis.code, value));
}
let held = |bit| i32::from(state.buttons & bit != 0);
events.push(InputEvent::abs(
code::ABS_HAT0X,
held(button::DPAD_RIGHT) - held(button::DPAD_LEFT),
));
events.push(InputEvent::abs(
code::ABS_HAT0Y,
held(button::DPAD_DOWN) - held(button::DPAD_UP),
));
events.push(InputEvent::report());
events
}
/// Every axis the device has, with its range.
pub fn axes(&self) -> Vec<AbsAxis> {
let hat = |code| AbsAxis {
code,
min: -1,
max: 1,
fuzz: 0,
flat: 0,
};
let mut axes: Vec<AbsAxis> = self
.sticks
.iter()
.chain(self.triggers.iter().flatten())
.copied()
.collect();
axes.push(hat(code::ABS_HAT0X));
axes.push(hat(code::ABS_HAT0Y));
axes
}
/// `ID_BUS` as udev writes it, where udev writes one at all.
pub fn udev_bus(&self) -> Option<&'static str> {
match self.bus {
BUS_USB => Some("usb"),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn value(events: &[InputEvent], code: u16) -> i32 {
events
.iter()
.find(|e| e.code == code && e.kind != code::EV_SYN)
.map(|e| e.value)
.unwrap()
}
fn all() -> Vec<Layout> {
vec![
dualsense(),
xbox_360(),
xbox_one(),
switch_pro(),
generic(&PadIdentity {
vendor: 1,
product: 2,
name: String::new(),
}),
]
}
/// The key capability as sysfs prints its first word: bit n is key
/// `0x130 + n`.
fn key_bits(layout: &Layout) -> u64 {
layout
.buttons
.iter()
.fold(0, |bits, &(_, key)| bits | 1 << (key - 0x130))
}
#[test]
fn each_template_has_the_keys_its_driver_registers() {
// hid-playstation's read from a DualShock 4 under the driver, which
// registers the same for every controller it drives; the others as
// their drivers' tables add up.
assert_eq!(key_bits(&dualsense()), 0x7fdb);
assert_eq!(key_bits(&xbox_360()), 0x7cdb);
assert_eq!(key_bits(&xbox_one()), 0x7cdb);
assert_eq!(key_bits(&switch_pro()), 0x7ffb);
}
#[test]
fn xpad_puts_the_left_face_button_on_btn_x() {
let events = xbox_360().events(&PadState {
buttons: button::WEST,
..PadState::default()
});
assert_eq!(value(&events, code::BTN_X), 1);
assert_eq!(value(&events, code::BTN_Y), 0);
}
#[test]
fn a_key_the_wire_cannot_carry_is_never_pressed() {
let events = switch_pro().events(&PadState {
buttons: u32::MAX,
..PadState::default()
});
assert_eq!(value(&events, code::BTN_Z), 0);
assert_eq!(value(&events, code::BTN_TL2), 1);
}
#[test]
fn a_controller_without_analog_triggers_has_no_trigger_axes() {
let codes: Vec<u16> = switch_pro().axes().iter().map(|a| a.code).collect();
assert!(!codes.contains(&code::ABS_Z) && !codes.contains(&code::ABS_RZ));
}
#[test]
fn a_resting_controller_rests_on_every_layout() {
for layout in all() {
let events = layout.events(&PadState::default());
for axis in &layout.sticks {
let v = value(&events, axis.code);
let mid = (axis.min + axis.max) / 2;
assert!(
(v - mid).abs() <= 1,
"{} stick {} at {v}",
layout.driver,
axis.code
);
}
for axis in layout.triggers.iter().flatten() {
assert_eq!(value(&events, axis.code), axis.min);
}
assert_eq!(value(&events, code::ABS_HAT0X), 0);
assert!(events.last().unwrap().kind == code::EV_SYN);
}
}
#[test]
fn full_deflection_reaches_both_ends_of_every_range() {
for layout in all() {
let pushed = PadState {
left_x: i16::MIN,
left_y: i16::MAX,
right_trigger: u16::MAX,
buttons: button::DPAD_UP | button::DPAD_LEFT | button::EAST,
..PadState::default()
};
let events = layout.events(&pushed);
let [x, y, ..] = layout.sticks;
assert!(
(value(&events, x.code) - x.min).abs() <= 1,
"{}",
layout.driver
);
assert_eq!(value(&events, y.code), y.max, "{}", layout.driver);
if let Some([_, rz]) = layout.triggers {
assert_eq!(value(&events, rz.code), rz.max);
}
assert_eq!(value(&events, code::ABS_HAT0X), -1);
assert_eq!(value(&events, code::ABS_HAT0Y), -1);
assert_eq!(value(&events, code::BTN_EAST), 1);
assert_eq!(value(&events, code::BTN_SOUTH), 0);
}
}
}
+168
View File
@@ -0,0 +1,168 @@
//! nesgamepad: the controllers plugged into a client, as devices in the box.
//!
//! The hub forwards what each client says about its controllers, tagged with
//! which client, and this makes virtual devices for each one that a game finds
//! the way it finds real hardware -- the controller itself where it can be
//! rebuilt, a gamepad otherwise -- announced through libudev. Rumble a game
//! asks for goes back the same way.
//!
//! Idle until the first controller arrives. A box with none connected has no
//! devices at all, which matters: some games stop listening to the keyboard
//! and mouse the moment a controller exists.
//!
//! Runs as root, for three things nothing else can grant: creating devices
//! through `/dev/uinput` and `/dev/uhid`, opening their nodes to the
//! workload, and sending the udev broadcast, whose receivers drop anything not
//! sent by uid 0.
#[cfg(test)]
mod kernel_tests;
mod layout;
mod pads;
mod replica;
mod template;
mod udev;
mod uhid;
mod uinput;
use std::path::PathBuf;
use std::time::Duration;
use anyhow::Result;
use clap::Parser;
use nesprotocol::gamepad::{PadMessage, decode_ipc, encode_ipc};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
use tracing::{debug, info, warn};
use crate::pads::{Feedback, HidNotice, Key, Pads};
#[derive(Parser, Debug)]
#[command(name = "nesgamepad")]
struct Args {
/// The hub's gamepad socket. The hub listens; this dials.
#[arg(
long,
env = "NESTRI_GAMEPAD_IPC",
default_value = "/tmp/nestri-gamepad.sock"
)]
ipc: PathBuf,
}
/// How long to wait before dialling the hub again.
const REDIAL: Duration = Duration::from_secs(1);
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
)
.init();
let args = Args::parse();
// Without it the devices still exist, and nothing that finds devices
// through libudev can see them -- which is almost everything. Said loudly
// and carried on with, because a box that at least makes the device is
// easier to diagnose than one that refused to start.
let udev = match udev::Udev::new() {
Ok(udev) => Some(udev),
Err(e) => {
warn!("cannot stand in for udev, so games will not find controllers: {e}");
None
}
};
let (feedback_tx, mut feedback_rx) = tokio::sync::mpsc::unbounded_channel::<Feedback>();
let (hid_tx, mut hid_rx) = tokio::sync::mpsc::unbounded_channel::<(Key, HidNotice)>();
let mut pads = Pads::new(udev, feedback_tx, hid_tx);
info!("waiting for the hub on {}", args.ipc.display());
let mut failures = 0u32;
loop {
let stream = match UnixStream::connect(&args.ipc).await {
Ok(stream) => stream,
Err(e) => {
// Once at warn, and after that at debug: the hub starting a
// little later than this is normal, and a line a second while
// it does is noise.
if failures == 0 {
warn!("hub not reachable at {} yet: {e}", args.ipc.display());
} else {
debug!("hub still not reachable: {e}");
}
failures += 1;
tokio::time::sleep(REDIAL).await;
continue;
}
};
failures = 0;
info!("connected to the hub");
if let Err(e) = serve(stream, &mut pads, &mut feedback_rx, &mut hid_rx).await {
debug!("hub connection ended: {e}");
}
// Every controller belonged to a client of that hub, and the hub has
// gone -- so have they. Leaving them would leave a game holding
// controllers that nobody is on the other end of.
pads.clear();
// Feedback queued for clients that no longer exist, and notices from
// devices that no longer do.
while feedback_rx.try_recv().is_ok() {}
while hid_rx.try_recv().is_ok() {}
info!("lost the hub; every controller unplugged");
tokio::time::sleep(REDIAL).await;
}
}
async fn serve(
stream: UnixStream,
pads: &mut Pads,
feedback: &mut tokio::sync::mpsc::UnboundedReceiver<Feedback>,
hid: &mut tokio::sync::mpsc::UnboundedReceiver<(Key, HidNotice)>,
) -> Result<()> {
let (mut read, mut write) = stream.into_split();
// Reading on a task of its own: a read of a frame is several awaits, and
// one cancelled halfway by rumble arriving would lose the framing for
// good.
let (tx, mut messages) = tokio::sync::mpsc::unbounded_channel::<(u32, PadMessage)>();
let reader = tokio::spawn(async move {
let mut len_buf = [0u8; 2];
loop {
read.read_exact(&mut len_buf).await?;
let mut body = vec![0u8; u16::from_le_bytes(len_buf) as usize];
read.read_exact(&mut body).await?;
let Some((session, message)) = decode_ipc(&body) else {
debug!("short frame from the hub");
continue;
};
match PadMessage::decode(message) {
Some(message) => {
if tx.send((session, message)).is_err() {
return Ok::<_, std::io::Error>(());
}
}
None => debug!(session, "a gamepad message this does not understand"),
}
}
});
let result = loop {
tokio::select! {
message = messages.recv() => match message {
Some((session, message)) => pads.handle(session, message),
None => break Ok(()),
},
Some((key, notice)) = hid.recv() => pads.hid_notice(key, notice),
Some((session, message)) = feedback.recv() => {
let mut body = Vec::with_capacity(16);
message.encode(&mut body);
let mut frame = Vec::with_capacity(6 + body.len());
encode_ipc(&mut frame, session, &body);
if let Err(e) = write.write_all(&frame).await {
break Err(e.into());
}
}
}
};
reader.abort();
result
}
+920
View File
@@ -0,0 +1,920 @@
//! Every controller plugged into the box, by the client and slot it came from.
//!
//! The client only ever describes a controller; what a game finds for it is
//! decided here, in one of two ways:
//!
//! - **A family the box can rebuild** (see `crate::replica`) becomes the device
//! itself, through uhid, for games that read the device and parse its
//! reports. 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 such a controller when a raw device with the same
//! vendor and product exists, so the copy survives only without them -- and
//! without them, no game that recognises the family by those numbers
//! mistakes the copy for the device. Games that offer both ways of reading a
//! controller use one at a time, so a press is never answered twice.
//! - **Anything else** becomes one uinput gamepad, laid out the way the Linux
//! driver for its identity would lay it out (see `crate::layout`).
use std::collections::{HashMap, HashSet};
use std::os::fd::{AsRawFd, RawFd};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use nesprotocol::gamepad::{PadFeedback, PadIdentity, PadMessage, PadState};
use tokio::io::unix::AsyncFd;
use tokio::sync::mpsc::UnboundedSender;
use tracing::{debug, info, trace, warn};
use crate::layout::{self, Layout};
use crate::replica::{self, Model, Reporter};
use crate::template::{self, Template};
use crate::udev::{Record, Udev};
use crate::uhid;
use crate::uinput::{Device, Request, Spec, code};
/// Controllers across every client at once.
///
/// Not a limit anyone should meet: well past what any game expects to see, and
/// there so that one that has misbehaved cannot fill the box with devices.
pub const MAX_PADS: usize = 16;
/// A client's slot. The session is the hub's number for the client.
pub type Key = (u32, u8);
/// Feedback for one client.
pub type Feedback = (u32, PadFeedback);
/// Something a uhid device's own task has for the manager.
#[derive(Debug)]
pub enum HidNotice {
Kernel(uhid::Event),
/// Look for the device's nodes again. They appear after the kernel says
/// the device started, not with it.
Discover {
attempt: u32,
},
}
/// How often, and for how long, to look for a started device's nodes.
const DISCOVER_EVERY: Duration = Duration::from_millis(20);
const DISCOVER_ATTEMPTS: u32 = 50;
/// A uinput device.
struct Gamepad {
device: Arc<Device>,
layout: Layout,
/// The devices announced for it, parents first.
records: Vec<Record>,
/// Reads the device for rumble. Stopped before the device goes.
task: tokio::task::JoinHandle<()>,
}
/// A device rebuilt as itself.
struct Replica {
model: Model,
vendor: u16,
product: u16,
device: Arc<uhid::Device>,
address: [u8; 6],
reporter: Arc<Mutex<Reporter>>,
/// The kernel's directory for it, once found.
syspath: Option<PathBuf>,
records: Vec<Record>,
/// Hears the kernel side, and keeps the reports coming. Stopped before
/// the device goes.
tasks: [tokio::task::JoinHandle<()>; 2],
/// The rumble last passed on. Games write the report that carries it for
/// the lights as well, often, and only a change is worth sending.
rumble: (u16, u16),
}
struct Pad {
identity: PadIdentity,
replica: Option<Replica>,
gamepad: Option<Gamepad>,
/// Whether any input has arrived for it yet.
///
/// Said once, at info: from a log alone, a controller that is plugged in
/// and never moves is otherwise indistinguishable from one whose input
/// reaches a device no game is reading.
moved: bool,
}
pub struct Pads {
udev: Option<Udev>,
pads: HashMap<Key, Pad>,
/// Slots already asked to re-announce, so input arriving for one the box
/// does not have asks once rather than once per report.
asked: HashSet<Key>,
feedback: UnboundedSender<Feedback>,
hid: UnboundedSender<(Key, HidNotice)>,
}
impl Pads {
pub fn new(
udev: Option<Udev>,
feedback: UnboundedSender<Feedback>,
hid: UnboundedSender<(Key, HidNotice)>,
) -> Self {
Self {
udev,
pads: HashMap::new(),
asked: HashSet::new(),
feedback,
hid,
}
}
pub fn handle(&mut self, session: u32, message: PadMessage) {
match message {
PadMessage::Connect { slot, identity } => self.connect((session, slot), identity),
PadMessage::State { slot, state } => self.state((session, slot), state),
PadMessage::Disconnect { slot } => self.remove((session, slot)),
PadMessage::SessionEnd => self.end_session(session),
}
}
fn state(&mut self, key: Key, state: PadState) {
let (session, slot) = key;
let Some(pad) = self.present(key) else { return };
trace!(session, slot, ?state, "state");
if let Some(replica) = &pad.replica {
// Now rather than at the next tick of its clock, which would add
// up to a tick of latency to every press.
let report = {
let mut reporter = replica.reporter.lock().unwrap_or_else(|e| e.into_inner());
reporter.set(state);
reporter.next()
};
if let Err(e) = replica.device.input(&report) {
// Per state, so debug: a device that stopped taking writes
// fails every one of them.
debug!(session, slot, "could not write a report: {e}");
}
}
if let Some(gamepad) = &pad.gamepad
&& let Err(e) = gamepad.device.write(&gamepad.layout.events(&state))
{
debug!(session, slot, "could not write to the device: {e}");
}
}
/// The pad in `key`, noting its first input, or asking the client to
/// announce it when there is none.
fn present(&mut self, key: Key) -> Option<&mut Pad> {
let (session, slot) = key;
if !self.pads.contains_key(&key) {
if self.asked.insert(key) {
debug!(
session,
slot, "input for a controller not here; asking for it"
);
let _ = self
.feedback
.send((session, PadFeedback::Announce { slot }));
}
return None;
}
let pad = self.pads.get_mut(&key)?;
if !pad.moved {
pad.moved = true;
info!(session, slot, "first input from the controller");
}
Some(pad)
}
fn connect(&mut self, key: Key, identity: PadIdentity) {
let (session, slot) = key;
if self
.pads
.get(&key)
.is_some_and(|pad| pad.identity == identity)
{
// A re-announce of what is already here, which a client does
// after asking and is harmless.
return;
}
self.asked.remove(&key);
self.remove(key);
if self.pads.len() >= MAX_PADS {
warn!(
session,
slot,
"{MAX_PADS} controllers are already plugged in; not adding \"{}\"",
identity.name
);
return;
}
info!(
session,
slot,
name = identity.name,
id = format!("{:04x}:{:04x}", identity.vendor, identity.product),
"controller connected"
);
let mut pad = Pad {
identity,
replica: None,
gamepad: None,
moved: false,
};
let template = template::for_identity(&pad.identity);
debug!(
session,
slot,
template = template.describe(),
"template chosen"
);
match template {
// Its gamepad follows once its own nodes exist, so that a game
// settling on the first controller it finds finds the device.
Template::Replica { model, product } => match self.rebuild(key, model, product) {
Ok(replica) => pad.replica = Some(replica),
Err(e) => {
warn!(
session,
slot,
"could not rebuild \"{}\" as itself, so it goes as a gamepad alone: {e}",
pad.identity.name
);
pad.gamepad = self.plug(key, layout::generic(&pad.identity));
}
},
Template::Gamepad(layout) => pad.gamepad = self.plug(key, layout),
}
self.pads.insert(key, pad);
}
fn rebuild(&self, key: Key, model: Model, product: u16) -> std::io::Result<Replica> {
let (session, slot) = key;
let address = replica::address(session, slot);
let uniq = replica::uniq(address);
let spec = model.spec(product, &uniq);
let device = Arc::new(uhid::Device::create(&spec)?);
info!(
session,
slot,
model = ?model,
device = format!("{} {:04x}:{:04x}", spec.name, spec.vendor, spec.product),
"controller plugged in as itself"
);
let reporter = Arc::new(Mutex::new(Reporter::new(model)));
let tasks = [
spawn_hid(device.clone(), key, self.hid.clone()),
spawn_clock(device.clone(), reporter.clone(), model.report_every()),
];
Ok(Replica {
model,
vendor: spec.vendor,
product: spec.product,
device,
address,
reporter,
syspath: None,
records: Vec::new(),
tasks,
rumble: (0, 0),
})
}
/// A uinput device for `key`, or none when it cannot be made -- said, and
/// the controller left without it.
fn plug(&self, key: Key, layout: Layout) -> Option<Gamepad> {
let (session, slot) = key;
match self.make_gamepad(key, &layout) {
Ok((device, records)) => {
info!(
session,
slot,
driver = layout.driver,
device = format!(
"{} {:04x}:{:04x}:{:04x}",
layout.name, layout.vendor, layout.product, layout.version
),
node = records
.last()
.and_then(|r| r.properties.get("DEVNAME"))
.map(String::as_str)
.unwrap_or("none"),
"controller plugged in as a gamepad"
);
let task = spawn_rumble(device.clone(), session, slot, self.feedback.clone());
Some(Gamepad {
device,
layout,
records,
task,
})
}
Err(e) => {
warn!(
session,
slot, "could not create a gamepad for \"{}\": {e:#}", layout.name
);
None
}
}
}
fn make_gamepad(
&self,
key: Key,
layout: &Layout,
) -> anyhow::Result<(Arc<Device>, Vec<Record>)> {
let keys: Vec<u16> = layout.buttons.iter().map(|&(_, key)| key).collect();
let device = Device::create(&Spec {
name: &layout.name,
bus: layout.bus,
vendor: layout.vendor,
product: layout.product,
version: layout.version,
keys: &keys,
axes: &layout.axes(),
})?;
let syspath = device.syspath();
let event = event_node(&syspath)?;
open_up(&Path::new("/dev/input").join(event.file_name().unwrap()));
let extra = classification(layout, key);
let records = vec![
Record::read(&syspath, "input", &extra)?,
Record::read(&event, "input", &extra)?,
];
self.announce(&records)?;
Ok((Arc::new(device), records))
}
fn announce(&self, records: &[Record]) -> std::io::Result<()> {
match &self.udev {
Some(udev) => {
for record in records {
udev.add(record)?;
}
}
None => debug!("no udev stand-in; the device exists but nothing is told"),
}
Ok(())
}
/// Something a uhid device's task passed on.
pub fn hid_notice(&mut self, key: Key, notice: HidNotice) {
let (session, slot) = key;
let Some(replica) = self.pads.get(&key).and_then(|p| p.replica.as_ref()) else {
// Unplugged since the task said it; nothing is waiting on it.
return;
};
let (model, address, device, reporter) = (
replica.model,
replica.address,
replica.device.clone(),
replica.reporter.clone(),
);
match notice {
HidNotice::Kernel(uhid::Event::Start) => {
debug!(session, slot, "a driver took the device");
self.schedule_discovery(key, 0);
}
HidNotice::Discover { attempt } => self.discover_nodes(key, attempt),
HidNotice::Kernel(uhid::Event::Stop) => {
debug!(session, slot, "the driver let go of the device");
}
HidNotice::Kernel(uhid::Event::Open | uhid::Event::Close) => {}
HidNotice::Kernel(uhid::Event::Output { kind, data }) => {
trace!(session, slot, kind, len = data.len(), "output report");
self.rumble_from(key, &data);
}
HidNotice::Kernel(uhid::Event::GetReport { id, number, kind }) => {
let answer = match kind {
uhid::REPORT_FEATURE => model.feature(number, address),
uhid::REPORT_INPUT => {
let current = reporter.lock().unwrap_or_else(|e| e.into_inner()).current();
(current.first() == Some(&number)).then_some(current)
}
_ => None,
};
// Some software asks for every report a descriptor declares,
// and the real device answers only some: an unknown one is
// expected, not a fault.
debug!(
session,
slot,
number = format!("{number:#04x}"),
kind,
answered = answer.is_some(),
"a report was asked for"
);
let result = match answer {
Some(data) => device.get_report_reply(id, 0, &data),
None => device.get_report_reply(id, libc::EIO as u16, &[]),
};
if let Err(e) = result {
debug!(session, slot, id, "could not answer a request: {e}");
}
}
HidNotice::Kernel(uhid::Event::SetReport {
id,
number,
kind,
data,
}) => {
trace!(session, slot, id, number, kind, "set report");
if let Err(e) = device.set_report_reply(id, 0) {
debug!(session, slot, id, "could not answer a request: {e}");
}
if kind == uhid::REPORT_OUTPUT {
self.rumble_from(key, &data);
}
}
}
}
/// Pass on the rumble a report written to a replica asks for.
fn rumble_from(&mut self, key: Key, report: &[u8]) {
let Some(replica) = self.pads.get_mut(&key).and_then(|p| p.replica.as_mut()) else {
return;
};
let Some(rumble) = replica.model.rumble(report) else {
return;
};
if rumble == replica.rumble {
return;
}
replica.rumble = rumble;
let (strong, weak) = rumble;
let _ = self.feedback.send((
key.0,
PadFeedback::Rumble {
slot: key.1,
strong,
weak,
duration_ms: 0,
},
));
}
fn discover_nodes(&mut self, key: Key, attempt: u32) {
let (session, slot) = key;
let Some(pad) = self.pads.get(&key) else {
return;
};
let Some(Replica {
syspath: None,
vendor,
product,
..
}) = pad.replica
else {
return;
};
let name = pad.identity.name.clone();
let claimed: HashSet<PathBuf> = self
.pads
.values()
.filter_map(|p| p.replica.as_ref()?.syspath.clone())
.collect();
match discover(code::BUS_USB, vendor, product, &claimed) {
Some((found, nodes)) => self.nodes_found(key, found, nodes),
None if attempt + 1 < DISCOVER_ATTEMPTS => self.schedule_discovery(key, attempt + 1),
None => {
warn!(
session,
slot,
"the kernel made no nodes for \"{name}\", so only its gamepad can be read"
);
self.plug_copy(key);
}
}
}
fn schedule_discovery(&self, key: Key, attempt: u32) {
let tx = self.hid.clone();
tokio::spawn(async move {
if attempt > 0 {
tokio::time::sleep(DISCOVER_EVERY).await;
}
let _ = tx.send((key, HidNotice::Discover { attempt }));
});
}
fn nodes_found(&mut self, key: Key, found: PathBuf, nodes: Nodes) {
let (session, slot) = key;
for node in nodes.dev_nodes() {
open_up(&node);
}
let mut records = Vec::new();
let joystick = [
("ID_INPUT", "1".to_owned()),
("ID_INPUT_JOYSTICK", "1".to_owned()),
];
let mut read = |path: &Path, subsystem: &str, extra: &[(&str, String)]| match Record::read(
path, subsystem, extra,
) {
Ok(record) => records.push(record),
Err(e) => debug!("could not describe {}: {e}", path.display()),
};
for hidraw in &nodes.hidraw {
read(hidraw, "hidraw", &[]);
}
for (input, events) in &nodes.inputs {
read(input, "input", &joystick);
for event in events {
read(event, "input", &joystick);
}
}
if let Err(e) = self.announce(&records) {
warn!(session, slot, "could not announce the device: {e}");
}
info!(
session,
slot,
nodes = nodes
.dev_nodes()
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(" "),
"controller nodes ready"
);
if let Some(replica) = self.pads.get_mut(&key).and_then(|p| p.replica.as_mut()) {
replica.records = records;
replica.syspath = Some(found);
}
self.plug_copy(key);
}
/// The gamepad beside a replica, under a neutral identity (see the module
/// docs).
fn plug_copy(&mut self, key: Key) {
let Some(pad) = self.pads.get(&key) else {
return;
};
if pad.gamepad.is_some() {
return;
}
let gamepad = self.plug(key, layout::generic(&pad.identity));
if let Some(pad) = self.pads.get_mut(&key) {
pad.gamepad = gamepad;
}
}
fn remove(&mut self, key: Key) {
let Some(pad) = self.pads.remove(&key) else {
return;
};
let mut records = Vec::new();
// Destroyed explicitly, before anyone is told it is gone. Dropping the
// handle would not do it: an aborted task holds another, and lets go
// of it only when the runtime next gets round to dropping the task.
if let Some(gamepad) = pad.gamepad {
gamepad.task.abort();
gamepad.device.destroy();
records.extend(gamepad.records.into_iter().rev());
}
if let Some(replica) = pad.replica {
for task in &replica.tasks {
task.abort();
}
replica.device.destroy();
records.extend(replica.records.into_iter().rev());
}
if let Some(udev) = &self.udev {
for record in &records {
if let Err(e) = udev.remove(record) {
debug!("could not announce {} gone: {e}", record.devpath);
}
}
}
info!(
session = key.0,
slot = key.1,
"controller unplugged: {}",
pad.identity.name
);
}
fn end_session(&mut self, session: u32) {
let keys: Vec<Key> = self
.pads
.keys()
.filter(|k| k.0 == session)
.copied()
.collect();
for key in keys {
self.remove(key);
}
self.asked.retain(|k| k.0 != session);
}
/// Unplug everything. The hub has gone, and with it every client.
pub fn clear(&mut self) {
let keys: Vec<Key> = self.pads.keys().copied().collect();
for key in keys {
self.remove(key);
}
self.asked.clear();
}
}
/// Open a node to the workload.
///
/// devtmpfs creates it root-only, and the workload is not root. `nesinit`
/// opens up the box's other device nodes the same way, for the same reason:
/// the virtual machine is the boundary.
///
/// A failure is a warning rather than no controller: the device still exists,
/// a workload running as root can still use it, and the line says why one
/// that is not cannot.
fn open_up(node: &Path) {
if let Err(e) =
std::fs::set_permissions(node, std::os::unix::fs::PermissionsExt::from_mode(0o666))
{
warn!(
"could not open up {} to the workload, so only root can read this controller: {e}",
node.display()
);
}
}
/// The `eventN` node the kernel attached to an input device.
fn event_node(syspath: &Path) -> anyhow::Result<PathBuf> {
for entry in std::fs::read_dir(syspath)? {
let entry = entry?;
if entry.file_name().to_string_lossy().starts_with("event") {
return Ok(entry.path());
}
}
anyhow::bail!("{} has no event node", syspath.display())
}
/// Where uhid devices appear in sysfs.
const UHID_SYSFS: &str = "/sys/devices/virtual/misc/uhid";
/// The nodes a driver made for one HID device, as sysfs paths.
#[derive(Debug, Default)]
pub(crate) struct Nodes {
pub(crate) hidraw: Vec<PathBuf>,
/// Each input device, with its event nodes.
inputs: Vec<(PathBuf, Vec<PathBuf>)>,
}
impl Nodes {
fn dev_nodes(&self) -> Vec<PathBuf> {
let dev =
|path: &PathBuf, dir: &str| Path::new(dir).join(path.file_name().unwrap_or_default());
let mut nodes: Vec<PathBuf> = self.hidraw.iter().map(|h| dev(h, "/dev")).collect();
for (_, events) in &self.inputs {
nodes.extend(events.iter().map(|e| dev(e, "/dev/input")));
}
nodes
}
}
/// Find the kernel's directory for a uhid device with this identity, and what
/// was made under it -- `None` until the hidraw node exists, which is the node
/// that matters.
///
/// The kernel names the directory `BUS:VENDOR:PRODUCT.INSTANCE`, and several
/// devices can share everything but the instance, so one already claimed by
/// another controller is skipped. Two identical devices started at the same
/// moment could be found the other way round, which costs nothing: the same
/// nodes are opened up and announced either way.
pub(crate) fn discover(
bus: u16,
vendor: u16,
product: u16,
claimed: &HashSet<PathBuf>,
) -> Option<(PathBuf, Nodes)> {
let prefix = format!("{bus:04X}:{vendor:04X}:{product:04X}.");
let mut candidates: Vec<PathBuf> = std::fs::read_dir(UHID_SYSFS)
.ok()?
.filter_map(Result::ok)
.filter(|e| e.file_name().to_string_lossy().starts_with(&prefix))
.map(|e| e.path())
.filter(|p| !claimed.contains(p))
.collect();
candidates.sort();
// The newest first: instances count up.
let dir = candidates.pop()?;
let nodes = nodes_under(&dir);
if nodes.hidraw.is_empty() {
return None;
}
Some((dir, nodes))
}
fn nodes_under(dir: &Path) -> Nodes {
let children = |path: &Path, prefix: &str| -> Vec<PathBuf> {
let mut found: Vec<PathBuf> = std::fs::read_dir(path)
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter(|e| e.file_name().to_string_lossy().starts_with(prefix))
.map(|e| e.path())
.collect();
found.sort();
found
};
let mut nodes = Nodes {
hidraw: children(&dir.join("hidraw"), "hidraw"),
..Nodes::default()
};
for input in children(&dir.join("input"), "input") {
let events = children(&input, "event");
nodes.inputs.push((input, events));
}
nodes
}
/// What udev's rules would have added: that this is a joystick, and, where
/// the device carries a real identity, which one.
fn classification(layout: &Layout, (session, slot): Key) -> Vec<(&'static str, String)> {
let mut extra = vec![
("ID_INPUT", "1".to_owned()),
("ID_INPUT_JOYSTICK", "1".to_owned()),
];
if let Some(bus) = layout.udev_bus() {
let serial = layout.name.replace(' ', "_");
extra.extend([
("ID_BUS", bus.to_owned()),
("ID_VENDOR_ID", format!("{:04x}", layout.vendor)),
("ID_MODEL_ID", format!("{:04x}", layout.product)),
// Unique per device, as real serials are, so two identical
// controllers are not taken for one.
("ID_SERIAL", format!("{serial}_{session}_{slot}")),
]);
}
extra
}
/// A descriptor, for the reactor.
struct Readable<T>(Arc<T>);
impl<T: AsRawFd> AsRawFd for Readable<T> {
fn as_raw_fd(&self) -> RawFd {
self.0.as_raw_fd()
}
}
/// Pass everything a uhid device's kernel side says to the manager.
fn spawn_hid(
device: Arc<uhid::Device>,
key: Key,
tx: UnboundedSender<(Key, HidNotice)>,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let fd = match AsyncFd::new(Readable(device)) {
Ok(fd) => fd,
Err(e) => {
warn!(
session = key.0,
slot = key.1,
"cannot hear the device's kernel side: {e}"
);
return;
}
};
loop {
let mut guard = match fd.readable().await {
Ok(guard) => guard,
Err(e) => {
debug!(
session = key.0,
slot = key.1,
"device stopped being readable: {e}"
);
return;
}
};
let events = match guard.get_inner().0.drain() {
Ok(events) => events,
Err(e) => {
debug!(
session = key.0,
slot = key.1,
"reading the device failed: {e}"
);
return;
}
};
guard.clear_ready();
for event in events {
if tx.send((key, HidNotice::Kernel(event))).is_err() {
return;
}
}
}
})
}
/// Keep a replica's reports coming at the rate the real device sends them,
/// whether or not anything changed.
fn spawn_clock(
device: Arc<uhid::Device>,
reporter: Arc<Mutex<Reporter>>,
every: Duration,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let mut tick = tokio::time::interval(every);
// A late tick is a report that never happened, not one owed.
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tick.tick().await;
let report = reporter.lock().unwrap_or_else(|e| e.into_inner()).next();
if device.input(&report).is_err() {
// Only ever a device on its way out; its removal stops this.
continue;
}
}
})
}
/// Read a uinput device for what games ask of it, and turn that into rumble
/// for the client.
///
/// A game uploads an effect once and then plays and stops it by id, so the
/// effects are kept here to know what "play 3" means. Only the most recently
/// started one is sent: a real controller has one pair of motors, and the
/// client has no way to mix several.
fn spawn_rumble(
device: Arc<Device>,
session: u32,
slot: u8,
tx: UnboundedSender<Feedback>,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let fd = match AsyncFd::new(Readable(device)) {
Ok(fd) => fd,
Err(e) => {
warn!(session, slot, "rumble unavailable: {e}");
return;
}
};
let mut effects: HashMap<i16, (u16, u16, u16)> = HashMap::new();
let mut playing: Option<i16> = None;
let send = |strong, weak, duration_ms| {
let _ = tx.send((
session,
PadFeedback::Rumble {
slot,
strong,
weak,
duration_ms,
},
));
};
loop {
let mut guard = match fd.readable().await {
Ok(guard) => guard,
Err(e) => {
debug!(session, slot, "device stopped being readable: {e}");
return;
}
};
let requests = match guard.get_inner().0.drain() {
Ok(requests) => requests,
Err(e) => {
debug!(session, slot, "reading the device failed: {e}");
return;
}
};
guard.clear_ready();
for request in requests {
trace!(session, slot, ?request, "from a game");
match request {
Request::Upload(effect) => {
let Some((strong, weak)) = effect.rumble() else {
continue;
};
let length = effect.length_ms();
effects.insert(effect.id, (strong, weak, length));
// Replacing the effect that is playing changes what
// the motors are doing now, not only next time.
if playing == Some(effect.id) {
send(strong, weak, length);
}
}
Request::Erase(id) => {
effects.remove(&id);
if playing == Some(id) {
playing = None;
send(0, 0, 0);
}
}
Request::Play { id, on: true } => {
if let Some(&(strong, weak, length)) = effects.get(&id) {
playing = Some(id);
send(strong, weak, length);
}
}
Request::Play { id, on: false } => {
if playing == Some(id) {
playing = None;
send(0, 0, 0);
}
}
}
}
}
})
}
+201
View File
@@ -0,0 +1,201 @@
//! Controllers the box rebuilds as the device itself, through `/dev/uhid`.
//!
//! 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; Proton hands
//! such a controller to Wine as the raw device when it has a hidraw node, and
//! only then. For the families here, the box builds that device from nothing
//! but the controller's identity and its state: the real one's report
//! descriptor, and its reports written from the positional snapshot the client
//! sends. What a game asks of the device -- calibration, firmware, pairing --
//! is answered here the way the real one answers, and rumble it writes goes
//! back to the client.
//!
//! What the snapshot cannot say, the replica reports as a controller at rest:
//! motion sensors still, touchpad untouched.
mod dualshock4;
use std::time::{Duration, Instant};
use nesprotocol::gamepad::PadState;
use crate::layout::SONY;
use crate::uhid;
use crate::uinput::code;
/// The DualShock 4 a Sony controller with no template of its own is
/// presented as.
pub const DUALSHOCK4_V2: u16 = 0x09cc;
/// A family the box can rebuild.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Model {
DualShock4,
}
/// Whether `product` is one of `model`'s.
pub fn is(model: Model, product: u16) -> bool {
model.products().iter().any(|&(p, _)| p == product)
}
impl Model {
fn products(self) -> &'static [(u16, &'static str)] {
match self {
Self::DualShock4 => dualshock4::PRODUCTS,
}
}
fn vendor(self) -> u16 {
match self {
Self::DualShock4 => SONY,
}
}
/// The name the device gives itself, which the client's platform may not
/// have passed on as it was.
pub fn name(self, product: u16) -> &'static str {
let products = self.products();
products
.iter()
.find(|&&(p, _)| p == product)
.map_or(products[0].1, |&(_, name)| name)
}
/// Everything the device is built from. `uniq` is its address as text
/// (see [`uniq`]).
pub fn spec(self, product: u16, uniq: &str) -> uhid::Spec<'_> {
let (version, descriptor) = match self {
Self::DualShock4 => (dualshock4::VERSION, dualshock4::DESCRIPTOR),
};
uhid::Spec {
name: self.name(product),
uniq,
bus: code::BUS_USB,
vendor: self.vendor(),
product,
version,
country: 0,
descriptor,
}
}
pub fn report_every(self) -> Duration {
match self {
Self::DualShock4 => dualshock4::REPORT_EVERY,
}
}
/// The answer to a feature report read, `None` for one this does not
/// know.
pub fn feature(self, number: u8, address: [u8; 6]) -> Option<Vec<u8>> {
match self {
Self::DualShock4 => dualshock4::feature(number, address),
}
}
/// Rumble a report written to the device asks for, as `(strong, weak)`.
pub fn rumble(self, report: &[u8]) -> Option<(u16, u16)> {
match self {
Self::DualShock4 => dualshock4::rumble(report),
}
}
}
/// The input reports of one device: the latest state, and the counters a
/// real device moves on every report.
pub struct Reporter {
model: Model,
state: PadState,
counter: u8,
started: Instant,
}
impl Reporter {
pub fn new(model: Model) -> Self {
Self {
model,
state: PadState::default(),
counter: 0,
started: Instant::now(),
}
}
pub fn set(&mut self, state: PadState) {
self.state = state;
}
/// The next report, for the state last set.
pub fn next(&mut self) -> Vec<u8> {
let counter = self.counter;
self.counter = self.counter.wrapping_add(1);
match self.model {
Model::DualShock4 => dualshock4::input_report(
&self.state,
counter,
dualshock4::sensor_ticks(self.started.elapsed()),
)
.to_vec(),
}
}
/// The report a read of the current input report gets, without moving
/// the counters.
pub fn current(&self) -> Vec<u8> {
match self.model {
Model::DualShock4 => dualshock4::input_report(
&self.state,
self.counter,
dualshock4::sensor_ticks(self.started.elapsed()),
)
.to_vec(),
}
}
}
/// A locally administered address, unique to one client's slot. Software
/// pairs a device's parts, and tells two identical devices apart, by it.
pub fn address(session: u32, slot: u8) -> [u8; 6] {
let s = session.to_be_bytes();
[0x02, s[0], s[1], s[2], s[3], slot]
}
/// An address as a device's unique string, the way Linux writes one.
pub fn uniq(address: [u8; 6]) -> String {
address
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(":")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_device_names_itself_as_the_real_one_does() {
let spec = Model::DualShock4.spec(0x09cc, "");
assert_eq!(
spec.name,
"Sony Interactive Entertainment Wireless Controller"
);
assert_eq!((spec.vendor, spec.product), (SONY, 0x09cc));
assert!(is(Model::DualShock4, 0x05c4));
assert!(!is(Model::DualShock4, 0x0ce6));
}
#[test]
fn every_slot_gets_its_own_address() {
assert_ne!(address(1, 0), address(1, 1));
assert_ne!(address(1, 0), address(2, 0));
assert_eq!(uniq(address(0x0102_0304, 5)), "02:01:02:03:04:05");
}
#[test]
fn the_counter_moves_with_every_report() {
let mut reporter = Reporter::new(Model::DualShock4);
let first = reporter.next();
let second = reporter.next();
assert_eq!(second[7] >> 2, (first[7] >> 2) + 1);
}
}
+309
View File
@@ -0,0 +1,309 @@
//! The DualShock 4, over USB.
//!
//! Everything here was read from a DualShock 4 v2 (`054c:09cc`) on a cable:
//! its report descriptor byte for byte, its calibration and firmware reports,
//! and the layout of the input report it sends. The layout matches what
//! Linux's hid-playstation and SDL's HIDAPI driver parse, which between them
//! are what games reading the device by hand were written against.
use nesprotocol::gamepad::{PadState, button};
/// Products that are this controller: the original, the v2, and Sony's
/// wireless adapter, which presents whichever DualShock 4 is paired to it as
/// the same device.
pub const PRODUCTS: &[(u16, &str)] = &[
(0x05c4, "Sony Computer Entertainment Wireless Controller"),
(0x09cc, "Sony Interactive Entertainment Wireless Controller"),
(
0x0ba0,
"Sony Interactive Entertainment DUALSHOCK\u{ae}4 USB Wireless Adaptor",
),
];
/// The HID version the device's descriptor gives, which Linux reports as the
/// device's version.
pub const VERSION: u16 = 0x0111;
#[rustfmt::skip]
pub const DESCRIPTOR: &[u8] = &[
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31,
0x09, 0x32, 0x09, 0x35, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95,
0x04, 0x81, 0x02, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46,
0x3b, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00,
0x05, 0x09, 0x19, 0x01, 0x29, 0x0e, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
0x95, 0x0e, 0x81, 0x02, 0x06, 0x00, 0xff, 0x09, 0x20, 0x75, 0x06, 0x95,
0x01, 0x15, 0x00, 0x25, 0x7f, 0x81, 0x02, 0x05, 0x01, 0x09, 0x33, 0x09,
0x34, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02,
0x06, 0x00, 0xff, 0x09, 0x21, 0x95, 0x36, 0x81, 0x02, 0x85, 0x05, 0x09,
0x22, 0x95, 0x1f, 0x91, 0x02, 0x85, 0x04, 0x09, 0x23, 0x95, 0x24, 0xb1,
0x02, 0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xb1, 0x02, 0x85, 0x08, 0x09,
0x25, 0x95, 0x03, 0xb1, 0x02, 0x85, 0x10, 0x09, 0x26, 0x95, 0x04, 0xb1,
0x02, 0x85, 0x11, 0x09, 0x27, 0x95, 0x02, 0xb1, 0x02, 0x85, 0x12, 0x06,
0x02, 0xff, 0x09, 0x21, 0x95, 0x0f, 0xb1, 0x02, 0x85, 0x13, 0x09, 0x22,
0x95, 0x16, 0xb1, 0x02, 0x85, 0x14, 0x06, 0x05, 0xff, 0x09, 0x20, 0x95,
0x10, 0xb1, 0x02, 0x85, 0x15, 0x09, 0x21, 0x95, 0x2c, 0xb1, 0x02, 0x06,
0x80, 0xff, 0x85, 0x80, 0x09, 0x20, 0x95, 0x06, 0xb1, 0x02, 0x85, 0x81,
0x09, 0x21, 0x95, 0x06, 0xb1, 0x02, 0x85, 0x82, 0x09, 0x22, 0x95, 0x05,
0xb1, 0x02, 0x85, 0x83, 0x09, 0x23, 0x95, 0x01, 0xb1, 0x02, 0x85, 0x84,
0x09, 0x24, 0x95, 0x04, 0xb1, 0x02, 0x85, 0x85, 0x09, 0x25, 0x95, 0x06,
0xb1, 0x02, 0x85, 0x86, 0x09, 0x26, 0x95, 0x06, 0xb1, 0x02, 0x85, 0x87,
0x09, 0x27, 0x95, 0x23, 0xb1, 0x02, 0x85, 0x88, 0x09, 0x28, 0x95, 0x3f,
0xb1, 0x02, 0x85, 0x89, 0x09, 0x29, 0x95, 0x02, 0xb1, 0x02, 0x85, 0x90,
0x09, 0x30, 0x95, 0x05, 0xb1, 0x02, 0x85, 0x91, 0x09, 0x31, 0x95, 0x03,
0xb1, 0x02, 0x85, 0x92, 0x09, 0x32, 0x95, 0x03, 0xb1, 0x02, 0x85, 0x93,
0x09, 0x33, 0x95, 0x0c, 0xb1, 0x02, 0x85, 0x94, 0x09, 0x34, 0x95, 0x3f,
0xb1, 0x02, 0x85, 0xa0, 0x09, 0x40, 0x95, 0x06, 0xb1, 0x02, 0x85, 0xa1,
0x09, 0x41, 0x95, 0x01, 0xb1, 0x02, 0x85, 0xa2, 0x09, 0x42, 0x95, 0x01,
0xb1, 0x02, 0x85, 0xa3, 0x09, 0x43, 0x95, 0x30, 0xb1, 0x02, 0x85, 0xa4,
0x09, 0x44, 0x95, 0x0d, 0xb1, 0x02, 0x85, 0xf0, 0x09, 0x47, 0x95, 0x3f,
0xb1, 0x02, 0x85, 0xf1, 0x09, 0x48, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xf2,
0x09, 0x49, 0x95, 0x0f, 0xb1, 0x02, 0x85, 0xa7, 0x09, 0x4a, 0x95, 0x01,
0xb1, 0x02, 0x85, 0xa8, 0x09, 0x4b, 0x95, 0x01, 0xb1, 0x02, 0x85, 0xa9,
0x09, 0x4c, 0x95, 0x08, 0xb1, 0x02, 0x85, 0xaa, 0x09, 0x4e, 0x95, 0x01,
0xb1, 0x02, 0x85, 0xab, 0x09, 0x4f, 0x95, 0x39, 0xb1, 0x02, 0x85, 0xac,
0x09, 0x50, 0x95, 0x39, 0xb1, 0x02, 0x85, 0xad, 0x09, 0x51, 0x95, 0x0b,
0xb1, 0x02, 0x85, 0xae, 0x09, 0x52, 0x95, 0x01, 0xb1, 0x02, 0x85, 0xaf,
0x09, 0x53, 0x95, 0x02, 0xb1, 0x02, 0x85, 0xb0, 0x09, 0x54, 0x95, 0x3f,
0xb1, 0x02, 0x85, 0xe0, 0x09, 0x57, 0x95, 0x02, 0xb1, 0x02, 0x85, 0xb3,
0x09, 0x55, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xb4, 0x09, 0x55, 0x95, 0x3f,
0xb1, 0x02, 0x85, 0xb5, 0x09, 0x56, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xd0,
0x09, 0x58, 0x95, 0x3f, 0xb1, 0x02, 0x85, 0xd4, 0x09, 0x59, 0x95, 0x3f,
0xb1, 0x02, 0xc0,
];
/// Input report 0x01, the one the device sends continuously.
pub const INPUT_LEN: usize = 64;
/// How often the device sends it. A real one never goes quiet while plugged
/// in, and software that reads it by hand counts on the counter and the
/// sensor clock moving.
pub const REPORT_EVERY: std::time::Duration = std::time::Duration::from_millis(4);
/// The sensor clock ticks every 16/3 µs.
pub fn sensor_ticks(elapsed: std::time::Duration) -> u16 {
(elapsed.as_micros() * 3 / 16) as u16
}
/// A stick axis in the device's byte, from the wire's full range. Both have
/// down as positive.
fn stick(value: i16) -> u8 {
((i32::from(value) + 32768) >> 8) as u8
}
/// The d-pad as the device's hat: 0 is up, counting clockwise in eighths, 8
/// is released. Opposite directions held together cancel, as on the real
/// d-pad they cannot both be.
fn hat(buttons: u32) -> u8 {
let held = |bit| i8::from(buttons & bit != 0);
let x = held(button::DPAD_RIGHT) - held(button::DPAD_LEFT);
let y = held(button::DPAD_DOWN) - held(button::DPAD_UP);
match (x, y) {
(0, -1) => 0,
(1, -1) => 1,
(1, 0) => 2,
(1, 1) => 3,
(0, 1) => 4,
(-1, 1) => 5,
(-1, 0) => 6,
(-1, -1) => 7,
_ => 8,
}
}
/// The input report for `state`. `counter` wraps at 64; `clock` is the
/// sensor clock (see [`sensor_ticks`]).
pub fn input_report(state: &PadState, counter: u8, clock: u16) -> [u8; INPUT_LEN] {
let b = state.buttons;
let bit = |mask, value| if b & mask != 0 { value } else { 0 };
let left_trigger = (state.left_trigger >> 8) as u8;
let right_trigger = (state.right_trigger >> 8) as u8;
let mut r = [0u8; INPUT_LEN];
r[0] = 0x01;
r[1] = stick(state.left_x);
r[2] = stick(state.left_y);
r[3] = stick(state.right_x);
r[4] = stick(state.right_y);
r[5] = hat(b)
| bit(button::WEST, 0x10)
| bit(button::SOUTH, 0x20)
| bit(button::EAST, 0x40)
| bit(button::NORTH, 0x80);
// The trigger bits are set by any pull at all, as the device sets them.
let l2 = if b & button::LEFT_TRIGGER != 0 || left_trigger > 0 {
0x04
} else {
0
};
let r2 = if b & button::RIGHT_TRIGGER != 0 || right_trigger > 0 {
0x08
} else {
0
};
r[6] = bit(button::LEFT_SHOULDER, 0x01)
| bit(button::RIGHT_SHOULDER, 0x02)
| l2
| r2
| bit(button::SELECT, 0x10)
| bit(button::START, 0x20)
| bit(button::LEFT_STICK, 0x40)
| bit(button::RIGHT_STICK, 0x80);
r[7] = bit(button::MODE, 0x01) | ((counter & 0x3f) << 2);
r[8] = left_trigger;
r[9] = right_trigger;
r[10..12].copy_from_slice(&clock.to_le_bytes());
// Gyro still, and the accelerometer reading one g on the axis gravity
// pulls along with the controller lying flat: a controller at rest, which
// is all the client can say about its motion.
r[21..23].copy_from_slice(&8192i16.to_le_bytes());
// On the cable, battery full.
r[30] = 0x1b;
// One touch report, both fingers lifted: the high bit of a point's
// first byte marks it inactive.
r[33] = 1;
r[35] = 0x80;
r[39] = 0x80;
r
}
/// Feature 0x02: the motion sensors' calibration.
#[rustfmt::skip]
const CALIBRATION: &[u8] = &[
0x02, 0x03, 0x00, 0x06, 0x00, 0xfe, 0xff, 0x12, 0x22, 0x34, 0xde, 0xc5,
0x22, 0x51, 0xdd, 0x6d, 0x24, 0x48, 0xdb, 0x1c, 0x02, 0x1c, 0x02, 0xd6,
0x1f, 0x29, 0xe0, 0x69, 0x20, 0x96, 0xdf, 0xd9, 0x1f, 0x27, 0xe0, 0x02,
0x00,
];
/// Feature 0xa3: firmware build date and versions.
#[rustfmt::skip]
const FIRMWARE: &[u8] = &[
0xa3, 0x53, 0x65, 0x70, 0x20, 0x32, 0x31, 0x20, 0x32, 0x30, 0x31, 0x38,
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x34, 0x3a, 0x35, 0x30, 0x3a, 0x35,
0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
0xb4, 0x01, 0x00, 0x00, 0x00, 0x0a, 0xa0, 0x10, 0x20, 0x00, 0xa0, 0x02,
0x00,
];
/// The answer to a feature report read, for the controller at `address`.
pub fn feature(number: u8, address: [u8; 6]) -> Option<Vec<u8>> {
// Addresses go over the wire lowest byte first.
let mut reversed = address;
reversed.reverse();
match number {
0x02 => Some(CALIBRATION.to_vec()),
0xa3 => Some(FIRMWARE.to_vec()),
// Its own address.
0x81 => Some([&[0x81][..], &reversed].concat()),
// Pairing: its own address, a fixed class, and the host it is paired
// with, which on a cable is none.
0x12 => Some([&[0x12][..], &reversed, &[0x08, 0x25, 0x00], &[0; 6]].concat()),
_ => None,
}
}
/// Rumble from output report 0x05: `(strong, weak)`, when the report sets it.
/// The left motor is the heavy one.
pub fn rumble(report: &[u8]) -> Option<(u16, u16)> {
const SETS_MOTORS: u8 = 0x01;
match report {
[0x05, flags, _, _, weak, strong, ..] if flags & SETS_MOTORS != 0 => {
Some((u16::from(*strong) * 257, u16::from(*weak) * 257))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_descriptor_declares_what_the_reports_are() {
// Input 0x01 is 63 bytes after its id, output 0x05 31.
let d = DESCRIPTOR;
assert_eq!(d.len(), 507);
assert_eq!(&d[..6], &[0x05, 0x01, 0x09, 0x05, 0xa1, 0x01]);
assert!(d.windows(4).any(|w| w == [0x85, 0x05, 0x09, 0x22]));
}
#[test]
fn a_controller_at_rest_reads_as_one() {
// Compared with a report from the real device lying still.
let r = input_report(&PadState::default(), 0, 0);
assert_eq!(&r[..10], &[0x01, 0x80, 0x80, 0x80, 0x80, 0x08, 0, 0, 0, 0]);
assert_eq!(r[30], 0x1b);
}
#[test]
fn every_button_lands_on_its_bit() {
let all = PadState {
buttons: !(button::DPAD_UP
| button::DPAD_DOWN
| button::DPAD_LEFT
| button::DPAD_RIGHT),
..PadState::default()
};
let r = input_report(&all, 0, 0);
assert_eq!((r[5], r[6], r[7]), (0xf8, 0xff, 0x01));
}
#[test]
fn the_hat_goes_round_clockwise_from_up() {
use button::*;
for (held, value) in [
(DPAD_UP, 0),
(DPAD_UP | DPAD_RIGHT, 1),
(DPAD_RIGHT, 2),
(DPAD_DOWN | DPAD_RIGHT, 3),
(DPAD_DOWN, 4),
(DPAD_DOWN | DPAD_LEFT, 5),
(DPAD_LEFT, 6),
(DPAD_UP | DPAD_LEFT, 7),
(DPAD_UP | DPAD_DOWN, 8),
] {
assert_eq!(hat(held), value, "{held:#x}");
}
}
#[test]
fn sticks_and_triggers_reach_both_ends() {
let pushed = PadState {
left_x: i16::MIN,
left_y: i16::MAX,
right_trigger: u16::MAX,
..PadState::default()
};
let r = input_report(&pushed, 63, 0x1234);
assert_eq!((r[1], r[2], r[9]), (0, 255, 255));
assert_eq!(r[6] & 0x08, 0x08);
assert_eq!(r[7] >> 2, 63);
assert_eq!(&r[10..12], &[0x34, 0x12]);
}
#[test]
fn reports_carry_the_address_they_were_given() {
let address = [0xa0, 0xab, 0x51, 0x0b, 0xad, 0x42];
assert_eq!(
feature(0x81, address).unwrap(),
[0x81, 0x42, 0xad, 0x0b, 0x51, 0xab, 0xa0]
);
let pairing = feature(0x12, address).unwrap();
assert_eq!(pairing.len(), 16);
assert_eq!(&pairing[1..7], &[0x42, 0xad, 0x0b, 0x51, 0xab, 0xa0]);
assert_eq!(feature(0x02, address).unwrap().len(), 37);
assert_eq!(feature(0xa3, address).unwrap().len(), 49);
assert_eq!(feature(0x04, address), None);
}
#[test]
fn rumble_is_read_only_when_the_report_sets_it() {
assert_eq!(
rumble(&[0x05, 0x07, 0x04, 0, 0x40, 0xff, 0, 0, 0xff]),
Some((0xffff, 0x4040))
);
// Lights only.
assert_eq!(rumble(&[0x05, 0x02, 0x04, 0, 0x40, 0xff]), None);
assert_eq!(rumble(&[0x11, 0xff]), None);
}
}
+125
View File
@@ -0,0 +1,125 @@
//! Which controller the box presents for the one a client described.
//!
//! A controller is matched to a template the box supports by vendor and
//! product. One that matches none, from a vendor the box knows, gets that
//! vendor's default -- the controller of theirs games know best -- so that it
//! still shows up as its vendor's, with that vendor's buttons in a game. Only a
//! controller the box cannot tell at all is presented as generic.
//!
//! A fallback presents the template's identity, not the client's: an identity
//! is only worth passing on with the layout that goes with it (see
//! `crate::layout`).
use nesprotocol::gamepad::PadIdentity;
use crate::layout::{self, Layout, MICROSOFT, NINTENDO, SONY};
use crate::replica::{self, Model};
pub enum Template {
/// The device itself, as `product` (see `crate::replica`).
Replica { model: Model, product: u16 },
/// One evdev device.
Gamepad(Layout),
}
pub fn for_identity(identity: &PadIdentity) -> Template {
let product = identity.product;
match identity.vendor {
SONY => match product {
0x0ce6 => Template::Gamepad(layout::dualsense()),
_ if replica::is(Model::DualShock4, product) => Template::Replica {
model: Model::DualShock4,
product,
},
_ => Template::Replica {
model: Model::DualShock4,
product: replica::DUALSHOCK4_V2,
},
},
MICROSOFT => match product {
0x02ea => Template::Gamepad(layout::xbox_one()),
_ => Template::Gamepad(layout::xbox_360()),
},
NINTENDO => Template::Gamepad(layout::switch_pro()),
_ => Template::Gamepad(layout::generic(identity)),
}
}
impl Template {
/// Which template, for the log.
pub fn describe(&self) -> String {
match self {
Self::Replica { model, product } => format!("{model:?} {product:04x}"),
Self::Gamepad(layout) => format!("{} {:04x}", layout.driver, layout.product),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pick(vendor: u16, product: u16) -> Template {
for_identity(&PadIdentity {
vendor,
product,
name: "Pad".into(),
})
}
fn gamepad(template: Template) -> Layout {
match template {
Template::Gamepad(layout) => layout,
Template::Replica { .. } => panic!("a replica"),
}
}
#[test]
fn a_dualshock_4_is_rebuilt_as_the_one_it_is() {
for product in [0x05c4, 0x09cc, 0x0ba0] {
assert!(matches!(
pick(SONY, product),
Template::Replica { model: Model::DualShock4, product: p } if p == product
));
}
}
#[test]
fn a_sony_controller_nothing_fits_is_a_dualshock_4() {
// A DualSense Edge, which has no template of its own.
assert!(matches!(
pick(SONY, 0x0df2),
Template::Replica {
model: Model::DualShock4,
product: 0x09cc
}
));
}
#[test]
fn a_dualsense_has_its_own() {
assert_eq!(gamepad(pick(SONY, 0x0ce6)).product, 0x0ce6);
}
#[test]
fn an_xbox_controller_nothing_fits_is_a_360_pad() {
assert_eq!(gamepad(pick(MICROSOFT, 0x02ea)).product, 0x02ea);
assert_eq!(gamepad(pick(MICROSOFT, 0x028e)).product, 0x028e);
// A Series X|S pad.
let layout = gamepad(pick(MICROSOFT, 0x0b12));
assert_eq!((layout.vendor, layout.product), (MICROSOFT, 0x028e));
}
#[test]
fn anything_from_nintendo_is_a_pro_controller() {
for product in [0x2009, 0x2006, 0x2007] {
assert_eq!(gamepad(pick(NINTENDO, product)).product, 0x2009);
}
}
#[test]
fn a_controller_nobody_can_tell_is_generic() {
assert_eq!(gamepad(pick(0x2dc8, 0x3106)).driver, "generic");
assert_eq!(gamepad(pick(0, 0)).driver, "generic");
}
}
+391
View File
@@ -0,0 +1,391 @@
//! Telling libudev about a device, in a box that has no udev.
//!
//! Games find controllers through libudev -- SDL, and Wine's device bus under
//! Proton, both enumerate with it and watch its monitor for hotplug. Without a
//! udev daemon the kernel still creates the device and its node, but two things
//! a daemon adds are missing, and each is enough on its own to make a
//! controller invisible:
//!
//! - **Properties.** Whether an input device is a joystick is not something
//! the kernel says; `ID_INPUT_JOYSTICK` is the daemon's classification,
//! stored in its database under `/run/udev/data`. Readers ask for it by name
//! and skip a device that lacks it.
//! - **Hotplug.** libudev's monitor listens on the netlink group the *daemon*
//! rebroadcasts on, not the one the kernel announces on, so a device created
//! while a game is running is never seen by it.
//!
//! This does both, for the devices this process makes and nothing else.
//!
//! # What a broadcast must look like to be believed
//!
//! Taken from libudev's receiving side (systemd's `device-monitor.c`), because
//! every one of these is a silent drop on failure:
//!
//! - The monitor only listens without a daemon at all when `/dev` is a
//! devtmpfs, which in a box it is.
//! - The sender must be uid 0. This process runs as root for that reason.
//! - A message with the `libudev` header marks the device initialized; one in
//! the kernel's own `action@devpath` form does not, and readers that check
//! drop it. The header carries MurmurHash2 hashes that subscribers filter on
//! *in the kernel*, so a wrong hash is a message nobody receives.
use std::collections::BTreeMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
const DATA_DIR: &str = "/run/udev/data";
const TAGS_DIR: &str = "/run/udev/tags";
/// The netlink group udevd rebroadcasts on, as opposed to the kernel's 1.
const GROUP_UDEV: u32 = 2;
const UDEV_MONITOR_MAGIC: u32 = 0xfeed_cafe;
/// Tags every input device a seat owns gets from udev's stock rules. A reader
/// enumerating by tag, rather than by subsystem, finds nothing without them.
const TAGS: [&str; 2] = ["seat", "uaccess"];
/// systemd's `MurmurHash2`, seed and all. Its hashes go into a header the
/// kernel filters on, so this has to agree bit for bit.
fn murmur2(data: &[u8], seed: u32) -> u32 {
const M: u32 = 0x5bd1_e995;
let mut h = seed ^ data.len() as u32;
let (chunks, tail) = data.as_chunks::<4>();
for chunk in chunks {
let mut k = u32::from_le_bytes(*chunk);
k = k.wrapping_mul(M);
k ^= k >> 24;
k = k.wrapping_mul(M);
h = h.wrapping_mul(M) ^ k;
}
if !tail.is_empty() {
for (i, &b) in tail.iter().enumerate().rev() {
h ^= u32::from(b) << (8 * i);
}
h = h.wrapping_mul(M);
}
h ^= h >> 13;
h = h.wrapping_mul(M);
h ^ (h >> 15)
}
fn bloom(tag: &str) -> u64 {
let hash = murmur2(tag.as_bytes(), 0);
[0, 6, 12, 18]
.iter()
.fold(0, |bits, shift| bits | 1u64 << ((hash >> shift) & 63))
}
/// One device as udev would describe it.
#[derive(Debug, Clone)]
pub struct Record {
/// Path under `/sys`, which is what udev calls the devpath.
pub devpath: String,
/// The database's name for it: `c13:80` for a node, `+input:input5` for
/// a device without one.
pub db_name: String,
pub properties: BTreeMap<String, String>,
}
impl Record {
/// Describe a device from what the kernel says about it, plus what udev's
/// rules would have added.
///
/// The kernel's own `uevent` file is the base, because that is exactly
/// what udevd starts from; `extra` is the classification on top.
pub fn read(syspath: &Path, subsystem: &str, extra: &[(&str, String)]) -> io::Result<Self> {
let mut properties = BTreeMap::new();
for line in std::fs::read_to_string(syspath.join("uevent"))?.lines() {
if let Some((key, value)) = line.split_once('=') {
properties.insert(key.to_owned(), value.to_owned());
}
}
let devpath = devpath(syspath)?;
let sysname = syspath
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let db_name = match (properties.get("MAJOR"), properties.get("MINOR")) {
(Some(major), Some(minor)) => format!("c{major}:{minor}"),
_ => format!("+{subsystem}:{sysname}"),
};
// The kernel names the node relative to /dev; udev names it in full.
if let Some(devname) = properties.get_mut("DEVNAME")
&& !devname.starts_with('/')
{
*devname = format!("/dev/{devname}");
}
properties.insert("DEVPATH".into(), devpath.clone());
properties.insert("SUBSYSTEM".into(), subsystem.into());
properties.insert("USEC_INITIALIZED".into(), monotonic_usec().to_string());
let tags = format!(":{}:", TAGS.join(":"));
properties.insert("TAGS".into(), tags.clone());
properties.insert("CURRENT_TAGS".into(), tags);
for (key, value) in extra {
properties.insert((*key).to_owned(), value.clone());
}
Ok(Self {
devpath,
db_name,
properties,
})
}
/// The database entry, in the format udevd writes.
fn db_entry(&self) -> String {
let mut entry = format!("I:{}\n", self.properties["USEC_INITIALIZED"]);
// Only what a rule added goes in the database; the rest a reader gets
// from sysfs itself.
for (key, value) in &self.properties {
if key.starts_with("ID_") {
entry.push_str(&format!("E:{key}={value}\n"));
}
}
for tag in TAGS {
entry.push_str(&format!("G:{tag}\nQ:{tag}\n"));
}
entry.push_str("V:1\n");
entry
}
}
/// udev's devpath: the syspath without `/sys`, keeping the leading slash.
///
/// `Path::strip_prefix` drops that slash, and libudev joins `/sys` straight
/// onto whatever it is given -- so without it every device lands at
/// `/sysdevices/...`, and every broadcast is rejected with nothing logged.
fn devpath(syspath: &Path) -> io::Result<String> {
let rest = syspath
.strip_prefix("/sys")
.map_err(|_| io::Error::other("not under /sys"))?;
Ok(format!("/{}", rest.to_string_lossy()))
}
fn monotonic_usec() -> u64 {
let mut ts = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
// SAFETY: a valid clock and a timespec to fill.
unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) };
ts.tv_sec as u64 * 1_000_000 + ts.tv_nsec as u64 / 1_000
}
/// Stands in for udevd's database and broadcasts.
pub struct Udev {
socket: std::os::fd::OwnedFd,
seqnum: AtomicU64,
}
impl Udev {
pub fn new() -> io::Result<Self> {
let udev = Self::broadcaster()?;
for dir in [PathBuf::from(DATA_DIR)]
.into_iter()
.chain(TAGS.iter().map(|t| Path::new(TAGS_DIR).join(t)))
{
std::fs::create_dir_all(&dir)?;
}
Ok(udev)
}
/// The broadcasting half alone, which touches nothing on disk.
fn broadcaster() -> io::Result<Self> {
// SAFETY: plain socket creation; the result is checked.
let fd = unsafe {
libc::socket(
libc::AF_NETLINK,
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
libc::NETLINK_KOBJECT_UEVENT,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: a descriptor just returned to us and owned by nothing else.
let socket = unsafe { std::os::fd::FromRawFd::from_raw_fd(fd) };
Ok(Self {
socket,
seqnum: AtomicU64::new(1),
})
}
/// Record a device and announce it.
pub fn add(&self, record: &Record) -> io::Result<()> {
std::fs::write(Path::new(DATA_DIR).join(&record.db_name), record.db_entry())?;
for tag in TAGS {
std::fs::write(Path::new(TAGS_DIR).join(tag).join(&record.db_name), "")?;
}
self.broadcast("add", record)
}
/// Announce a device gone and forget it. Every step is attempted even if
/// an earlier one fails: a stale database entry is harmless next to a
/// removal nobody heard.
pub fn remove(&self, record: &Record) -> io::Result<()> {
let _ = std::fs::remove_file(Path::new(DATA_DIR).join(&record.db_name));
for tag in TAGS {
let _ = std::fs::remove_file(Path::new(TAGS_DIR).join(tag).join(&record.db_name));
}
self.broadcast("remove", record)
}
fn broadcast(&self, action: &str, record: &Record) -> io::Result<()> {
let seqnum = self.seqnum.fetch_add(1, Ordering::Relaxed);
let message = encode(action, seqnum, record);
let mut addr: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
addr.nl_family = libc::AF_NETLINK as u16;
addr.nl_groups = GROUP_UDEV;
// SAFETY: the address and buffer outlive the call.
let n = unsafe {
libc::sendto(
std::os::fd::AsRawFd::as_raw_fd(&self.socket),
message.as_ptr().cast(),
message.len(),
0,
(&addr as *const libc::sockaddr_nl).cast(),
std::mem::size_of::<libc::sockaddr_nl>() as u32,
)
};
if n < 0 {
let error = io::Error::last_os_error();
// What a multicast send reports when nobody is listening yet,
// which before a game has started is the normal case.
if error.raw_os_error() == Some(libc::ECONNREFUSED) {
return Ok(());
}
return Err(error);
}
Ok(())
}
}
/// A libudev monitor message: the `monitor_netlink_header`, then the
/// properties as NUL-terminated `KEY=value` strings.
fn encode(action: &str, seqnum: u64, record: &Record) -> Vec<u8> {
let mut properties = Vec::new();
let mut push = |key: &str, value: &str| {
properties.extend_from_slice(key.as_bytes());
properties.push(b'=');
properties.extend_from_slice(value.as_bytes());
properties.push(0);
};
push("ACTION", action);
push("SEQNUM", &seqnum.to_string());
for (key, value) in &record.properties {
push(key, value);
}
const HEADER_LEN: u32 = 40;
let tags = TAGS.iter().fold(0u64, |bits, tag| bits | bloom(tag));
let mut message = Vec::with_capacity(HEADER_LEN as usize + properties.len());
message.extend_from_slice(b"libudev\0");
// The magic, hashes and bloom are in network order; the lengths are not.
message.extend_from_slice(&UDEV_MONITOR_MAGIC.to_be_bytes());
message.extend_from_slice(&HEADER_LEN.to_ne_bytes());
message.extend_from_slice(&HEADER_LEN.to_ne_bytes());
message.extend_from_slice(&(properties.len() as u32).to_ne_bytes());
let subsystem = record
.properties
.get("SUBSYSTEM")
.map_or("", String::as_str);
message.extend_from_slice(&murmur2(subsystem.as_bytes(), 0).to_be_bytes());
// Neither input nor hidraw devices have a devtype, and zero is what udevd
// sends for none.
message.extend_from_slice(&0u32.to_be_bytes());
message.extend_from_slice(&((tags >> 32) as u32).to_be_bytes());
message.extend_from_slice(&(tags as u32).to_be_bytes());
debug_assert_eq!(message.len(), HEADER_LEN as usize);
message.extend_from_slice(&properties);
message
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_devpath_starts_where_sys_ends() {
assert_eq!(
devpath(Path::new("/sys/devices/virtual/input/input5/event3")).unwrap(),
"/devices/virtual/input/input5/event3"
);
assert!(devpath(Path::new("/dev/input/event3")).is_err());
}
#[test]
fn hashes_agree_with_systemd() {
// Computed by compiling systemd's own MurmurHash2.c.
for (input, expected) in [
("input", 0xc1a2_8470),
("seat", 0x435b_3e40),
("uaccess", 0xe88e_d0cc),
("hidraw", 0xc2ca_f397),
("a", 0x9268_5f5e),
("ab", 0x1aa1_4063),
("abc", 0x1357_7c9b),
] {
assert_eq!(murmur2(input.as_bytes(), 0), expected, "{input}");
}
}
fn record() -> Record {
let mut properties = BTreeMap::new();
properties.insert(
"DEVPATH".into(),
"/devices/virtual/input/input5/event3".into(),
);
properties.insert("SUBSYSTEM".into(), "input".into());
properties.insert("USEC_INITIALIZED".into(), "42".into());
properties.insert("ID_INPUT_JOYSTICK".into(), "1".into());
properties.insert("NAME".into(), "\"x\"".into());
properties.insert("TAGS".into(), ":seat:uaccess:".into());
Record {
devpath: "/devices/virtual/input/input5/event3".into(),
db_name: "c13:67".into(),
properties,
}
}
#[test]
fn a_broadcast_has_the_header_libudev_checks() {
let message = encode("add", 7, &record());
assert_eq!(&message[..8], b"libudev\0");
assert_eq!(&message[8..12], &[0xfe, 0xed, 0xca, 0xfe]);
let off = u32::from_ne_bytes(message[16..20].try_into().unwrap()) as usize;
let len = u32::from_ne_bytes(message[20..24].try_into().unwrap()) as usize;
assert_eq!(off + len, message.len());
assert_eq!(&message[24..28], &0xc1a2_8470u32.to_be_bytes());
let props: Vec<&[u8]> = message[off..].split(|&b| b == 0).collect();
for needed in [&b"ACTION=add"[..], b"SEQNUM=7", b"SUBSYSTEM=input"] {
assert!(
props.contains(&needed),
"{}",
String::from_utf8_lossy(needed)
);
}
}
/// The broadcast as real libudev receives it. Needs uid 0 and its own
/// network namespace, both of which `unshare -rn` gives without root, and
/// a libudev monitor listening in that namespace to report what it got.
#[test]
#[ignore = "sends a real udev broadcast; run inside `unshare -rn` beside a monitor"]
fn a_broadcast_for_a_monitor() {
Udev::broadcaster()
.unwrap()
.broadcast("add", &record())
.unwrap();
}
#[test]
fn the_database_holds_the_classification_and_the_tags() {
let entry = record().db_entry();
assert!(entry.starts_with("I:42\n"));
assert!(entry.contains("E:ID_INPUT_JOYSTICK=1\n"));
assert!(!entry.contains("NAME"));
assert!(entry.contains("G:seat\n") && entry.contains("Q:uaccess\n"));
assert!(entry.ends_with("V:1\n"));
}
}
+326
View File
@@ -0,0 +1,326 @@
//! A HID device made in userspace, through `/dev/uhid`.
//!
//! The kernel treats it as real hardware: a driver binds to it -- hid-generic,
//! for anything not claimed more specifically -- and it gets a hidraw node,
//! which is what Proton reads a controller through when it wants the device
//! itself rather than a gamepad abstraction of it. Reports written here are
//! the device's; everything asked of it comes back out of here to be answered.
//!
//! The ABI is `linux/uhid.h`: every message in either direction is one packed
//! `struct uhid_event`, a type and a union. The layouts below are the kernel's,
//! checked by size at compile time.
use std::fs::OpenOptions;
use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, RawFd};
use std::os::unix::fs::OpenOptionsExt;
use std::sync::Mutex;
const UHID_DESTROY: u32 = 1;
const UHID_START: u32 = 2;
const UHID_STOP: u32 = 3;
const UHID_OPEN: u32 = 4;
const UHID_CLOSE: u32 = 5;
const UHID_OUTPUT: u32 = 6;
const UHID_GET_REPORT: u32 = 9;
const UHID_GET_REPORT_REPLY: u32 = 10;
const UHID_CREATE2: u32 = 11;
const UHID_INPUT2: u32 = 12;
const UHID_SET_REPORT: u32 = 13;
const UHID_SET_REPORT_REPLY: u32 = 14;
/// `UHID_DATA_MAX`, and also `HID_MAX_DESCRIPTOR_SIZE`: the kernel uses the
/// same number for both.
pub const DATA_MAX: usize = 4096;
/// The largest member of the event union is `uhid_create2_req`.
const CREATE2_LEN: usize = 128 + 64 + 64 + 2 + 2 + 4 * 4 + DATA_MAX;
/// `struct uhid_event`: a `u32` type, then the union.
const EVENT_LEN: usize = 4 + CREATE2_LEN;
const _: () = assert!(EVENT_LEN == 4376);
/// Report kinds, as `uhid` numbers them.
pub const REPORT_FEATURE: u8 = 0;
pub const REPORT_OUTPUT: u8 = 1;
pub const REPORT_INPUT: u8 = 2;
/// What a device in the box was built from.
pub struct Spec<'a> {
pub name: &'a str,
pub uniq: &'a str,
pub bus: u16,
pub vendor: u16,
pub product: u16,
pub version: u16,
pub country: u8,
pub descriptor: &'a [u8],
}
/// What the kernel said.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
/// A driver bound. Its nodes follow shortly after, not with this.
Start,
Stop,
/// Something opened a node of it, or the last one closed.
Open,
Close,
/// A report written to the device.
Output {
kind: u8,
data: Vec<u8>,
},
/// Something wants a report read from the device, answered by `id`.
GetReport {
id: u32,
number: u8,
kind: u8,
},
/// Something wants a report written to the device, answered by `id`.
SetReport {
id: u32,
number: u8,
kind: u8,
data: Vec<u8>,
},
}
/// A device that exists for as long as this does.
pub struct Device {
/// Writes are whole events, and several threads may answer requests; one
/// at a time keeps each event in one piece.
file: Mutex<std::fs::File>,
fd: RawFd,
}
fn put_str(buf: &mut [u8], value: &str) {
// One short of the field, so the kernel always finds a terminator.
let bytes = value.as_bytes();
let len = bytes.len().min(buf.len() - 1);
buf[..len].copy_from_slice(&bytes[..len]);
}
impl Device {
pub fn create(spec: &Spec<'_>) -> io::Result<Self> {
if spec.descriptor.is_empty() || spec.descriptor.len() > DATA_MAX {
return Err(io::Error::other(format!(
"a report descriptor of {} bytes",
spec.descriptor.len()
)));
}
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK | libc::O_CLOEXEC)
.open("/dev/uhid")?;
let fd = file.as_raw_fd();
let device = Self {
file: Mutex::new(file),
fd,
};
let mut event = vec![0u8; EVENT_LEN];
event[..4].copy_from_slice(&UHID_CREATE2.to_ne_bytes());
let body = &mut event[4..];
put_str(&mut body[..128], spec.name);
// `phys` is where a device is attached. This one is attached nowhere
// real, and saying so is truer than inventing a USB path.
put_str(&mut body[128..192], "nesgamepad");
put_str(&mut body[192..256], spec.uniq);
body[256..258].copy_from_slice(&(spec.descriptor.len() as u16).to_ne_bytes());
body[258..260].copy_from_slice(&spec.bus.to_ne_bytes());
body[260..264].copy_from_slice(&u32::from(spec.vendor).to_ne_bytes());
body[264..268].copy_from_slice(&u32::from(spec.product).to_ne_bytes());
body[268..272].copy_from_slice(&u32::from(spec.version).to_ne_bytes());
body[272..276].copy_from_slice(&u32::from(spec.country).to_ne_bytes());
body[276..276 + spec.descriptor.len()].copy_from_slice(spec.descriptor);
device.write_event(&event)?;
Ok(device)
}
fn write_event(&self, event: &[u8]) -> io::Result<()> {
let mut file = self.file.lock().unwrap_or_else(|e| e.into_inner());
file.write_all(event)
}
/// One input report, as the device sent it.
pub fn input(&self, report: &[u8]) -> io::Result<()> {
let len = report.len().min(DATA_MAX);
let mut event = Vec::with_capacity(4 + 2 + len);
event.extend_from_slice(&UHID_INPUT2.to_ne_bytes());
event.extend_from_slice(&(len as u16).to_ne_bytes());
event.extend_from_slice(&report[..len]);
self.write_event(&event)
}
/// Answer a [`Event::GetReport`]. A short event is extended with zeroes
/// by the kernel, so only what is used is written.
pub fn get_report_reply(&self, id: u32, err: u16, data: &[u8]) -> io::Result<()> {
let len = data.len().min(DATA_MAX);
let mut event = Vec::with_capacity(4 + 8 + len);
event.extend_from_slice(&UHID_GET_REPORT_REPLY.to_ne_bytes());
event.extend_from_slice(&id.to_ne_bytes());
event.extend_from_slice(&err.to_ne_bytes());
event.extend_from_slice(&(len as u16).to_ne_bytes());
event.extend_from_slice(&data[..len]);
self.write_event(&event)
}
/// Answer a [`Event::SetReport`].
pub fn set_report_reply(&self, id: u32, err: u16) -> io::Result<()> {
let mut event = Vec::with_capacity(10);
event.extend_from_slice(&UHID_SET_REPORT_REPLY.to_ne_bytes());
event.extend_from_slice(&id.to_ne_bytes());
event.extend_from_slice(&err.to_ne_bytes());
self.write_event(&event)
}
/// Everything the kernel has said since the last call.
pub fn drain(&self) -> io::Result<Vec<Event>> {
let mut out = Vec::new();
let mut buf = vec![0u8; EVENT_LEN];
loop {
let n = {
let mut file = self.file.lock().unwrap_or_else(|e| e.into_inner());
match file.read(&mut buf) {
Ok(n) => n,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(out),
Err(e) => return Err(e),
}
};
if n == 0 {
return Ok(out);
}
// The kernel may write short; the rest reads as zero.
buf[n..].fill(0);
if let Some(event) = parse(&buf) {
out.push(event);
}
}
}
}
/// One event, from a buffer at least as long as `struct uhid_event`.
fn parse(buf: &[u8]) -> Option<Event> {
let kind = u32::from_ne_bytes(buf[..4].try_into().ok()?);
let body = &buf[4..];
let u16_at = |i: usize| u16::from_ne_bytes([body[i], body[i + 1]]);
let u32_at = |i: usize| u32::from_ne_bytes(body[i..i + 4].try_into().unwrap());
Some(match kind {
UHID_START => Event::Start,
UHID_STOP => Event::Stop,
UHID_OPEN => Event::Open,
UHID_CLOSE => Event::Close,
// uhid_output_req: data[4096], size u16, rtype u8
UHID_OUTPUT => {
let size = (u16_at(DATA_MAX) as usize).min(DATA_MAX);
Event::Output {
kind: body[DATA_MAX + 2],
data: body[..size].to_vec(),
}
}
// uhid_get_report_req: id u32, rnum u8, rtype u8
UHID_GET_REPORT => Event::GetReport {
id: u32_at(0),
number: body[4],
kind: body[5],
},
// uhid_set_report_req: id u32, rnum u8, rtype u8, size u16, data
UHID_SET_REPORT => {
let size = (u16_at(6) as usize).min(DATA_MAX);
Event::SetReport {
id: u32_at(0),
number: body[4],
kind: body[5],
data: body[8..8 + size].to_vec(),
}
}
_ => return None,
})
}
impl AsRawFd for Device {
fn as_raw_fd(&self) -> RawFd {
self.fd
}
}
impl Device {
/// Remove the device now. Closing the descriptor would too, but only when
/// the last holder of it closes.
pub fn destroy(&self) {
let _ = self.write_event(&UHID_DESTROY.to_ne_bytes());
}
}
impl Drop for Device {
fn drop(&mut self) {
self.destroy();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn event(kind: u32, body: &[u8]) -> Vec<u8> {
let mut buf = vec![0u8; EVENT_LEN];
buf[..4].copy_from_slice(&kind.to_ne_bytes());
buf[4..4 + body.len()].copy_from_slice(body);
buf
}
#[test]
fn an_output_report_is_read_from_the_end_of_its_buffer() {
// The size and kind come *after* the 4096-byte data field, which is
// the easiest part of this ABI to get wrong.
let mut body = vec![0u8; DATA_MAX + 3];
body[..3].copy_from_slice(&[0x05, 0xff, 0x00]);
body[DATA_MAX..DATA_MAX + 2].copy_from_slice(&3u16.to_ne_bytes());
body[DATA_MAX + 2] = 1;
assert_eq!(
parse(&event(UHID_OUTPUT, &body)),
Some(Event::Output {
kind: 1,
data: vec![0x05, 0xff, 0x00]
})
);
}
#[test]
fn requests_carry_what_they_ask_for() {
let mut body = Vec::new();
body.extend_from_slice(&42u32.to_ne_bytes());
body.extend_from_slice(&[0x02, 0]);
assert_eq!(
parse(&event(UHID_GET_REPORT, &body)),
Some(Event::GetReport {
id: 42,
number: 0x02,
kind: 0
})
);
body.extend_from_slice(&2u16.to_ne_bytes());
body.extend_from_slice(&[0xaa, 0xbb]);
assert_eq!(
parse(&event(UHID_SET_REPORT, &body)),
Some(Event::SetReport {
id: 42,
number: 0x02,
kind: 0,
data: vec![0xaa, 0xbb]
})
);
}
#[test]
fn a_size_larger_than_the_buffer_is_clamped() {
let mut body = vec![0u8; DATA_MAX + 3];
body[DATA_MAX..DATA_MAX + 2].copy_from_slice(&u16::MAX.to_ne_bytes());
let Some(Event::Output { data, .. }) = parse(&event(UHID_OUTPUT, &body)) else {
panic!("did not parse");
};
assert_eq!(data.len(), DATA_MAX);
}
}
+434
View File
@@ -0,0 +1,434 @@
//! A virtual input device, through `/dev/uinput`.
//!
//! Raw ioctls rather than a crate: the interface is a dozen calls and four
//! structs, and the part that matters -- force feedback, where the kernel
//! blocks a game's upload until this process answers it -- is the part a
//! wrapper is most likely to get subtly wrong. The struct layouts are checked
//! against the kernel ABI at compile time below.
use std::ffi::CStr;
use std::fs::OpenOptions;
use std::io;
use std::mem::size_of;
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
/// Event types and codes, from `linux/input-event-codes.h`.
pub mod code {
pub const EV_SYN: u16 = 0x00;
pub const EV_KEY: u16 = 0x01;
pub const EV_ABS: u16 = 0x03;
pub const EV_FF: u16 = 0x15;
pub const EV_UINPUT: u16 = 0x0101;
pub const SYN_REPORT: u16 = 0;
pub const BUS_USB: u16 = 0x03;
pub const BUS_VIRTUAL: u16 = 0x06;
pub const BTN_SOUTH: u16 = 0x130;
pub const BTN_EAST: u16 = 0x131;
pub const BTN_NORTH: u16 = 0x133;
pub const BTN_WEST: u16 = 0x134;
/// Letter names for the same codes as north and west, which some drivers
/// use by letter rather than by position (see `crate::layout`).
pub const BTN_X: u16 = 0x133;
pub const BTN_Y: u16 = 0x134;
pub const BTN_Z: u16 = 0x135;
pub const BTN_TL: u16 = 0x136;
pub const BTN_TR: u16 = 0x137;
pub const BTN_TL2: u16 = 0x138;
pub const BTN_TR2: u16 = 0x139;
pub const BTN_SELECT: u16 = 0x13a;
pub const BTN_START: u16 = 0x13b;
pub const BTN_MODE: u16 = 0x13c;
pub const BTN_THUMBL: u16 = 0x13d;
pub const BTN_THUMBR: u16 = 0x13e;
pub const ABS_X: u16 = 0x00;
pub const ABS_Y: u16 = 0x01;
pub const ABS_Z: u16 = 0x02;
pub const ABS_RX: u16 = 0x03;
pub const ABS_RY: u16 = 0x04;
pub const ABS_RZ: u16 = 0x05;
pub const ABS_HAT0X: u16 = 0x10;
pub const ABS_HAT0Y: u16 = 0x11;
pub const FF_RUMBLE: u16 = 0x50;
pub const UI_FF_UPLOAD: u16 = 1;
pub const UI_FF_ERASE: u16 = 2;
}
/// One axis and its range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AbsAxis {
pub code: u16,
pub min: i32,
pub max: i32,
pub fuzz: i32,
pub flat: i32,
}
/// `struct input_event` on a 64-bit kernel.
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct InputEvent {
tv_sec: i64,
tv_usec: i64,
pub kind: u16,
pub code: u16,
pub value: i32,
}
impl InputEvent {
pub fn key(code: u16, down: bool) -> Self {
Self::new(code::EV_KEY, code, i32::from(down))
}
pub fn abs(code: u16, value: i32) -> Self {
Self::new(code::EV_ABS, code, value)
}
pub fn report() -> Self {
Self::new(code::EV_SYN, code::SYN_REPORT, 0)
}
fn new(kind: u16, code: u16, value: i32) -> Self {
// The time is the kernel's to stamp; a writer's is ignored.
Self {
kind,
code,
value,
..Self::default()
}
}
}
#[repr(C)]
struct InputId {
bustype: u16,
vendor: u16,
product: u16,
version: u16,
}
#[repr(C)]
struct UinputSetup {
id: InputId,
name: [u8; 80],
ff_effects_max: u32,
}
#[repr(C)]
struct UinputAbsSetup {
code: u16,
// absinfo: value, minimum, maximum, fuzz, flat, resolution
absinfo: [i32; 6],
}
/// `struct ff_effect`. The union is kept as raw bytes: only its rumble member
/// is ever read, and that is the first four of them.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct FfEffect {
pub kind: u16,
pub id: i16,
direction: u16,
trigger: [u16; 2],
/// length (ms), delay (ms)
replay: [u16; 2],
union: [u64; 4],
}
impl FfEffect {
/// Strong and weak magnitudes, if this is a rumble effect.
pub fn rumble(&self) -> Option<(u16, u16)> {
if self.kind != code::FF_RUMBLE {
return None;
}
let raw = self.union[0].to_ne_bytes();
Some((
u16::from_ne_bytes([raw[0], raw[1]]),
u16::from_ne_bytes([raw[2], raw[3]]),
))
}
pub fn length_ms(&self) -> u16 {
self.replay[0]
}
}
#[repr(C)]
struct UinputFfUpload {
request_id: u32,
retval: i32,
effect: FfEffect,
old: FfEffect,
}
#[repr(C)]
struct UinputFfErase {
request_id: u32,
retval: i32,
effect_id: u32,
}
const _: () = {
assert!(size_of::<InputEvent>() == 24);
assert!(size_of::<UinputSetup>() == 92);
assert!(size_of::<UinputAbsSetup>() == 28);
assert!(size_of::<FfEffect>() == 48);
assert!(size_of::<UinputFfUpload>() == 104);
assert!(size_of::<UinputFfErase>() == 12);
};
// `_IOC` as `asm-generic/ioctl.h` builds it, which is what x86 uses.
const fn ioc(dir: u64, nr: u64, size: usize) -> u64 {
(dir << 30) | ((size as u64) << 16) | ((b'U' as u64) << 8) | nr
}
const NONE: u64 = 0;
const WRITE: u64 = 1;
const READ: u64 = 2;
const UI_DEV_CREATE: u64 = ioc(NONE, 1, 0);
const UI_DEV_DESTROY: u64 = ioc(NONE, 2, 0);
const UI_DEV_SETUP: u64 = ioc(WRITE, 3, size_of::<UinputSetup>());
const UI_ABS_SETUP: u64 = ioc(WRITE, 4, size_of::<UinputAbsSetup>());
const UI_SET_EVBIT: u64 = ioc(WRITE, 100, size_of::<libc::c_int>());
const UI_SET_KEYBIT: u64 = ioc(WRITE, 101, size_of::<libc::c_int>());
const UI_SET_ABSBIT: u64 = ioc(WRITE, 103, size_of::<libc::c_int>());
const UI_SET_FFBIT: u64 = ioc(WRITE, 107, size_of::<libc::c_int>());
const UI_BEGIN_FF_UPLOAD: u64 = ioc(READ | WRITE, 200, size_of::<UinputFfUpload>());
const UI_END_FF_UPLOAD: u64 = ioc(WRITE, 201, size_of::<UinputFfUpload>());
const UI_BEGIN_FF_ERASE: u64 = ioc(READ | WRITE, 202, size_of::<UinputFfErase>());
const UI_END_FF_ERASE: u64 = ioc(WRITE, 203, size_of::<UinputFfErase>());
const SYSNAME_LEN: usize = 64;
const UI_GET_SYSNAME: u64 = ioc(READ, 44, SYSNAME_LEN);
/// How many effects a game may have uploaded at once. The same as ff-memless,
/// which is what real controller drivers are built on.
const FF_EFFECTS_MAX: u32 = 16;
fn ioctl<T>(fd: RawFd, request: u64, arg: *mut T) -> io::Result<()> {
// SAFETY: every request above is paired with the struct its size encodes.
if unsafe { libc::ioctl(fd, request as _, arg) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn ioctl_int(fd: RawFd, request: u64, value: u16) -> io::Result<()> {
// SAFETY: the `UI_SET_*BIT` requests take their argument by value.
if unsafe { libc::ioctl(fd, request as _, libc::c_int::from(value)) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
/// What a device was built with.
pub struct Spec<'a> {
pub name: &'a str,
pub bus: u16,
pub vendor: u16,
pub product: u16,
pub version: u16,
pub keys: &'a [u16],
pub axes: &'a [AbsAxis],
}
/// A device that exists for as long as this does.
pub struct Device {
fd: OwnedFd,
/// `inputN`, the kernel's name for it.
pub sysname: String,
}
/// What reading the device produced.
#[derive(Debug, Clone, Copy)]
pub enum Request {
/// A game uploaded or replaced an effect. It has already been accepted.
Upload(FfEffect),
Erase(i16),
/// A game started (`true`) or stopped an effect.
Play {
id: i16,
on: bool,
},
}
impl std::fmt::Debug for FfEffect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FfEffect")
.field("kind", &self.kind)
.field("id", &self.id)
.field("rumble", &self.rumble())
.field("length_ms", &self.length_ms())
.finish()
}
}
impl Device {
pub fn create(spec: &Spec<'_>) -> io::Result<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(libc::O_NONBLOCK | libc::O_CLOEXEC)
.open("/dev/uinput")?;
let fd: OwnedFd = file.into();
let raw = fd.as_raw_fd();
ioctl_int(raw, UI_SET_EVBIT, code::EV_KEY)?;
for &key in spec.keys {
ioctl_int(raw, UI_SET_KEYBIT, key)?;
}
ioctl_int(raw, UI_SET_EVBIT, code::EV_ABS)?;
for axis in spec.axes {
ioctl_int(raw, UI_SET_ABSBIT, axis.code)?;
let mut setup = UinputAbsSetup {
code: axis.code,
absinfo: [
(axis.min + axis.max) / 2,
axis.min,
axis.max,
axis.fuzz,
axis.flat,
0,
],
};
ioctl(raw, UI_ABS_SETUP, &mut setup)?;
}
ioctl_int(raw, UI_SET_EVBIT, code::EV_FF)?;
ioctl_int(raw, UI_SET_FFBIT, code::FF_RUMBLE)?;
let mut setup = UinputSetup {
id: InputId {
bustype: spec.bus,
vendor: spec.vendor,
product: spec.product,
version: spec.version,
},
name: [0; 80],
ff_effects_max: FF_EFFECTS_MAX,
};
// One short of the buffer, so the kernel always finds a terminator.
let name = spec.name.as_bytes();
let len = name.len().min(setup.name.len() - 1);
setup.name[..len].copy_from_slice(&name[..len]);
ioctl(raw, UI_DEV_SETUP, &mut setup)?;
ioctl(raw, UI_DEV_CREATE, std::ptr::null_mut::<u8>())?;
let mut sysname = [0u8; SYSNAME_LEN];
ioctl(raw, UI_GET_SYSNAME, sysname.as_mut_ptr())?;
let sysname = CStr::from_bytes_until_nul(&sysname)
.map_err(|_| io::Error::other("unterminated sysname"))?
.to_string_lossy()
.into_owned();
Ok(Self { fd, sysname })
}
/// Where the kernel put it in sysfs.
pub fn syspath(&self) -> PathBuf {
PathBuf::from("/sys/devices/virtual/input").join(&self.sysname)
}
pub fn write(&self, events: &[InputEvent]) -> io::Result<()> {
let bytes = std::mem::size_of_val(events);
// SAFETY: `InputEvent` is `repr(C)` plain data.
let n = unsafe { libc::write(self.fd.as_raw_fd(), events.as_ptr().cast(), bytes) };
if n < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
/// Everything waiting on the device, answering any upload or erase as it
/// goes. Returns once the device has nothing more to say.
///
/// The answering is not optional: a game's upload ioctl blocks in the
/// kernel until this process ends it, so an unread request is a game
/// frozen mid-call.
pub fn drain(&self) -> io::Result<Vec<Request>> {
let raw = self.fd.as_raw_fd();
let mut out = Vec::new();
loop {
let mut event = InputEvent::default();
// SAFETY: reading one plain-data event into a buffer its size.
let n = unsafe {
libc::read(
raw,
(&mut event as *mut InputEvent).cast(),
size_of::<InputEvent>(),
)
};
if n < 0 {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::WouldBlock {
return Ok(out);
}
return Err(error);
}
if n as usize != size_of::<InputEvent>() {
return Ok(out);
}
match (event.kind, event.code) {
(code::EV_UINPUT, code::UI_FF_UPLOAD) => {
// SAFETY: plain data, filled in by the kernel.
let mut upload: UinputFfUpload = unsafe { std::mem::zeroed() };
upload.request_id = event.value as u32;
ioctl(raw, UI_BEGIN_FF_UPLOAD, &mut upload)?;
// Rumble is the only effect the device advertises, so the
// kernel refuses anything else before it gets here.
upload.retval = 0;
let effect = upload.effect;
ioctl(raw, UI_END_FF_UPLOAD, &mut upload)?;
out.push(Request::Upload(effect));
}
(code::EV_UINPUT, code::UI_FF_ERASE) => {
let mut erase = UinputFfErase {
request_id: event.value as u32,
retval: 0,
effect_id: 0,
};
ioctl(raw, UI_BEGIN_FF_ERASE, &mut erase)?;
erase.retval = 0;
let id = erase.effect_id as i16;
ioctl(raw, UI_END_FF_ERASE, &mut erase)?;
out.push(Request::Erase(id));
}
(code::EV_FF, id) => out.push(Request::Play {
id: id as i16,
on: event.value > 0,
}),
_ => {}
}
}
}
}
impl AsRawFd for Device {
fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
}
impl Device {
/// Remove the device now, whoever else still holds this.
///
/// Closing the descriptor would do it too, but only when the last holder
/// closes it. Destroying twice is harmless: the second is refused.
pub fn destroy(&self) {
let _ = ioctl(
self.fd.as_raw_fd(),
UI_DEV_DESTROY,
std::ptr::null_mut::<u8>(),
);
}
}
impl Drop for Device {
fn drop(&mut self) {
self.destroy();
}
}
+88
View File
@@ -249,6 +249,94 @@ pub async fn run_input_ipc_listener(
tracing::info!("input IPC listener exited"); tracing::info!("input IPC listener exited");
} }
/// The box's gamepad side, which dials in here.
///
/// Every client's gamepad messages go out already tagged with which client
/// (see `nesprotocol::gamepad`), and feedback comes back tagged the same way
/// and is routed to that client. Nothing here reads a message: what a
/// controller is belongs to the other end of this socket.
///
/// One peer at a time, like the input socket. Messages sent while none is
/// connected are dropped, which costs nothing: the gamepad side asks for any
/// controller it has not heard of the next time that controller's state
/// arrives.
pub async fn run_gamepad_ipc_listener(socket_path: PathBuf, mgr: Arc<SessionManager>) {
if socket_path.exists() {
let _ = std::fs::remove_file(&socket_path);
}
let listener = match UnixListener::bind(&socket_path) {
Ok(l) => l,
Err(e) => {
tracing::error!(
"Failed to bind gamepad IPC socket {}: {e}",
socket_path.display()
);
return;
}
};
tracing::info!("Gamepad IPC listening on {}", socket_path.display());
loop {
let stream = match listener.accept().await {
Ok((stream, _)) => stream,
Err(e) => {
tracing::error!("gamepad IPC accept error: {e}");
break;
}
};
tracing::info!("gamepad service connected");
let (mut read_half, mut write_half) = stream.into_split();
let mut messages = mgr.gamepad_messages();
let write_handle = tokio::spawn(async move {
loop {
match messages.recv().await {
Ok(frame) => {
if write_half.write_all(&frame).await.is_err() {
tracing::debug!("gamepad IPC write failed");
break;
}
}
// A controller's state is a snapshot, so a lost one is
// superseded by the next; a lost connect is asked for
// again. Lagging is survivable, and worth knowing about.
Err(RecvError::Lagged(n)) => {
tracing::warn!("gamepad messages lagged by {n}");
}
Err(RecvError::Closed) => break,
}
}
});
let read_mgr = mgr.clone();
let read_handle = tokio::spawn(async move {
let mut len_buf = [0u8; 2];
loop {
if read_half.read_exact(&mut len_buf).await.is_err() {
break;
}
let mut body = vec![0u8; u16::from_le_bytes(len_buf) as usize];
if read_half.read_exact(&mut body).await.is_err() {
break;
}
let Some((session, message)) = nesprotocol::gamepad::decode_ipc(&body) else {
tracing::debug!("short gamepad feedback frame");
continue;
};
read_mgr
.send_gamepad_feedback(session, message.to_vec())
.await;
}
});
tokio::select! {
_ = write_handle => {}
_ = read_handle => {}
}
tracing::info!("gamepad service disconnected");
}
}
pub async fn run_stats_ipc_listener( pub async fn run_stats_ipc_listener(
socket_path: PathBuf, socket_path: PathBuf,
stats_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>, stats_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
+16
View File
@@ -84,6 +84,15 @@ struct Args {
#[arg(long, env = "NESTRI_MAX_BITRATE")] #[arg(long, env = "NESTRI_MAX_BITRATE")]
max_bitrate_kbps: Option<u32>, max_bitrate_kbps: Option<u32>,
/// Path for the gamepad IPC socket (neshub ↔ nesgamepad). neshub
/// listens; the gamepad service dials in.
#[arg(
long,
env = "NESTRI_GAMEPAD_IPC",
default_value = "/tmp/nestri-gamepad.sock"
)]
gamepad_ipc: PathBuf,
/// Socket nescope sends screenshots on. neshub listens; nescope dials out. /// Socket nescope sends screenshots on. neshub listens; nescope dials out.
#[arg( #[arg(
long, long,
@@ -362,6 +371,12 @@ async fn main() -> Result<()> {
async move { ipc_listener::run_stats_ipc_listener(stats_ipc, stx).await } async move { ipc_listener::run_stats_ipc_listener(stats_ipc, stx).await }
}); });
tokio::spawn({
let mgr = session_manager.clone();
let gamepad_ipc = args.gamepad_ipc.clone();
async move { ipc_listener::run_gamepad_ipc_listener(gamepad_ipc, mgr).await }
});
let ticket_ipc = args.ticket_ipc.clone(); let ticket_ipc = args.ticket_ipc.clone();
tokio::spawn({ tokio::spawn({
// The endpoint rather than a ticket made from it: the addresses it can // The endpoint rather than a ticket made from it: the addresses it can
@@ -449,6 +464,7 @@ async fn main() -> Result<()> {
let _ = std::fs::remove_file(&args.audio_ipc); let _ = std::fs::remove_file(&args.audio_ipc);
let _ = std::fs::remove_file(&args.input_ipc); let _ = std::fs::remove_file(&args.input_ipc);
let _ = std::fs::remove_file(&args.stats_ipc); let _ = std::fs::remove_file(&args.stats_ipc);
let _ = std::fs::remove_file(&args.gamepad_ipc);
let _ = std::fs::remove_file("/tmp/nescapture-cmd.sock"); let _ = std::fs::remove_file("/tmp/nescapture-cmd.sock");
let _ = std::fs::remove_file(&args.ticket_ipc); let _ = std::fs::remove_file(&args.ticket_ipc);
Ok(()) Ok(())
+159 -30
View File
@@ -11,8 +11,8 @@ use nesprotocol::{BIDI_CONTROL, BIDI_INPUT, Carrier, STREAM_CURSOR, STREAM_STATS
use nesprotocol::{ControlMode, ReceiverReport, decode_control_mode, decode_receiver_report}; use nesprotocol::{ControlMode, ReceiverReport, decode_control_mode, decode_receiver_report};
use nesprotocol::{FRAME_HDR_LEN, STREAM_VERSION, encode_frame}; use nesprotocol::{FRAME_HDR_LEN, STREAM_VERSION, encode_frame};
use nesprotocol::{ use nesprotocol::{
MSG_CLIENT_CAPS, MSG_CONTROL_MODE, MSG_ENCODE_SETTINGS, MSG_IDR_REQUEST, MSG_INPUT_BATCH, MSG_CLIENT_CAPS, MSG_CONTROL_MODE, MSG_ENCODE_SETTINGS, MSG_GAMEPAD, MSG_GAMEPAD_FEEDBACK,
MSG_RECEIVER_REPORT, MSG_IDR_REQUEST, MSG_INPUT_BATCH, MSG_RECEIVER_REPORT,
}; };
use crate::control::{Controller, PathView}; use crate::control::{Controller, PathView};
@@ -60,6 +60,14 @@ pub struct ClientSession {
latest_report: Arc<std::sync::Mutex<Option<ReceiverReport>>>, latest_report: Arc<std::sync::Mutex<Option<ReceiverReport>>>,
relay_ms: Arc<AtomicU32>, relay_ms: Arc<AtomicU32>,
input_broadcast: tokio::sync::broadcast::Sender<Vec<u8>>, input_broadcast: tokio::sync::broadcast::Sender<Vec<u8>>,
/// This client's number on the gamepad socket, so the box's side can tell
/// two clients' controllers apart. Assigned by the manager, never reused
/// within one hub's lifetime.
gamepad_session: u32,
gamepad_tx: tokio::sync::broadcast::Sender<Vec<u8>>,
/// Rumble and the like, for this client's input stream to carry back.
send_gamepad_feedback: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
pending_gamepad_feedback: Option<tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>>,
idr_cmd_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>, idr_cmd_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
controller: Arc<Mutex<Controller>>, controller: Arc<Mutex<Controller>>,
send_video: tokio::sync::mpsc::UnboundedSender<Vec<u8>>, send_video: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
@@ -92,10 +100,13 @@ impl ClientSession {
/// A client with no connections yet. They attach as they are accepted. /// A client with no connections yet. They attach as they are accepted.
pub fn new( pub fn new(
input_broadcast: tokio::sync::broadcast::Sender<Vec<u8>>, input_broadcast: tokio::sync::broadcast::Sender<Vec<u8>>,
gamepad_session: u32,
gamepad_tx: tokio::sync::broadcast::Sender<Vec<u8>>,
relay_ms: Arc<AtomicU32>, relay_ms: Arc<AtomicU32>,
idr_cmd_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>, idr_cmd_tx: tokio::sync::mpsc::UnboundedSender<Vec<u8>>,
controller: Arc<Mutex<Controller>>, controller: Arc<Mutex<Controller>>,
) -> Self { ) -> Self {
let (gamepad_feedback_tx, gamepad_feedback_rx) = tokio::sync::mpsc::unbounded_channel();
let (video_tx, video_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>(); let (video_tx, video_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
let (audio_tx, audio_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>(); let (audio_tx, audio_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
let (cursor_tx, cursor_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>(); let (cursor_tx, cursor_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
@@ -108,6 +119,10 @@ impl ClientSession {
latest_report: Arc::new(std::sync::Mutex::new(None)), latest_report: Arc::new(std::sync::Mutex::new(None)),
relay_ms, relay_ms,
input_broadcast, input_broadcast,
gamepad_session,
gamepad_tx,
send_gamepad_feedback: gamepad_feedback_tx,
pending_gamepad_feedback: Some(gamepad_feedback_rx),
idr_cmd_tx, idr_cmd_tx,
controller, controller,
send_video: video_tx, send_video: video_tx,
@@ -177,10 +192,18 @@ impl ClientSession {
})); }));
} }
Carrier::Input => { Carrier::Input => {
let Some(feedback) = self.pending_gamepad_feedback.take() else {
debug!("input carrier attached twice; ignoring the second");
return;
};
self.other_conns.push(conn.clone()); self.other_conns.push(conn.clone());
let broadcast = self.input_broadcast.clone(); let broadcast = self.input_broadcast.clone();
let gamepad = Gamepads {
session: self.gamepad_session,
to_box: self.gamepad_tx.clone(),
};
self.tasks.push(tokio::spawn(async move { self.tasks.push(tokio::spawn(async move {
run_input_reader(conn, broadcast).await run_input_reader(conn, broadcast, gamepad, feedback).await
})); }));
} }
Carrier::Control => { Carrier::Control => {
@@ -279,6 +302,13 @@ impl ClientSession {
} }
} }
/// Hand one gamepad feedback message to this client's input stream.
pub fn send_gamepad_feedback(&self, message: Vec<u8>) {
if let Err(e) = self.send_gamepad_feedback.send(message) {
debug!("failed to send gamepad feedback: {e}");
}
}
pub fn send_stats_data(&self, data: Vec<u8>) { pub fn send_stats_data(&self, data: Vec<u8>) {
if let Err(e) = self.send_stats.send(data) { if let Err(e) = self.send_stats.send(data) {
debug!("failed to send stats data: {e}"); debug!("failed to send stats data: {e}");
@@ -293,10 +323,14 @@ impl ClientSession {
/// Shared because input and control differ only in which messages they expect: /// Shared because input and control differ only in which messages they expect:
/// the framing, the announcement and the reconnect behaviour are the same, and /// the framing, the announcement and the reconnect behaviour are the same, and
/// two copies of that would drift. /// two copies of that would drift.
///
/// With `outbound`, the stream's other half stays open and carries those
/// frames back to the client; without, it is finished once announced.
async fn run_framed_reader<F, Fut>( async fn run_framed_reader<F, Fut>(
conn: Connection, conn: Connection,
stream_type: u8, stream_type: u8,
label: &'static str, label: &'static str,
mut outbound: Option<(u8, tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>)>,
mut handle: F, mut handle: F,
) where ) where
F: FnMut(u8, Vec<u8>) -> Fut, F: FnMut(u8, Vec<u8>) -> Fut,
@@ -314,24 +348,60 @@ async fn run_framed_reader<F, Fut>(
debug!("{label} type byte write failed"); debug!("{label} type byte write failed");
break; break;
} }
let _ = send.finish(); if outbound.is_none() {
debug!("{label} bidi stream ready, reading framed messages"); let _ = send.finish();
loop {
// [4B len][1B type][2B seq][payload]
let mut len_buf = [0u8; 4];
if recv.read_exact(&mut len_buf).await.is_err() {
break;
}
let frame_len = u32::from_le_bytes(len_buf) as usize;
if frame_len < 3 || frame_len > 65536 {
break;
}
let mut frame = vec![0u8; frame_len];
if recv.read_exact(&mut frame).await.is_err() {
break;
}
handle(frame[0], frame[3..].to_vec()).await;
} }
debug!("{label} bidi stream ready, reading framed messages");
// Frames are read on a task of their own. Reading one is
// several awaits, and a read cancelled halfway -- which is what
// waiting on outbound frames beside it would do -- loses the
// framing for the rest of the stream.
let (frames_tx, mut frames) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
let reader = tokio::spawn(async move {
loop {
// [4B len][1B type][2B seq][payload]
let mut len_buf = [0u8; 4];
if recv.read_exact(&mut len_buf).await.is_err() {
break;
}
let frame_len = u32::from_le_bytes(len_buf) as usize;
if frame_len < 3 || frame_len > 65536 {
break;
}
let mut frame = vec![0u8; frame_len];
if recv.read_exact(&mut frame).await.is_err() {
break;
}
if frames_tx.send(frame).is_err() {
break;
}
}
});
let mut seq: u16 = 0;
loop {
let outgoing = async {
match outbound.as_mut() {
Some((_, rx)) => rx.recv().await,
None => std::future::pending().await,
}
};
tokio::select! {
frame = frames.recv() => match frame {
Some(frame) => handle(frame[0], frame[3..].to_vec()).await,
None => break,
},
Some(payload) = outgoing => {
let msg_type = outbound.as_ref().map_or(0, |(t, _)| *t);
let mut buf = Vec::with_capacity(FRAME_HDR_LEN + payload.len());
encode_frame(&mut buf, msg_type, seq, &payload);
seq = seq.wrapping_add(1);
if send.write_all(&buf).await.is_err() {
break;
}
}
}
}
reader.abort();
} }
Err(e) => { Err(e) => {
debug!("{label} open_bi failed: {e}"); debug!("{label} open_bi failed: {e}");
@@ -383,22 +453,46 @@ fn split_input_events(payload: &[u8]) -> Vec<Vec<u8>> {
out out
} }
/// Input events, and nothing else. /// Where one client's gamepad messages go.
struct Gamepads {
session: u32,
to_box: tokio::sync::broadcast::Sender<Vec<u8>>,
}
/// Input events and gamepad messages, and nothing else.
/// ///
/// On its own connection so a keypress never waits behind a video keyframe. /// On its own connection so a keypress never waits behind a video keyframe.
/// Gamepad messages are forwarded unread, tagged with the client: what a
/// controller is and what to make of it is the box's side to decide, not the
/// hub's. The stream's return half carries that side's feedback -- rumble --
/// back.
async fn run_input_reader( async fn run_input_reader(
conn: Connection, conn: Connection,
input_broadcast: tokio::sync::broadcast::Sender<Vec<u8>>, input_broadcast: tokio::sync::broadcast::Sender<Vec<u8>>,
gamepads: Gamepads,
feedback: tokio::sync::mpsc::UnboundedReceiver<Vec<u8>>,
) { ) {
run_framed_reader(conn, BIDI_INPUT, "input", |msg_type, payload| { let outbound = Some((MSG_GAMEPAD_FEEDBACK, feedback));
run_framed_reader(conn, BIDI_INPUT, "input", outbound, |msg_type, payload| {
let input_broadcast = input_broadcast.clone(); let input_broadcast = input_broadcast.clone();
let to_box = gamepads.to_box.clone();
let session = gamepads.session;
async move { async move {
if msg_type != MSG_INPUT_BATCH { match msg_type {
debug!("unknown input msg type: {msg_type}"); MSG_INPUT_BATCH => {
return; for event in split_input_events(&payload) {
} let _ = input_broadcast.send(event);
for event in split_input_events(&payload) { }
let _ = input_broadcast.send(event); }
MSG_GAMEPAD => {
let mut frame = Vec::with_capacity(6 + payload.len());
nesprotocol::gamepad::encode_ipc(&mut frame, session, &payload);
// An error is nobody listening: the box's gamepad side is
// not up, and a controller it never heard of will be asked
// for again once it is.
let _ = to_box.send(frame);
}
other => debug!("unknown input msg type: {other}"),
} }
} }
}) })
@@ -413,7 +507,7 @@ async fn run_control_reader(
controller: Arc<Mutex<Controller>>, controller: Arc<Mutex<Controller>>,
awaiting_keyframe: Arc<AtomicBool>, awaiting_keyframe: Arc<AtomicBool>,
) { ) {
run_framed_reader(conn, BIDI_CONTROL, "control", |msg_type, payload| { run_framed_reader(conn, BIDI_CONTROL, "control", None, |msg_type, payload| {
let idr_cmd_tx = idr_cmd_tx.clone(); let idr_cmd_tx = idr_cmd_tx.clone();
let latest_report = latest_report.clone(); let latest_report = latest_report.clone();
let controller = controller.clone(); let controller = controller.clone();
@@ -658,6 +752,9 @@ fn is_keyframe(payload: &[u8]) -> bool {
pub struct SessionManager { pub struct SessionManager {
sessions: Arc<Mutex<HashMap<iroh::EndpointId, ClientSession>>>, sessions: Arc<Mutex<HashMap<iroh::EndpointId, ClientSession>>>,
/// Every client's gamepad messages, already framed for the gamepad socket.
gamepad_tx: tokio::sync::broadcast::Sender<Vec<u8>>,
next_gamepad_session: AtomicU32,
/// Video bytes, split by what they were. /// Video bytes, split by what they were.
/// ///
/// One counter could not tell an encoder ignoring its bitrate target from a /// One counter could not tell an encoder ignoring its bitrate target from a
@@ -685,8 +782,11 @@ pub struct SessionManager {
impl SessionManager { impl SessionManager {
pub fn new() -> Self { pub fn new() -> Self {
let (gamepad_tx, _) = tokio::sync::broadcast::channel(256);
Self { Self {
sessions: Arc::new(Mutex::new(HashMap::new())), sessions: Arc::new(Mutex::new(HashMap::new())),
gamepad_tx,
next_gamepad_session: AtomicU32::new(1),
video_key_bytes: AtomicU64::new(0), video_key_bytes: AtomicU64::new(0),
video_delta_bytes: AtomicU64::new(0), video_delta_bytes: AtomicU64::new(0),
keyframes: AtomicU64::new(0), keyframes: AtomicU64::new(0),
@@ -720,6 +820,8 @@ impl SessionManager {
let session = sessions.entry(id).or_insert_with(|| { let session = sessions.entry(id).or_insert_with(|| {
ClientSession::new( ClientSession::new(
input_broadcast, input_broadcast,
self.next_gamepad_session.fetch_add(1, Ordering::Relaxed),
self.gamepad_tx.clone(),
self.relay_ms.clone(), self.relay_ms.clone(),
idr_cmd_tx, idr_cmd_tx,
controller, controller,
@@ -740,8 +842,35 @@ impl SessionManager {
// session as removed four times over -- three of them describing a // session as removed four times over -- three of them describing a
// session that had already gone, which reads like four clients // session that had already gone, which reads like four clients
// leaving. // leaving.
if sessions.remove(id).is_some() { if let Some(session) = sessions.remove(id) {
info!(remote = %id.fmt_short(), "client session removed ({} remaining)", sessions.len()); info!(remote = %id.fmt_short(), "client session removed ({} remaining)", sessions.len());
// Its controllers go with it. The client cannot say so itself --
// it is the thing that went -- and a controller left plugged in
// would still be there for the game, held by nobody.
let mut message = Vec::with_capacity(1);
nesprotocol::gamepad::PadMessage::SessionEnd.encode(&mut message);
let mut frame = Vec::with_capacity(7);
nesprotocol::gamepad::encode_ipc(&mut frame, session.gamepad_session, &message);
let _ = self.gamepad_tx.send(frame);
}
}
/// A receiver for every client's gamepad messages, for the gamepad socket.
pub fn gamepad_messages(&self) -> tokio::sync::broadcast::Receiver<Vec<u8>> {
self.gamepad_tx.subscribe()
}
/// Route gamepad feedback to the client it names.
///
/// A client that has gone is not an error: rumble a game started a moment
/// before its player left has nowhere to go, and that is fine.
pub async fn send_gamepad_feedback(&self, gamepad_session: u32, message: Vec<u8>) {
let sessions = self.sessions.lock().await;
if let Some(session) = sessions
.values()
.find(|s| s.gamepad_session == gamepad_session)
{
session.send_gamepad_feedback(message);
} }
} }
+19 -1
View File
@@ -221,7 +221,8 @@ const WRITABLE: &[(&str, &str)] = &[
/// ///
/// Ported from the nine init scripts this replaces, and the ordering is theirs: /// Ported from the nine init scripts this replaces, and the ordering is theirs:
/// the bus before anything that speaks on it, audio before whatever plays into /// the bus before anything that speaks on it, audio before whatever plays into
/// it, and the hub last because it binds the sockets the rest connect to. /// it, and the hub after them because it binds the sockets the rest connect to.
/// Only something that dials the hub, and waits for it, comes after.
pub const STACK: &[Service] = &[ pub const STACK: &[Service] = &[
Service { Service {
name: "dbus-system", name: "dbus-system",
@@ -337,6 +338,23 @@ pub const STACK: &[Service] = &[
umask: None, umask: None,
ready: None, ready: None,
}, },
Service {
name: "nesgamepad",
argv: &["/usr/bin/nesgamepad"],
env: &[],
// Root, and it has to be: it creates devices through /dev/uinput,
// opens their nodes to the workload, and announces them the way udev
// would -- and libudev ignores an announcement from anyone but root.
user: None,
// Optional: a session without controllers is still played with a
// keyboard and mouse.
cost: "controllers plugged into the client do not reach the game",
required: false,
umask: None,
// It dials the hub rather than the other way round, and redials until
// the hub is there, so nothing waits on it.
ready: None,
},
]; ];
/// The stack as running processes. /// The stack as running processes.
+4
View File
@@ -13,6 +13,10 @@
// compositor handles input through Wayland and opens nothing udev provides, so // compositor handles input through Wayland and opens nothing udev provides, so
// dropping it costs a box nothing and saves it a daemon and a settle. // dropping it costs a box nothing and saves it a daemon and a settle.
// //
// The one thing in a box that does need udev is a game looking for
// controllers, and those are devices `nesgamepad` creates itself -- so it
// stands in for udev for exactly those, and nothing else has to.
//
// # Best effort, one line per failure, each naming a cost // # Best effort, one line per failure, each naming a cost
// //
// Same discipline as the early filesystems: refusing to boot over any one of // Same discipline as the early filesystems: refusing to boot over any one of
+18 -5
View File
@@ -2,8 +2,8 @@
# nestri guest rootfs — the open half # nestri guest rootfs — the open half
# #
# Builds a bootable Arch image containing Mesa (virtio-gpu native context) # Builds a bootable Arch image containing Mesa (virtio-gpu native context)
# and the five open guest components: nesinit, nescope, neshub, neswire, # and the six open guest components: nesinit, nescope, neshub, neswire,
# nescapture. Two leaf targets, selected with `--target`: # nescapture, nesgamepad. Two leaf targets, selected with `--target`:
# #
# runtime_prod stripped, root locked (default: `make build`) # runtime_prod stripped, root locked (default: `make build`)
# runtime_debug debug tools, autologin root (`make build-debug`) # runtime_debug debug tools, autologin root (`make build-debug`)
@@ -141,18 +141,20 @@ COPY apps/nescope apps/nescope
COPY apps/neshub apps/neshub COPY apps/neshub apps/neshub
COPY apps/neswire apps/neswire COPY apps/neswire apps/neswire
COPY apps/nescapture apps/nescapture COPY apps/nescapture apps/nescapture
COPY apps/nesgamepad apps/nesgamepad
FROM nestri-src AS nestri-build FROM nestri-src AS nestri-build
RUN --mount=type=cache,target=/root/.cargo/registry \ RUN --mount=type=cache,target=/root/.cargo/registry \
--mount=type=cache,target=/build/nestri/target \ --mount=type=cache,target=/build/nestri/target \
cargo build --release \ cargo build --release \
-p nesinit -p nescope -p neshub -p neswire -p nescapture && \ -p nesinit -p nescope -p neshub -p neswire -p nescapture -p nesgamepad && \
mkdir -p /artifacts/nestri/usr/bin /artifacts/nestri/usr/lib \ mkdir -p /artifacts/nestri/usr/bin /artifacts/nestri/usr/lib \
/artifacts/nestri/usr/share/vulkan/implicit_layer.d && \ /artifacts/nestri/usr/share/vulkan/implicit_layer.d && \
install -Dm755 target/release/nesinit /artifacts/nestri/usr/bin/nesinit && \ install -Dm755 target/release/nesinit /artifacts/nestri/usr/bin/nesinit && \
install -Dm755 target/release/nescope /artifacts/nestri/usr/bin/nescope && \ install -Dm755 target/release/nescope /artifacts/nestri/usr/bin/nescope && \
install -Dm755 target/release/neshub /artifacts/nestri/usr/bin/neshub && \ install -Dm755 target/release/neshub /artifacts/nestri/usr/bin/neshub && \
install -Dm755 target/release/neswire /artifacts/nestri/usr/bin/neswire && \ install -Dm755 target/release/neswire /artifacts/nestri/usr/bin/neswire && \
install -Dm755 target/release/nesgamepad /artifacts/nestri/usr/bin/nesgamepad && \
install -Dm755 target/release/libnescapture_layer.so \ install -Dm755 target/release/libnescapture_layer.so \
/artifacts/nestri/usr/lib/libnescapture_layer.so && \ /artifacts/nestri/usr/lib/libnescapture_layer.so && \
install -Dm644 apps/nescapture/manifest/VK_LAYER_nescapture.json \ install -Dm644 apps/nescapture/manifest/VK_LAYER_nescapture.json \
@@ -264,7 +266,7 @@ RUN pacman -Syu --noconfirm --needed \
expat zlib llvm-libs lm_sensors elfutils libva shaderc vulkan-icd-loader \ expat zlib llvm-libs lm_sensors elfutils libva shaderc vulkan-icd-loader \
pixman libxkbcommon xcb-util-keysyms xorg-xwayland \ pixman libxkbcommon xcb-util-keysyms xorg-xwayland \
pipewire pipewire-audio pipewire-pulse libpulse wireplumber opus \ pipewire pipewire-audio pipewire-pulse libpulse wireplumber opus \
python libunwind \ python libunwind sdl2-compat \
&& rm -f /usr/share/libalpm/hooks/dbus-reload.hook \ && rm -f /usr/share/libalpm/hooks/dbus-reload.hook \
&& pacman -Rdd --noconfirm systemd systemd-sysvcompat \ && pacman -Rdd --noconfirm systemd systemd-sysvcompat \
&& pacman -Scc --noconfirm && pacman -Scc --noconfirm
@@ -291,6 +293,16 @@ RUN pacman -Syu --noconfirm --needed \
# reads like an ordinary finish. Found 2026-09-12, on the first session that # reads like an ordinary finish. Found 2026-09-12, on the first session that
# got as far as launching one. # got as far as launching one.
# `sdl2-compat` is Wine's controller support, and the only one there is for a
# controller that is not a Steam Input one. Proton's device bus reads evdev
# controllers through SDL2 and nothing else -- its udev backend defers every
# evdev device to the SDL one on purpose -- and it loads the library with
# `dlopen`, so its absence fails nothing at build time or at launch. A game
# simply sees no controller, while `nesgamepad` reports it plugged in. Found
# 2026-09-27, on the first session with one. Real Steam brings SDL in its
# runtime, which is why this is never missed anywhere else. On Arch the
# library is SDL2's API over SDL3, which arrives as its dependency.
# `dbus-reload.hook` is deleted above, before the removal rather than after, # `dbus-reload.hook` is deleted above, before the removal rather than after,
# and it is the whole reason that line is there: the hook runs # and it is the whole reason that line is there: the hook runs
# `/usr/share/libalpm/scripts/systemd-hook`, which systemd owns, so the # `/usr/share/libalpm/scripts/systemd-hook`, which systemd owns, so the
@@ -494,7 +506,7 @@ RUN for intruder in /usr/lib/systemd/systemd /sbin/openrc-init /usr/bin/openrc-i
RUN failed=0; \ RUN failed=0; \
: > /tmp/missing-libs; \ : > /tmp/missing-libs; \
for f in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \ for f in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
/usr/lib/libnescapture_layer.so \ /usr/bin/nesgamepad /usr/lib/libnescapture_layer.so /usr/lib/libSDL2-2.0.so.0 \
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/pipewire-pulse \ /usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/pipewire-pulse \
/usr/bin/wireplumber /usr/bin/ip \ /usr/bin/wireplumber /usr/bin/ip \
/usr/lib/libgallium-*.so /usr/lib/libEGL_mesa.so.0 \ /usr/lib/libgallium-*.so /usr/lib/libEGL_mesa.so.0 \
@@ -517,6 +529,7 @@ RUN failed=0; \
fi fi
RUN for required in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \ RUN for required in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
/usr/bin/nesgamepad /usr/lib/libSDL2-2.0.so.0 \
/usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/pipewire-pulse \ /usr/bin/dbus-daemon /usr/bin/pipewire /usr/bin/pipewire-pulse \
/usr/bin/wireplumber /usr/bin/ip /usr/bin/python3 \ /usr/bin/wireplumber /usr/bin/ip /usr/bin/python3 \
/usr/share/steam/compatibilitytools.d/proton-cachyos/proton; do \ /usr/share/steam/compatibilitytools.d/proton-cachyos/proton; do \
+5 -5
View File
@@ -1,10 +1,10 @@
# build/ — the guest rootfs # build/ — the guest rootfs
Builds a bootable Arch image for the box's virtio-blk root: Mesa (virtio-gpu Builds a bootable Arch image for the box's virtio-blk root: Mesa (virtio-gpu
native context) plus the five open guest components — native context) plus the six open guest components —
[`nesinit`](../apps/nesinit), [`nescope`](../apps/nescope), [`nesinit`](../apps/nesinit), [`nescope`](../apps/nescope),
[`neshub`](../apps/neshub), [`neswire`](../apps/neswire), [`neshub`](../apps/neshub), [`neswire`](../apps/neswire),
[`nescapture`](../apps/nescapture) — laid out [`nescapture`](../apps/nescapture), [`nesgamepad`](../apps/nesgamepad) — laid out
the way [borealis](https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/main/project-borealis) the way [borealis](https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/main/project-borealis)
lays out its `build/`: one big multi-stage `Containerfile`, `--target` picks the lays out its `build/`: one big multi-stage `Containerfile`, `--target` picks the
flavor, `etc/` holds the files that get overlaid onto the image verbatim. flavor, `etc/` holds the files that get overlaid onto the image verbatim.
@@ -49,7 +49,7 @@ Three things worth knowing about how this is put together:
explicit sanity check for doesn't exist here to check for. explicit sanity check for doesn't exist here to check for.
3. **One `cargo build --release --workspace`, not one stage per binary.** 3. **One `cargo build --release --workspace`, not one stage per binary.**
`nescope`, `neshub`, `neswire` and `nescapture` share one Cargo workspace `nescope`, `neshub`, `neswire`, `nescapture` and `nesgamepad` share one Cargo workspace
and one `Cargo.lock` — a BuildKit cache mount on `target/` gives cargo's and one `Cargo.lock` — a BuildKit cache mount on `target/` gives cargo's
own incremental compiler per-crate isolation without needing a separate own incremental compiler per-crate isolation without needing a separate
Docker stage (and a separate full rebuild of `nesprotocol`) per binary. Docker stage (and a separate full rebuild of `nesprotocol`) per binary.
@@ -254,9 +254,9 @@ needs — was paid for an init system that is no longer here.
| was | now | | was | now |
|---|---| |---|---|
| `devfs`, `dmesg`, `udev`, `udev-trigger` | `devtmpfs` makes the nodes; init sets the two modes that matter. The compositor takes input through Wayland and opens nothing `udev` provides | | `devfs`, `dmesg`, `udev`, `udev-trigger` | `devtmpfs` makes the nodes; init sets the two modes that matter. The compositor takes input through Wayland and opens nothing `udev` provides. Controllers are the one thing a game finds through `udev`, and `nesgamepad` announces the ones it creates itself |
| `guest-net`, `hostname`, `xdg-runtime`, `cgroups` | init, before it dials out | | `guest-net`, `hostname`, `xdg-runtime`, `cgroups` | init, before it dials out |
| `dbus`, `dbus-session`, `pipewire`, `wireplumber`, `neshub`, `neswire` | a table compiled into `nesinit` | | `dbus`, `dbus-session`, `pipewire`, `wireplumber`, `neshub`, `neswire`, `nesgamepad` | a table compiled into `nesinit` |
| `nescope` in the `default` runlevel | **not a service.** It wraps the workload and is started by a launch, with that launch's geometry, and dies with it | | `nescope` in the `default` runlevel | **not a service.** It wraps the workload and is started by a launch, with that launch's geometry, and dies with it |
| `agetty` on `hvc0` | nothing. See below | | `agetty` on `hvc0` | nothing. See below |
| `/etc/fstab` | init's own mounts, and shares named in the boot descriptor | | `/etc/fstab` | init's own mounts, and shares named in the boot descriptor |
+14
View File
@@ -14,6 +14,20 @@
# NTSYNC is a great improvement over fsync and esync approaches. # NTSYNC is a great improvement over fsync and esync approaches.
CONFIG_NTSYNC=y CONFIG_NTSYNC=y
# ── Controllers ──────────────────────────────────────────
# A controller of a family the box can rebuild is made here as the device
# itself, from the real one's descriptor, through uhid; hid-generic binds to
# it and gives it a hidraw node. That node is what Proton reads a controller through
# when it wants the device rather than a gamepad abstraction of it, and it is
# the only way a game that parses a controller's own reports -- to tell a
# DualShock from an Xbox pad, say, and show the right buttons -- can work.
#
# Two generic switches and no vendor drivers, on purpose: games read the
# hidraw node, not whatever a vendor driver would have made of the device, so
# no controller family needs a switch of its own. HID_GENERIC is already on.
CONFIG_UHID=y
CONFIG_HIDRAW=y
# ── Timers ─────────────────────────────────────────────── # ── Timers ───────────────────────────────────────────────
# The one that cost a day of silent audio. The guest has no sound hardware, so # The one that cost a day of silent audio. The guest has no sound hardware, so
# PipeWire drives its whole graph off a timerfd at a 2.67ms cycle, which a # PipeWire drives its whole graph off a timerfd at a 2.67ms cycle, which a
+392
View File
@@ -0,0 +1,392 @@
// Gamepads: what the client says about the controllers plugged into it, and
// what the box says back.
//
// Client → box travels as `MSG_GAMEPAD` frames on the input stream, one
// message per frame. Box → client travels as `MSG_GAMEPAD_FEEDBACK` frames on
// the same stream's other half. The hub forwards both without reading them,
// tagged with which client they belong to (see `encode_ipc`).
//
// # The client describes the controller, it does not choose one
//
// A `Connect` carries the real device's identity as the client saw it, and the
// box decides what a game inside it should see. Nothing here names a
// controller family, and there is no default one. The identity is only what
// every platform's gamepad API can tell -- vendor, product, name -- so a
// client needs nothing beyond such an API, and all the knowledge of what a
// controller looks like to a game lives in the box, in one place.
//
// # State, not edges
//
// A `State` is the whole controller every time. A lost or reordered edge would
// leave a button held forever; a snapshot can only ever be stale until the next
// one. Button names are positional (south, east, ...), after the kernel's own
// gamepad layout, so no face-button lettering from any one vendor is implied.
/// A controller appeared on the client. Payload:
/// `[slot][vendor u16][product u16][name_len u8][name]`.
pub const PAD_CONNECT: u8 = 0x00;
/// The whole of a controller's state. Payload:
/// `[slot][buttons u32][lx i16][ly i16][rx i16][ry i16][lt u16][rt u16]`.
pub const PAD_STATE: u8 = 0x01;
/// A controller went away. Payload: `[slot]`.
pub const PAD_DISCONNECT: u8 = 0x02;
/// Rumble to play on a client's controller. Payload:
/// `[slot][strong u16][weak u16][duration_ms u16]`. Both magnitudes zero is a
/// stop; a duration of zero plays until the next message for that slot.
pub const PAD_RUMBLE: u8 = 0x80;
/// The box has no controller in this slot: send its `Connect` again. Payload:
/// `[slot]`.
///
/// Asked when state arrives for a slot the box does not know, which is what a
/// client sees after the box's side restarted under it. Re-announcing is
/// cheaper than either end trying to remember the other's view.
pub const PAD_ANNOUNCE: u8 = 0x81;
/// Everything a client had plugged in is gone, because the client is.
///
/// Only ever said by the hub, on the IPC socket: a client that disconnects
/// cannot say it itself, and a controller left behind would still be plugged
/// into the game.
pub const PAD_SESSION_END: u8 = 0x40;
/// The longest name carried. A `Connect` with a longer one is cut at a
/// character boundary.
pub const NAME_MAX: usize = 255;
/// Buttons, as bits of [`PadState::buttons`].
pub mod button {
pub const SOUTH: u32 = 1 << 0;
pub const EAST: u32 = 1 << 1;
pub const NORTH: u32 = 1 << 2;
pub const WEST: u32 = 1 << 3;
pub const LEFT_SHOULDER: u32 = 1 << 4;
pub const RIGHT_SHOULDER: u32 = 1 << 5;
/// Digital trigger clicks. Sent alongside the analog value, because some
/// controllers report both and a game may read either.
pub const LEFT_TRIGGER: u32 = 1 << 6;
pub const RIGHT_TRIGGER: u32 = 1 << 7;
pub const SELECT: u32 = 1 << 8;
pub const START: u32 = 1 << 9;
pub const MODE: u32 = 1 << 10;
pub const LEFT_STICK: u32 = 1 << 11;
pub const RIGHT_STICK: u32 = 1 << 12;
pub const DPAD_UP: u32 = 1 << 13;
pub const DPAD_DOWN: u32 = 1 << 14;
pub const DPAD_LEFT: u32 = 1 << 15;
pub const DPAD_RIGHT: u32 = 1 << 16;
}
/// Who a controller is, as the client saw it: what every platform's gamepad
/// API can say. Zero in `vendor` or `product` is one that would not.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PadIdentity {
pub vendor: u16,
pub product: u16,
pub name: String,
}
/// One controller's whole state.
///
/// Sticks are full-range `i16` with Linux's orientation: positive x is right,
/// positive y is *down*. Triggers are `0..=u16::MAX`, released to fully
/// pulled. The default is a controller nobody is touching.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PadState {
pub buttons: u32,
pub left_x: i16,
pub left_y: i16,
pub right_x: i16,
pub right_y: i16,
pub left_trigger: u16,
pub right_trigger: u16,
}
/// Client → box.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PadMessage {
Connect {
slot: u8,
identity: PadIdentity,
},
State {
slot: u8,
state: PadState,
},
Disconnect {
slot: u8,
},
/// See [`PAD_SESSION_END`]. Never on the wire from a client.
SessionEnd,
}
/// Box → client.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PadFeedback {
Rumble {
slot: u8,
strong: u16,
weak: u16,
duration_ms: u16,
},
Announce {
slot: u8,
},
}
fn cut_name(name: &str) -> &str {
if name.len() <= NAME_MAX {
return name;
}
let mut end = NAME_MAX;
while !name.is_char_boundary(end) {
end -= 1;
}
&name[..end]
}
impl PadMessage {
pub fn encode(&self, buf: &mut Vec<u8>) {
match self {
Self::Connect { slot, identity } => {
let name = cut_name(&identity.name);
buf.reserve(7 + name.len());
buf.push(PAD_CONNECT);
buf.push(*slot);
buf.extend_from_slice(&identity.vendor.to_le_bytes());
buf.extend_from_slice(&identity.product.to_le_bytes());
buf.push(name.len() as u8);
buf.extend_from_slice(name.as_bytes());
}
Self::State { slot, state } => {
buf.reserve(18);
buf.push(PAD_STATE);
buf.push(*slot);
buf.extend_from_slice(&state.buttons.to_le_bytes());
for axis in [state.left_x, state.left_y, state.right_x, state.right_y] {
buf.extend_from_slice(&axis.to_le_bytes());
}
buf.extend_from_slice(&state.left_trigger.to_le_bytes());
buf.extend_from_slice(&state.right_trigger.to_le_bytes());
}
Self::Disconnect { slot } => {
buf.push(PAD_DISCONNECT);
buf.push(*slot);
}
Self::SessionEnd => buf.push(PAD_SESSION_END),
}
}
/// `None` for anything short, unknown, or with a name that is not UTF-8.
pub fn decode(data: &[u8]) -> Option<Self> {
let (&kind, rest) = data.split_first()?;
match kind {
PAD_CONNECT => {
let fixed = rest.get(..6)?;
let name_len = fixed[5] as usize;
let name = rest.get(6..6 + name_len)?;
Some(Self::Connect {
slot: fixed[0],
identity: PadIdentity {
vendor: u16::from_le_bytes([fixed[1], fixed[2]]),
product: u16::from_le_bytes([fixed[3], fixed[4]]),
name: std::str::from_utf8(name).ok()?.to_owned(),
},
})
}
PAD_STATE => {
let b = rest.get(..17)?;
let i16_at = |i: usize| i16::from_le_bytes([b[i], b[i + 1]]);
let u16_at = |i: usize| u16::from_le_bytes([b[i], b[i + 1]]);
Some(Self::State {
slot: b[0],
state: PadState {
buttons: u32::from_le_bytes([b[1], b[2], b[3], b[4]]),
left_x: i16_at(5),
left_y: i16_at(7),
right_x: i16_at(9),
right_y: i16_at(11),
left_trigger: u16_at(13),
right_trigger: u16_at(15),
},
})
}
PAD_DISCONNECT => Some(Self::Disconnect {
slot: *rest.first()?,
}),
PAD_SESSION_END => Some(Self::SessionEnd),
_ => None,
}
}
}
impl PadFeedback {
pub fn encode(&self, buf: &mut Vec<u8>) {
match *self {
Self::Rumble {
slot,
strong,
weak,
duration_ms,
} => {
buf.reserve(8);
buf.push(PAD_RUMBLE);
buf.push(slot);
buf.extend_from_slice(&strong.to_le_bytes());
buf.extend_from_slice(&weak.to_le_bytes());
buf.extend_from_slice(&duration_ms.to_le_bytes());
}
Self::Announce { slot } => {
buf.push(PAD_ANNOUNCE);
buf.push(slot);
}
}
}
pub fn decode(data: &[u8]) -> Option<Self> {
let (&kind, rest) = data.split_first()?;
match kind {
PAD_RUMBLE => {
let b = rest.get(..7)?;
Some(Self::Rumble {
slot: b[0],
strong: u16::from_le_bytes([b[1], b[2]]),
weak: u16::from_le_bytes([b[3], b[4]]),
duration_ms: u16::from_le_bytes([b[5], b[6]]),
})
}
PAD_ANNOUNCE => Some(Self::Announce {
slot: *rest.first()?,
}),
_ => None,
}
}
}
// ── Hub ↔ box-side IPC ──────────────────────────────────────────
//
// `[u16 LE len][u32 LE session][message]`, where `len` counts the session and
// the message. The session number is the hub's name for one client, so two
// clients can both have a slot 0 without meeting. It means nothing outside the
// hub's own lifetime.
/// Frame one message for the IPC socket, in either direction.
pub fn encode_ipc(buf: &mut Vec<u8>, session: u32, message: &[u8]) {
let len = 4 + message.len();
buf.reserve(2 + len);
buf.extend_from_slice(&(len as u16).to_le_bytes());
buf.extend_from_slice(&session.to_le_bytes());
buf.extend_from_slice(message);
}
/// Split one IPC frame's body (after the length) into session and message.
pub fn decode_ipc(body: &[u8]) -> Option<(u32, &[u8])> {
let session = u32::from_le_bytes(body.get(..4)?.try_into().ok()?);
Some((session, &body[4..]))
}
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(message: PadMessage) {
let mut buf = Vec::new();
message.encode(&mut buf);
assert_eq!(PadMessage::decode(&buf), Some(message));
}
#[test]
fn every_message_survives_the_wire() {
round_trip(PadMessage::Connect {
slot: 3,
identity: PadIdentity {
vendor: 0x054c,
product: 0x09cc,
name: "Wireless Controller".into(),
},
});
round_trip(PadMessage::State {
slot: 15,
state: PadState {
buttons: button::SOUTH | button::DPAD_RIGHT,
left_x: i16::MIN,
left_y: i16::MAX,
right_x: -1,
right_y: 1,
left_trigger: u16::MAX,
right_trigger: 7,
},
});
round_trip(PadMessage::Disconnect { slot: 0 });
round_trip(PadMessage::SessionEnd);
}
#[test]
fn feedback_survives_the_wire() {
for feedback in [
PadFeedback::Rumble {
slot: 2,
strong: 0xffff,
weak: 0x1234,
duration_ms: 250,
},
PadFeedback::Announce { slot: 9 },
] {
let mut buf = Vec::new();
feedback.encode(&mut buf);
assert_eq!(PadFeedback::decode(&buf), Some(feedback));
}
}
#[test]
fn a_long_name_is_cut_on_a_character_boundary() {
// Three bytes per character, so 255 falls exactly on one and 256 would
// not: a byte cut would produce a name that no longer decodes.
let name = "コ".repeat(100);
let mut buf = Vec::new();
PadMessage::Connect {
slot: 0,
identity: PadIdentity {
vendor: 0,
product: 0,
name: format!("x{name}"),
},
}
.encode(&mut buf);
let Some(PadMessage::Connect { identity, .. }) = PadMessage::decode(&buf) else {
panic!("did not decode");
};
assert!(identity.name.len() <= NAME_MAX);
assert!(identity.name.starts_with('x'));
}
#[test]
fn anything_short_is_refused_rather_than_read_past() {
let mut buf = Vec::new();
PadMessage::State {
slot: 1,
state: PadState::default(),
}
.encode(&mut buf);
for len in 0..buf.len() {
assert_eq!(PadMessage::decode(&buf[..len]), None, "length {len}");
}
assert_eq!(PadMessage::decode(&[0x7f, 0]), None);
}
#[test]
fn ipc_frames_carry_their_session() {
let mut message = Vec::new();
PadMessage::Disconnect { slot: 4 }.encode(&mut message);
let mut buf = Vec::new();
encode_ipc(&mut buf, 0xdead_beef, &message);
let len = u16::from_le_bytes([buf[0], buf[1]]) as usize;
assert_eq!(len, buf.len() - 2);
let (session, body) = decode_ipc(&buf[2..]).unwrap();
assert_eq!(session, 0xdead_beef);
assert_eq!(
PadMessage::decode(body),
Some(PadMessage::Disconnect { slot: 4 })
);
}
}
+11
View File
@@ -4,6 +4,7 @@
pub mod datagram; pub mod datagram;
pub mod delay; pub mod delay;
pub mod gamepad;
pub mod input; pub mod input;
#[cfg(feature = "lifecycle")] #[cfg(feature = "lifecycle")]
pub mod lifecycle; pub mod lifecycle;
@@ -105,6 +106,16 @@ pub const MSG_RECEIVER_REPORT: u8 = 0x13;
/// Who decides the bitrate, and the ceiling to decide within (desktop → hub). /// Who decides the bitrate, and the ceiling to decide within (desktop → hub).
pub const MSG_CONTROL_MODE: u8 = 0x14; pub const MSG_CONTROL_MODE: u8 = 0x14;
pub const MSG_INPUT_BATCH: u8 = 0xFE; // batched input events (desktop → hub) pub const MSG_INPUT_BATCH: u8 = 0xFE; // batched input events (desktop → hub)
/// One gamepad message (desktop → hub → the box's gamepad service), on the input
/// stream. See the [`gamepad`] module.
///
/// A frame of its own rather than an event inside an input batch: a batch is a
/// run of fixed-width events the hub splits by type, and a controller's name
/// has no fixed width.
pub const MSG_GAMEPAD: u8 = 0xFD;
/// Rumble and re-announce requests (box → hub → desktop), on the input
/// stream's return half.
pub const MSG_GAMEPAD_FEEDBACK: u8 = 0xFC;
/// Build a frame body: `[u8 type] [u16 LE seq] [payload]`. /// Build a frame body: `[u8 type] [u16 LE seq] [payload]`.
/// ///