mirror of
https://github.com/nestriness/nestri.git
synced 2026-10-02 15:42:24 +03:00
Adds `nesgamepad`, basically "diet-coke vimputti" for Nestri's microVM needs here. Comes with direct mapping for: - Sony - DualShock 4 (v2), DualSense - Microsoft - Xbox 360 pad, Xbox One S pad - Nintendo - Pro Controller - Generic pad for unknown controllers --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
327 lines
11 KiB
Rust
327 lines
11 KiB
Rust
//! 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);
|
|
}
|
|
}
|