Initial gamepad support

This commit is contained in:
Jeremy Soller
2026-06-23 17:29:25 -06:00
parent 97dc1d2aa4
commit ff7492949e
5 changed files with 504 additions and 121 deletions
Generated
+1 -1
View File
@@ -1957,7 +1957,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rehid"
version = "0.1.0"
source = "git+https://gitlab.redox-os.org/redox-os/rehid.git#39d7ef05c97fd4e5e2cfd38b1a21f20d888c5e00"
source = "git+https://gitlab.redox-os.org/redox-os/rehid.git#da987bc6d0f043bb025ece0d35804da2b250fab3"
dependencies = [
"hidreport",
"log",
+367 -106
View File
@@ -1,9 +1,15 @@
use anyhow::{Context, Result};
use std::{env, thread, time};
use std::{
collections::{BTreeMap, HashMap},
env,
sync::mpsc,
thread, time,
};
use inputd::ProducerHandle;
use orbclient::KeyEvent as OrbKeyEvent;
use rehid::{
hidreport::{Report, Usage},
report_desc::{ReportTy, REPORT_DESC_TY},
report_handler::ReportHandler,
usage_tables::{GenericDesktopUsage, UsagePage},
@@ -16,6 +22,7 @@ use xhcid_interface::{
use crate::descs::HidDescriptor;
mod descs;
mod overrides;
mod reqs;
fn send_key_event(display: &mut ProducerHandle, usage_page: u16, usage: u16, pressed: bool) {
@@ -173,6 +180,15 @@ fn main() -> Result<()> {
.expect(USAGE)
.parse::<u8>()
.expect("Expected integer as input of interface");
let mut report_desc_override = None;
if let Some(override_name) = args.next() {
for (name, bytes) in overrides::OVERRIDES.iter() {
if name == &override_name {
report_desc_override = Some(bytes.to_vec());
break;
}
}
}
let name = format!("{}_{}_{}_hid", scheme, port, interface_num);
common::setup_logging(
@@ -204,7 +220,7 @@ fn main() -> Result<()> {
log::debug!("{:X?}", desc);
let mut endp_count = 0;
let (conf_desc, (if_desc, endp_desc_opt, hid_desc)) = desc
let (conf_desc, (if_desc, endp_desc_opt, hid_desc_opt)) = desc
.config_descs
.iter()
.find_map(|conf_desc| {
@@ -215,16 +231,28 @@ fn main() -> Result<()> {
if endp_desc.ty() == EndpointTy::Interrupt
&& endp_desc.direction() == EndpDirection::In
{
log::warn!(
"using endpoint 0x{:x} {:?} {:?}",
endp_desc.address,
endp_desc.ty(),
endp_desc.direction()
);
Some((endp_count, endp_desc.clone()))
} else {
log::warn!(
"ignoring endpoint 0x{:x} {:?} {:?}",
endp_desc.address,
endp_desc.ty(),
endp_desc.direction()
);
None
}
});
let hid_desc = if_desc.unknown_descs.iter().find_map(|unknown_desc| {
let hid_desc_opt = if_desc.unknown_descs.iter().find_map(|unknown_desc| {
//TODO: should we do any filtering?
HidDescriptor::from_bytes(&unknown_desc.all_bytes).ok()
})?;
Some((if_desc.clone(), endp_desc_opt, hid_desc))
});
Some((if_desc.clone(), endp_desc_opt, hid_desc_opt))
} else {
endp_count += if_desc.endpoints.len();
None
@@ -247,21 +275,29 @@ fn main() -> Result<()> {
//TODO: dynamically create good values, fix xhcid so it does not block on each request
// This sets all reports to a duration of 4ms
reqs::set_idle(&handle, 1, 0, interface_num as u16).context("Failed to set idle")?;
if let Err(err) = reqs::set_idle(&handle, 1, 0, interface_num as u16) {
log::warn!("failed to set idle: {}", err);
}
let report_desc_len = hid_desc.get_report_desc()?.desc_len;
let report_desc_bytes = if let Some(hid_desc) = hid_desc_opt {
let report_desc_len = hid_desc.get_report_desc()?.desc_len;
let mut report_desc_bytes = vec![0u8; report_desc_len as usize];
handle
.get_descriptor(
PortReqRecipient::Interface,
REPORT_DESC_TY,
0,
//TODO: should this be an index into interface_descs?
interface_num as u16,
&mut report_desc_bytes,
)
.context("Failed to retrieve report descriptor")?;
let mut report_desc_bytes = vec![0u8; report_desc_len as usize];
handle
.get_descriptor(
PortReqRecipient::Interface,
REPORT_DESC_TY,
0,
//TODO: should this be an index into interface_descs?
interface_num as u16,
&mut report_desc_bytes,
)
.context("Failed to retrieve report descriptor")?;
report_desc_bytes
} else {
report_desc_override.expect("failed to find report descriptor")
};
let mut handler =
ReportHandler::new(&report_desc_bytes).expect("failed to parse report descriptor");
@@ -270,9 +306,23 @@ fn main() -> Result<()> {
Some((_endp_num, endp_desc)) => endp_desc.max_packet_size as usize,
None => handler.total_byte_length as usize,
};
let mut report_buffer = vec![0u8; report_len];
let report_ty = ReportTy::Input;
let report_id = 0;
let mut report_collections = HashMap::new();
for report in handler.descriptor.input_reports() {
for field in report.fields() {
let key = (report.report_id().map(u8::from), u32::from(field.id()));
if let Some(old) = report_collections.get(&key) {
log::warn!(
"key {:?} has old collections {:?} and new collections {:?}",
key,
old,
field.collections()
);
continue;
}
report_collections.insert(key, field.collections().to_vec());
}
}
let mut display = ProducerHandle::new().context("Failed to open input socket")?;
let mut endpoint_opt = match endp_desc_opt {
@@ -285,107 +335,318 @@ fn main() -> Result<()> {
},
None => None,
};
let (tx, rx) = mpsc::channel();
thread::spawn(move || -> Result<()> {
let mut report_buffer = vec![0u8; report_len];
let report_ty = ReportTy::Input;
let report_id = 0;
loop {
//TODO: get frequency from device
//TODO: use sleeps when accuracy is better: thread::sleep(time::Duration::from_millis(10));
let timer = time::Instant::now();
while timer.elapsed() < time::Duration::from_millis(1) {
thread::yield_now();
}
if let Some(endpoint) = &mut endpoint_opt {
// interrupt transfer
endpoint
.transfer_read(&mut report_buffer)
.context("failed to get report")?;
} else {
// control transfer
reqs::get_report(
&handle,
report_ty,
report_id,
//TODO: should this be an index into interface_descs?
interface_num as u16,
&mut report_buffer,
)
.context("failed to get report")?;
}
tx.send(report_buffer.clone())
.context("failed to send report to main thread")?;
}
});
let mut left_shift = false;
let mut right_shift = false;
let mut last_mouse_pos = (0, 0);
let mut last_buttons = [false, false, false];
let mut gamepad_state = HashMap::new();
loop {
//TODO: get frequency from device
//TODO: use sleeps when accuracy is better: thread::sleep(time::Duration::from_millis(10));
let timer = time::Instant::now();
while timer.elapsed() < time::Duration::from_millis(1) {
thread::yield_now();
}
if let Some(endpoint) = &mut endpoint_opt {
// interrupt transfer
endpoint
.transfer_read(&mut report_buffer)
.context("failed to get report")?;
} else {
// control transfer
reqs::get_report(
&handle,
report_ty,
report_id,
//TODO: should this be an index into interface_descs?
interface_num as u16,
&mut report_buffer,
)
.context("failed to get report")?;
}
let mut mouse_pos = last_mouse_pos;
let mut mouse_dx = 0i32;
let mut mouse_dy = 0i32;
let mut scroll_y = 0i32;
let mut buttons = last_buttons;
for event in handler
.handle(&report_buffer)
.expect("failed to parse report")
{
log::debug!("{}", event);
if event.usage_page == UsagePage::GenericDesktop as u16 {
if event.usage == GenericDesktopUsage::X as u16 {
if event.relative {
mouse_dx += event.value as i32;
match rx.try_recv() {
Ok(report_buffer) => {
for event in handler
.handle(&report_buffer)
.expect("failed to parse report")
{
let mut gamepad = false;
if let Some(collections) =
report_collections.get(&(event.report_id, event.field_id))
{
for collection in collections {
for usage in collection.usages() {
match UsagePage::from_repr(usage.usage_page.into()) {
Some(UsagePage::GenericDesktop) => {
match GenericDesktopUsage::from_repr(usage.usage_id.into())
{
Some(GenericDesktopUsage::GamePad) => {
gamepad = true;
}
_ => {}
}
}
_ => {}
}
}
}
} else {
mouse_pos.0 = event.value as i32;
log::warn!(
"collections missing for report {:?} field {}",
event.report_id,
event.field_id
);
}
} else if event.usage == GenericDesktopUsage::Y as u16 {
if event.relative {
mouse_dy += event.value as i32;
} else {
mouse_pos.1 = event.value as i32;
// Special handling for gamepad
//TODO: make this more generic
if gamepad {
let last = gamepad_state
.get(&(event.usage_page, event.usage))
.map_or(0, |x: &(i64, i64)| x.1);
gamepad_state.insert((event.usage_page, event.usage), (event.value, last));
continue;
}
} else if event.usage == GenericDesktopUsage::Wheel as u16 {
//TODO: what is X scroll?
if event.relative {
scroll_y += event.value as i32;
} else {
log::warn!("absolute mouse wheel not supported");
match UsagePage::from_repr(event.usage_page) {
Some(UsagePage::GenericDesktop) => {
match GenericDesktopUsage::from_repr(event.usage) {
Some(GenericDesktopUsage::X) => {
if event.relative {
mouse_dx += event.value as i32;
} else {
mouse_pos.0 = event.value as i32;
}
}
Some(GenericDesktopUsage::Y) => {
if event.relative {
mouse_dy += event.value as i32;
} else {
mouse_pos.1 = event.value as i32;
}
}
Some(GenericDesktopUsage::Wheel) => {
//TODO: what is X scroll?
if event.relative {
scroll_y += event.value as i32;
} else {
log::warn!("absolute mouse wheel not supported");
}
}
unsupported => {
log::info!(
"unsupported generic desktop usage 0x{:X}:0x{:X} ({:?}) value {}",
event.usage_page,
event.usage,
unsupported,
event.value
);
}
}
}
Some(UsagePage::KeyboardOrKeypad) => {
let (pressed, shift_opt) = if event.value != 0 {
(true, Some(left_shift | right_shift))
} else {
(false, None)
};
if event.usage == 0xE1 {
left_shift = pressed;
} else if event.usage == 0xE5 {
right_shift = pressed;
}
send_key_event(&mut display, event.usage_page, event.usage, pressed);
}
Some(UsagePage::Button) => {
if event.usage > 0 && event.usage as usize <= buttons.len() {
buttons[event.usage as usize - 1] = event.value != 0;
} else {
log::info!(
"unsupported buttons usage 0x{:X}:0x{:X} value {}",
event.usage_page,
event.usage,
event.value
);
}
}
_ => {
if event.usage_page >= 0xFF00 {
// Ignore vendor defined event
} else {
log::info!(
"unsupported usage 0x{:X}:0x{:X} value {}",
event.usage_page,
event.usage,
event.value
);
}
}
}
} else {
log::info!(
"unsupported generic desktop usage 0x{:X}:0x{:X} value {}",
event.usage_page,
event.usage,
event.value
);
}
} else if event.usage_page == UsagePage::KeyboardOrKeypad as u16 {
let (pressed, shift_opt) = if event.value != 0 {
(true, Some(left_shift | right_shift))
} else {
(false, None)
};
if event.usage == 0xE1 {
left_shift = pressed;
} else if event.usage == 0xE5 {
right_shift = pressed;
}
send_key_event(&mut display, event.usage_page, event.usage, pressed);
} else if event.usage_page == UsagePage::Button as u16 {
if event.usage > 0 && event.usage as usize <= buttons.len() {
buttons[event.usage as usize - 1] = event.value != 0;
} else {
log::info!(
"unsupported buttons usage 0x{:X}:0x{:X} value {}",
event.usage_page,
event.usage,
event.value
);
}
} else if event.usage_page >= 0xFF00 {
// Ignore vendor defined event
} else {
log::info!(
"unsupported usage 0x{:X}:0x{:X} value {}",
event.usage_page,
event.usage,
event.value
);
}
Err(mpsc::TryRecvError::Empty) => {
//TODO: get frequency from device
//TODO: use sleeps when accuracy is better: thread::sleep(time::Duration::from_millis(10));
let timer = time::Instant::now();
while timer.elapsed() < time::Duration::from_millis(1) {
thread::yield_now();
}
}
Err(err) => {
anyhow::bail!("failed to recv report buffer from thread: {:?}", err);
}
}
for (&(usage_page, usage), &(value, last)) in gamepad_state.iter() {
let gamepad_axis = |value| {
let deadzone = 8096;
if value < -deadzone {
value + deadzone
} else if value > deadzone {
value - deadzone
} else {
0
}
};
let mut gamepad_key = |scancode, pressed| {
let key_event = OrbKeyEvent {
character: '\0',
scancode,
pressed,
};
match display.write_event(key_event.to_event()) {
Ok(_) => (),
Err(err) => {
log::warn!("failed to send key event to orbital: {}", err);
}
}
};
let mut gamepad_axis_keys = |value, last, k_neg, k_pos| {
let axis = gamepad_axis(value);
let press_neg = axis < 0;
let press_pos = axis > 0;
let last_axis = gamepad_axis(last);
if press_neg != (last_axis < 0) {
gamepad_key(k_neg, press_neg);
}
if press_pos != (last_axis > 0) {
gamepad_key(k_pos, press_pos);
}
};
match UsagePage::from_repr(usage_page) {
Some(UsagePage::GenericDesktop) => match GenericDesktopUsage::from_repr(usage) {
Some(GenericDesktopUsage::X) => {
gamepad_axis_keys(value, last, orbclient::K_A, orbclient::K_D);
}
Some(GenericDesktopUsage::Y) => {
gamepad_axis_keys(value, last, orbclient::K_S, orbclient::K_W);
}
Some(GenericDesktopUsage::Rx) => {
mouse_dx += (gamepad_axis(value) / 4096) as i32;
}
Some(GenericDesktopUsage::Ry) => {
mouse_dy -= (gamepad_axis(value) / 4096) as i32;
}
Some(GenericDesktopUsage::DpadLeft) => {
if value != last {
gamepad_key(orbclient::K_A, value != 0);
}
}
Some(GenericDesktopUsage::DpadRight) => {
if value != last {
gamepad_key(orbclient::K_D, value != 0);
}
}
Some(GenericDesktopUsage::DpadUp) => {
if value != last {
gamepad_key(orbclient::K_W, value != 0);
}
}
Some(GenericDesktopUsage::DpadDown) => {
if value != last {
gamepad_key(orbclient::K_S, value != 0);
}
}
_ => {}
},
Some(UsagePage::Button) => match usage {
// A
1 => {
if value != last {
gamepad_key(orbclient::K_SEMICOLON, value != 0)
}
}
// B
2 => {
if value != last {
gamepad_key(orbclient::K_L, value != 0)
}
}
// X
3 => {
if value != last {
gamepad_key(orbclient::K_O, value != 0)
}
}
// Y
4 => {
if value != last {
gamepad_key(orbclient::K_K, value != 0)
}
}
// LB
5 => {
if value != last {
gamepad_key(orbclient::K_I, value != 0)
}
}
// RB
6 => {
if value != last {
gamepad_key(orbclient::K_P, value != 0)
}
}
// Select
7 => {
if value != last {
gamepad_key(orbclient::K_SPACE, value != 0)
}
}
// Start
8 => {
if value != last {
gamepad_key(orbclient::K_ENTER, value != 0)
}
}
_ => {
//log::warn!("unknown gamepad button {}", usage)
}
},
_ => {}
}
}
for (_, (value, last)) in gamepad_state.iter_mut() {
*last = *value;
}
if mouse_pos != last_mouse_pos {
+90
View File
@@ -0,0 +1,90 @@
#[rustfmt::skip]
pub const OVERRIDES: &'static [(&'static str, &'static [u8])] = &[
// Adapted from https://marc.info/?l=openbsd-tech&m=138227720105805&w=2
(
"xbox-360",
&[
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x05, // Usage (Game Pad)
0xA1, 0x01, // Collection (Application)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x08, // Report Size (8)
0x95, 0x01, // Report Count (1)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x3B, // Usage (Byte Count)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x75, 0x01, // Report Size (1)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x35, 0x00, // Physical Minimum (0)
0x45, 0x01, // Physical Maximum (1)
0x95, 0x04, // Report Count (4)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x90, // Usage (D-pad Up)
0x09, 0x91, // Usage (D-pad Down)
0x09, 0x93, // Usage (D-pad Left)
0x09, 0x92, // Usage (D-pad Right)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0x75, 0x01, // Report Size (1)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x35, 0x00, // Physical Minimum (0)
0x45, 0x01, // Physical Maximum (1)
0x95, 0x07, // Report Count (7)
0x05, 0x09, // Usage Page (Button)
0x09, 0x08, // Usage (0x08)
0x09, 0x07, // Usage (0x07)
0x09, 0x09, // Usage (0x09)
0x09, 0x0A, // Usage (0x0A)
0x09, 0x05, // Usage (0x05)
0x09, 0x06, // Usage (0x06)
0x09, 0x0B, // Usage (0x0B)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x01, // Report Size (1)
0x95, 0x01, // Report Count (1)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x01, // Report Size (1)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x35, 0x00, // Physical Minimum (0)
0x45, 0x01, // Physical Maximum (1)
0x95, 0x04, // Report Count (4)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x04, // Usage Maximum (0x04)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xFF, 0x00, // Logical Maximum (255)
0x35, 0x00, // Physical Minimum (0)
0x46, 0xFF, 0x00, // Physical Maximum (255)
0x95, 0x02, // Report Count (2)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x32, // Usage (Z)
0x09, 0x35, // Usage (Rz)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x10, // Report Size (16)
0x16, 0x00, 0x80, // Logical Minimum (-32768)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x36, 0x00, 0x80, // Physical Minimum (-32768)
0x46, 0xFF, 0x7F, // Physical Maximum (32767)
0x95, 0x04, // Report Count (4)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x09, 0x33, // Usage (Rx)
0x09, 0x34, // Usage (Ry)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x75, 0x30, // Report Size (48)
0x95, 0x01, // Report Count (1)
0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
],
),
];
+10
View File
@@ -16,3 +16,13 @@ name = "USB HID"
class = 3 # HID class
subclass = -1
command = ["usbhidd", "$SCHEME", "$PORT", "$IF_NUM"]
[[drivers]]
name = "USB HID Xbox 360"
class = 255 # Vendor specific class
subclass = 93
protocol = 1
ids = { 0x045e = [
0x028e # Xbox 360 controller
] }
command = ["usbhidd", "$SCHEME", "$PORT", "$IF_NUM", "xbox-360"]
+36 -14
View File
@@ -1251,19 +1251,14 @@ impl<const N: usize> Xhci<N> {
trace!("Spawning driver on port: {}", port);
//TODO: support choosing config?
let config_desc = &ps
.dev_desc
.as_ref()
.ok_or_else(|| {
log::warn!("Missing device descriptor");
Error::new(EBADF)
})?
.config_descs
.first()
.ok_or_else(|| {
log::warn!("Missing config descriptor");
Error::new(EBADF)
})?;
let dev_desc = ps.dev_desc.as_ref().ok_or_else(|| {
log::warn!("Missing device descriptor");
Error::new(EBADF)
})?;
let config_desc = dev_desc.config_descs.first().ok_or_else(|| {
log::warn!("Missing config descriptor");
Error::new(EBADF)
})?;
trace!("Got config and device descriptors on port {}", port);
@@ -1287,11 +1282,36 @@ impl<const N: usize> Xhci<N> {
}
if let Some(driver) = self.drivers_config.drivers.iter().find(|driver| {
if let Some(ref ids) = driver.ids {
let mut device_found = false;
for (vendor, devices) in ids {
let vendor_without_prefix = vendor.trim_start_matches("0x");
let vendor = i64::from_str_radix(vendor_without_prefix, 16).unwrap() as u16;
if vendor != dev_desc.vendor {
continue;
}
for device in devices {
if *device == dev_desc.product {
device_found = true;
break;
}
}
}
if !device_found {
return false;
}
}
driver.class == ifdesc.class
&& driver
.subclass()
.map(|subclass| subclass == ifdesc.sub_class)
.unwrap_or(true)
&& driver
.protocol
.map(|protocol| protocol == ifdesc.protocol)
.unwrap_or(true)
}) {
info!(
"Loading subdriver \"{}\" for port {} iface {} alternate {} class {}.{} proto {}",
@@ -1337,7 +1357,7 @@ impl<const N: usize> Xhci<N> {
ifdesc.alternate_setting,
ifdesc.class,
ifdesc.sub_class,
ifdesc.protocol
ifdesc.protocol,
);
}
}
@@ -1485,6 +1505,8 @@ struct DriverConfig {
name: String,
class: u8,
subclass: i16, // The subclass may be meaningless for some drivers, hence negative values (and values above 255) mean "undefined".
protocol: Option<u8>,
ids: Option<BTreeMap<String, Vec<u16>>>,
command: Vec<String>,
}
impl DriverConfig {