From 3f031b3518e099f4192c6393aa14775513e55874 Mon Sep 17 00:00:00 2001 From: Naxdy Date: Sun, 27 Oct 2024 21:41:00 +0100 Subject: [PATCH] feat: implement XInput mode --- .changelogs/v1.3.0.md | 10 + Cargo.lock | 2 +- Cargo.toml | 2 +- src/config.rs | 2 + src/hid/mod.rs | 68 ++++++ src/hid/xinput.rs | 544 ++++++++++++++++++++++++++++++++++++++++++ src/input.rs | 2 + src/usb_comms.rs | 139 ++++++++--- 8 files changed, 735 insertions(+), 34 deletions(-) create mode 100644 .changelogs/v1.3.0.md create mode 100644 src/hid/xinput.rs diff --git a/.changelogs/v1.3.0.md b/.changelogs/v1.3.0.md new file mode 100644 index 0000000..c5c1030 --- /dev/null +++ b/.changelogs/v1.3.0.md @@ -0,0 +1,10 @@ +This release introduces XInput mode for the NaxGCC. This mode is mostly useful for playing games on PC, as it offers the best out-of-the-box compatiblity experience with most titles. Similar to Pro-Controller Mode, this is not a permanent configuration that you set, but a mode that is activated by pressing a button while plugging your controller in. + +To enter XInput Mode, press and hold the `X` button while plugging in your controller. While in XInput mode, your NaxGCC will _always_ poll at 1ms intervals, regardless of your chosen input consistency mode setting. Once you go back to GCC or Pro-Controller Mode, your desired input consistency setting will be restored. + +> [!NOTE] +> As of this version, rumble will _not_ work while in XInput mode. + +--- + +To update your firmware, plug in your controller to your computer while keeping the `A+X+Y` buttons held. Then drag & drop the `.uf2` file (found below, under Downloads) onto the storage device that appears. diff --git a/Cargo.lock b/Cargo.lock index 018213b..ed7103a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -866,7 +866,7 @@ checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "naxgcc-fw" -version = "1.2.0" +version = "1.3.0" dependencies = [ "cortex-m", "cortex-m-rt", diff --git a/Cargo.toml b/Cargo.toml index 491a026..6da6e75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "naxgcc-fw" -version = "1.2.0" +version = "1.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/config.rs b/src/config.rs index 1ab9b36..343e429 100644 --- a/src/config.rs +++ b/src/config.rs @@ -571,6 +571,8 @@ pub enum ControllerMode { GcAdapter = 0, /// Pretend to be a Nintendo Switch Pro Controller connected via USB. Procon = 1, + /// Act as an XInput device, and also advertise itself with 1000Hz polling capability. + XInput = 2, } #[derive(Debug, Clone, Format, PackedStruct)] diff --git a/src/hid/mod.rs b/src/hid/mod.rs index 1b356d2..b570b21 100644 --- a/src/hid/mod.rs +++ b/src/hid/mod.rs @@ -1,2 +1,70 @@ +use embassy_usb::{ + class::hid::{HidReader, HidReaderWriter, HidWriter, ReadError, RequestHandler}, + driver::{Driver, EndpointError}, +}; + pub mod gcc; pub mod procon; +pub mod xinput; + +/// Custom trait to unify the API between embassy's HID writer, and our XInput reader/writer (and any +/// custom writers we may create in the future) +pub trait HidReaderWriterSplit<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> { + fn split( + self, + ) -> ( + impl UsbReader<'d, D, READ_N>, + impl UsbWriter<'d, D, WRITE_N>, + ); +} + +/// Custom trait to unify the API between embassy's HID writer, and our XInput reader (and any +/// custom writers we may create in the future) +pub trait UsbReader<'d, D: Driver<'d>, const READ_N: usize> { + async fn run(self, use_report_ids: bool, handler: &mut T) -> !; + + async fn read(&mut self, buf: &mut [u8]) -> Result; +} + +/// Custom trait to unify the API between embassy's HID writer, and our XInput writer (and any +/// custom writers we may create in the future) +pub trait UsbWriter<'d, D: Driver<'d>, const WRITE_N: usize> { + async fn ready(&mut self); + + async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError>; +} + +impl<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> + HidReaderWriterSplit<'d, D, READ_N, WRITE_N> for HidReaderWriter<'d, D, READ_N, WRITE_N> +{ + fn split( + self, + ) -> ( + impl UsbReader<'d, D, READ_N>, + impl UsbWriter<'d, D, WRITE_N>, + ) { + self.split() + } +} + +impl<'d, D: Driver<'d>, const READ_N: usize> UsbReader<'d, D, READ_N> for HidReader<'d, D, READ_N> { + async fn run(self, use_report_ids: bool, handler: &mut T) -> ! { + self.run(use_report_ids, handler).await + } + + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.read(buf).await + } +} + +impl<'d, D: Driver<'d>, const WRITE_N: usize> UsbWriter<'d, D, WRITE_N> + for HidWriter<'d, D, WRITE_N> +{ + async fn ready(&mut self) { + self.ready().await + } + + async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError> { + self.write(report).await + } +} diff --git a/src/hid/xinput.rs b/src/hid/xinput.rs new file mode 100644 index 0000000..ddd8c20 --- /dev/null +++ b/src/hid/xinput.rs @@ -0,0 +1,544 @@ +/// +/// # XInput Protocol Implementation +/// +/// The implementations for `XInputReader` and `XInputWriter` and the logic surrounding them is +/// mostly taken from embassy. +/// +/// Unfortunately, the embassy hid classes don't allow us to specify a custom interface protocol, +/// hence the little bit of code duplication. +/// +use core::{ + mem::MaybeUninit, + sync::atomic::{AtomicUsize, Ordering}, +}; + +use defmt::{info, trace, warn, Format}; +use embassy_usb::{ + class::hid::{Config, ReadError, ReportId, RequestHandler}, + control::{InResponse, OutResponse, Recipient, Request, RequestType}, + driver::{Driver, Endpoint, EndpointError, EndpointIn, EndpointOut}, + types::InterfaceNumber, + Builder, Handler, +}; +use packed_struct::{derive::PackedStruct, PackedStruct}; + +use crate::usb_comms::HidReportBuilder; + +use super::{gcc::GcState, HidReaderWriterSplit, UsbReader, UsbWriter}; + +/// lol +pub const XINPUT_REPORT_DESCRIPTOR: &[u8] = &[]; + +const HID_DESC_DESCTYPE_HID: u8 = 0x21; +const HID_DESC_DESCTYPE_HID_REPORT: u8 = 0x22; + +const HID_REQ_SET_IDLE: u8 = 0x0a; +const HID_REQ_GET_IDLE: u8 = 0x02; +const HID_REQ_GET_REPORT: u8 = 0x01; +const HID_REQ_SET_REPORT: u8 = 0x09; +const HID_REQ_GET_PROTOCOL: u8 = 0x03; +const HID_REQ_SET_PROTOCOL: u8 = 0x0b; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)] +#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")] +pub struct XInputButtons1 { + #[packed_field(bits = "0")] + pub dpad_up: bool, + #[packed_field(bits = "1")] + pub dpad_down: bool, + #[packed_field(bits = "2")] + pub dpad_left: bool, + #[packed_field(bits = "3")] + pub dpad_right: bool, + #[packed_field(bits = "4")] + pub button_menu: bool, + #[packed_field(bits = "5")] + pub button_back: bool, + #[packed_field(bits = "6")] + pub button_stick_l: bool, + #[packed_field(bits = "7")] + pub button_stick_r: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)] +#[packed_struct(bit_numbering = "lsb0", size_bytes = "1")] +pub struct XInputButtons2 { + #[packed_field(bits = "0")] + pub bumper_l: bool, + #[packed_field(bits = "1")] + pub bumper_r: bool, + #[packed_field(bits = "2")] + pub button_guide: bool, + #[packed_field(bits = "3")] + pub blank_1: bool, + #[packed_field(bits = "4")] + pub button_a: bool, + #[packed_field(bits = "5")] + pub button_b: bool, + #[packed_field(bits = "6")] + pub button_x: bool, + #[packed_field(bits = "7")] + pub button_y: bool, +} + +/// +/// HID report that is sent back to the host. +/// +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PackedStruct, Format)] +#[packed_struct(bit_numbering = "msb0", endian = "lsb", size_bytes = "32")] +pub struct XInputReport { + #[packed_field(bits = "0..=7")] + pub report_id: u8, + #[packed_field(bits = "8..=15")] + pub report_size: u8, + #[packed_field(bits = "16..=23")] + pub buttons_1: XInputButtons1, + #[packed_field(bits = "24..=31")] + pub buttons_2: XInputButtons2, + #[packed_field(bits = "32..=39")] + pub analog_trigger_l: u8, + #[packed_field(bits = "40..=47")] + pub analog_trigger_r: u8, + #[packed_field(bits = "48..=63")] + pub stick_left_x: i16, + #[packed_field(bits = "64..=79")] + pub stick_left_y: i16, + #[packed_field(bits = "80..=95")] + pub stick_right_x: i16, + #[packed_field(bits = "96..=111")] + pub stick_right_y: i16, + #[packed_field(bits = "112..=255")] + pub reserved: [u8; 18], +} + +impl From<&GcState> for XInputReport { + fn from(value: &GcState) -> Self { + Self { + report_id: 0, + report_size: 20, + buttons_1: XInputButtons1 { + dpad_up: value.buttons_1.dpad_up, + dpad_down: value.buttons_1.dpad_down, + dpad_right: value.buttons_1.dpad_right, + dpad_left: value.buttons_1.dpad_left, + button_menu: value.buttons_2.button_start, + button_back: false, + button_stick_l: false, + button_stick_r: false, + }, + buttons_2: XInputButtons2 { + blank_1: false, + bumper_l: false, + bumper_r: value.buttons_2.button_z, + button_a: value.buttons_1.button_a, + button_b: value.buttons_1.button_b, + button_x: value.buttons_1.button_x, + button_y: value.buttons_1.button_y, + button_guide: false, + }, + analog_trigger_l: value.trigger_l, + analog_trigger_r: value.trigger_r, + stick_left_x: (value.stick_x as i16 - 127).clamp(-127, 127) * 257, + stick_left_y: (value.stick_y as i16 - 127).clamp(-127, 127) * 257, + stick_right_x: (value.cstick_x as i16 - 127).clamp(-127, 127) * 257, + stick_right_y: (value.cstick_y as i16 - 127).clamp(-127, 127) * 257, + reserved: [0u8; 18], + } + } +} + +/// +/// Takes in a GcState, converts it to an `XInputReport` and returns its packed version. +/// +pub struct XInputReportBuilder; + +impl HidReportBuilder<32> for XInputReportBuilder { + async fn get_hid_report(&mut self, state: &super::gcc::GcState) -> [u8; 32] { + XInputReport::from(state) + .pack() + .expect("Failed to pack XInput State") + } +} + +/// +/// Handles packets sent from the host. +/// +pub struct XInputRequestHandler; + +impl RequestHandler for XInputRequestHandler { + fn get_report( + &mut self, + id: embassy_usb::class::hid::ReportId, + buf: &mut [u8], + ) -> Option { + let _ = (id, buf); + None + } + + fn set_report( + &mut self, + id: embassy_usb::class::hid::ReportId, + data: &[u8], + ) -> embassy_usb::control::OutResponse { + let _ = (id, data); + info!("Set report for {:?}: {:x}", id, data); + embassy_usb::control::OutResponse::Accepted + } + + fn get_idle_ms(&mut self, id: Option) -> Option { + let _ = id; + None + } + + fn set_idle_ms(&mut self, id: Option, duration_ms: u32) { + let _ = (id, duration_ms); + } +} + +/// Taken from embassy. +pub struct XInputWriter<'d, D: Driver<'d>, const N: usize> { + ep_in: D::EndpointIn, +} + +impl<'d, D: Driver<'d>, const N: usize> UsbWriter<'d, D, N> for XInputWriter<'d, D, N> { + /// Waits for the interrupt in endpoint to be enabled. + async fn ready(&mut self) { + self.ep_in.wait_enabled().await; + } + + /// Writes `report` to its interrupt endpoint. + async fn write(&mut self, report: &[u8]) -> Result<(), EndpointError> { + assert!(report.len() <= N); + + let max_packet_size = usize::from(self.ep_in.info().max_packet_size); + let zlp_needed = report.len() < N && (report.len() % max_packet_size == 0); + for chunk in report.chunks(max_packet_size) { + self.ep_in.write(chunk).await?; + } + + if zlp_needed { + self.ep_in.write(&[]).await?; + } + + Ok(()) + } +} + +/// Taken from embassy. +pub struct XInputReader<'d, D: Driver<'d>, const N: usize> { + ep_out: D::EndpointOut, + offset: &'d AtomicUsize, +} + +impl<'d, D: Driver<'d>, const N: usize> UsbReader<'d, D, N> for XInputReader<'d, D, N> { + async fn run(mut self, use_report_ids: bool, handler: &mut T) -> ! { + let offset = self.offset.load(Ordering::Acquire); + assert!(offset == 0); + let mut buf = [0; N]; + loop { + match self.read(&mut buf).await { + Ok(len) => { + let id = if use_report_ids { buf[0] } else { 0 }; + handler.set_report(ReportId::Out(id), &buf[..len]); + } + Err(ReadError::BufferOverflow) => warn!( + "Host ent output report larger than the configured maximum output report length ({})", + N + ), + Err(ReadError::Disabled) => self.ep_out.wait_enabled().await, + Err(ReadError::Sync(_)) => unreachable!(), + } + } + } + + async fn read(&mut self, buf: &mut [u8]) -> Result { + assert!(N != 0); + assert!(buf.len() >= N); + + // Read packets from the endpoint + let max_packet_size = usize::from(self.ep_out.info().max_packet_size); + let starting_offset = self.offset.load(Ordering::Acquire); + let mut total = starting_offset; + loop { + for chunk in buf[starting_offset..N].chunks_mut(max_packet_size) { + match self.ep_out.read(chunk).await { + Ok(size) => { + total += size; + if size < max_packet_size || total == N { + self.offset.store(0, Ordering::Release); + break; + } + self.offset.store(total, Ordering::Release); + } + Err(err) => { + self.offset.store(0, Ordering::Release); + return Err(err.into()); + } + } + } + + // Some hosts may send ZLPs even when not required by the HID spec, so we'll loop as long as total == 0. + if total > 0 { + break; + } + } + + if starting_offset > 0 { + Err(ReadError::Sync(starting_offset..total)) + } else { + Ok(total) + } + } +} + +/// Taken from embassy, with a few modifications to the descriptor. +pub struct XInputReaderWriter<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> { + reader: XInputReader<'d, D, READ_N>, + writer: XInputWriter<'d, D, WRITE_N>, +} + +impl<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> + XInputReaderWriter<'d, D, READ_N, WRITE_N> +{ + pub fn new( + builder: &mut Builder<'d, D>, + state: &'d mut XInputState<'d>, + config: Config<'d>, + ) -> Self { + let mut func = builder.function(0xff, 0x5d, 0x01); + let mut iface = func.interface(); + let if_num = iface.interface_number(); + let mut alt = iface.alt_setting(0xff, 0x5d, 0x01, None); + + #[rustfmt::skip] + alt.descriptor(0x21, &[ + 0x10, 0x01, // bcdHID 1.10 + 0x01, // bCountryCode + 0x24, // bNumDescriptors + 0x81, // bDescriptorType[0] (Unknown 0x81) + 0x14, 0x03, // wDescriptorLength[0] 788 + 0x00, // bDescriptorType[1] (Unknown 0x00) + 0x03, 0x13, // wDescriptorLength[1] 4867 + 0x02, // bDescriptorType[2] (Unknown 0x02) + 0x00, 0x03, // wDescriptorLength[2] 768 + 0x00, // bDescriptorType[3] (Unknown 0x00) + ]); + + let ep_in = alt.endpoint_interrupt_in(config.max_packet_size_in, config.poll_ms); + let ep_out = alt.endpoint_interrupt_out(config.max_packet_size_out, config.poll_ms); + + drop(func); + + let control = state.control.write(XInputControl::new( + if_num, + config.report_descriptor, + config.request_handler, + &state.out_report_offset, + )); + builder.handler(control); + + Self { + reader: XInputReader { + ep_out, + offset: &state.out_report_offset, + }, + writer: XInputWriter { ep_in }, + } + } +} + +impl<'d, D: Driver<'d>, const READ_N: usize, const WRITE_N: usize> + HidReaderWriterSplit<'d, D, READ_N, WRITE_N> for XInputReaderWriter<'d, D, READ_N, WRITE_N> +{ + fn split( + self, + ) -> ( + impl UsbReader<'d, D, READ_N>, + impl UsbWriter<'d, D, WRITE_N>, + ) { + (self.reader, self.writer) + } +} + +pub struct XInputState<'d> { + control: MaybeUninit>, + out_report_offset: AtomicUsize, +} + +impl<'d> Default for XInputState<'d> { + fn default() -> Self { + Self::new() + } +} + +impl<'d> XInputState<'d> { + pub const fn new() -> Self { + XInputState { + control: MaybeUninit::uninit(), + out_report_offset: AtomicUsize::new(0), + } + } +} + +/// Taken from embassy. +struct XInputControl<'d> { + if_num: InterfaceNumber, + report_descriptor: &'d [u8], + request_handler: Option<&'d mut dyn RequestHandler>, + out_report_offset: &'d AtomicUsize, + hid_descriptor: [u8; 16], +} + +impl<'d> XInputControl<'d> { + fn new( + if_num: InterfaceNumber, + report_descriptor: &'d [u8], + request_handler: Option<&'d mut dyn RequestHandler>, + out_report_offset: &'d AtomicUsize, + ) -> Self { + XInputControl { + if_num, + report_descriptor, + request_handler, + out_report_offset, + #[rustfmt::skip] + hid_descriptor: [ + 0x10, // bLength + 0x21, // bDescriptorType (HID) + 0x10, 0x01, // bcdHID 1.10 + 0x01, // bCountryCode + 0x24, // bNumDescriptors + 0x81, // bDescriptorType[0] (Unknown 0x81) + 0x14, 0x03, // wDescriptorLength[0] 788 + 0x00, // bDescriptorType[1] (Unknown 0x00) + 0x03, 0x13, // wDescriptorLength[1] 4867 + 0x02, // bDescriptorType[2] (Unknown 0x02) + 0x00, 0x03, // wDescriptorLength[2] 768 + 0x00, // bDescriptorType[3] (Unknown 0x00) + ], + } + } +} + +/// Helper function, since the function in `ReportId` is private. +const fn try_u16_to_report_id(value: u16) -> Result { + match value >> 8 { + 1 => Ok(ReportId::In(value as u8)), + 2 => Ok(ReportId::Out(value as u8)), + 3 => Ok(ReportId::Feature(value as u8)), + _ => Err(()), + } +} + +impl<'d> Handler for XInputControl<'d> { + fn reset(&mut self) { + self.out_report_offset.store(0, Ordering::Release); + } + + fn control_out(&mut self, req: Request, data: &[u8]) -> Option { + if (req.request_type, req.recipient, req.index) + != ( + RequestType::Class, + Recipient::Interface, + self.if_num.0 as u16, + ) + { + return None; + } + + trace!("HID control_out {:?} {=[u8]:x}", req, data); + + match req.request { + HID_REQ_SET_IDLE => { + if let Some(handler) = self.request_handler.as_mut() { + let id = req.value as u8; + let id = (id != 0).then_some(ReportId::In(id)); + let dur = u32::from(req.value >> 8); + let dur = if dur == 0 { u32::MAX } else { 4 * dur }; + handler.set_idle_ms(id, dur); + } + Some(OutResponse::Accepted) + } + HID_REQ_SET_REPORT => { + match ( + try_u16_to_report_id(req.value), + self.request_handler.as_mut(), + ) { + (Ok(id), Some(handler)) => Some(handler.set_report(id, data)), + _ => Some(OutResponse::Rejected), + } + } + HID_REQ_SET_PROTOCOL => { + if req.value == 1 { + Some(OutResponse::Accepted) + } else { + warn!("HID Boot Protocol is unsupported."); + Some(OutResponse::Rejected) // UNSUPPORTED: Boot Protocol + } + } + _ => Some(OutResponse::Rejected), + } + } + + fn control_in<'a>(&'a mut self, req: Request, buf: &'a mut [u8]) -> Option> { + if req.index != self.if_num.0 as u16 { + return None; + } + + match (req.request_type, req.recipient) { + (RequestType::Standard, Recipient::Interface) => match req.request { + Request::GET_DESCRIPTOR => match (req.value >> 8) as u8 { + HID_DESC_DESCTYPE_HID_REPORT => { + Some(InResponse::Accepted(self.report_descriptor)) + } + HID_DESC_DESCTYPE_HID => Some(InResponse::Accepted(&self.hid_descriptor)), + _ => Some(InResponse::Rejected), + }, + + _ => Some(InResponse::Rejected), + }, + (RequestType::Class, Recipient::Interface) => { + trace!("HID control_in {:?}", req); + match req.request { + HID_REQ_GET_REPORT => { + let size = match try_u16_to_report_id(req.value) { + Ok(id) => self + .request_handler + .as_mut() + .and_then(|x| x.get_report(id, buf)), + Err(_) => None, + }; + + if let Some(size) = size { + Some(InResponse::Accepted(&buf[0..size])) + } else { + Some(InResponse::Rejected) + } + } + HID_REQ_GET_IDLE => { + if let Some(handler) = self.request_handler.as_mut() { + let id = req.value as u8; + let id = (id != 0).then_some(ReportId::In(id)); + if let Some(dur) = handler.get_idle_ms(id) { + let dur = u8::try_from(dur / 4).unwrap_or(0); + buf[0] = dur; + Some(InResponse::Accepted(&buf[0..1])) + } else { + Some(InResponse::Rejected) + } + } else { + Some(InResponse::Rejected) + } + } + HID_REQ_GET_PROTOCOL => { + // UNSUPPORTED: Boot Protocol + buf[0] = 1; + Some(InResponse::Accepted(&buf[0..1])) + } + _ => Some(InResponse::Rejected), + } + } + _ => None, + } + } +} diff --git a/src/input.rs b/src/input.rs index c0d0d0b..3738ae0 100644 --- a/src/input.rs +++ b/src/input.rs @@ -428,6 +428,8 @@ pub async fn update_button_state_task( let mut m = MUTEX_CONTROLLER_MODE.lock().await; *m = if btn_start.is_low() { Some(ControllerMode::Procon) + } else if btn_x.is_low() { + Some(ControllerMode::XInput) } else { Some(ControllerMode::GcAdapter) }; diff --git a/src/usb_comms.rs b/src/usb_comms.rs index bf94edb..e89692d 100644 --- a/src/usb_comms.rs +++ b/src/usb_comms.rs @@ -15,7 +15,7 @@ use embassy_rp::{ use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, signal::Signal}; use embassy_time::{Duration, Instant, Timer}; use embassy_usb::{ - class::hid::{HidReader, HidReaderWriter, HidWriter, RequestHandler, State}, + class::hid::{HidReaderWriter, RequestHandler, State}, driver::Driver, msos::{self, windows_version}, Builder, Handler, UsbDevice, @@ -27,6 +27,11 @@ use crate::{ hid::{ gcc::{GcReportBuilder, GcState, GccRequestHandler, GCC_REPORT_DESCRIPTOR}, procon::{ProconReportBuilder, ProconRequestHandler, PROCON_REPORT_DESCRIPTOR}, + xinput::{ + XInputReaderWriter, XInputReportBuilder, XInputRequestHandler, XInputState, + XINPUT_REPORT_DESCRIPTOR, + }, + HidReaderWriterSplit, UsbReader, UsbWriter, }, input::CHANNEL_GCC_STATE, }; @@ -73,6 +78,11 @@ impl From for UsbConfig { pid: 0x2009, report_descriptor: PROCON_REPORT_DESCRIPTOR, }, + ControllerMode::XInput => Self { + vid: 0x045e, + pid: 0x028e, + report_descriptor: XINPUT_REPORT_DESCRIPTOR, + }, } } } @@ -122,27 +132,41 @@ impl Handler for MyDeviceHandler { } } -fn mk_hid_reader_writer<'d, D: Driver<'d>, const R: usize, const W: usize>( +fn mk_hid_reader_writer<'d, S, D, T, F, const R: usize, const W: usize>( input_consistency_mode: InputConsistencyMode, + controller_mode: ControllerMode, report_descriptor: &'d [u8], mut builder: Builder<'d, D>, - state: &'d mut State<'d>, -) -> (UsbDevice<'d, D>, HidReader<'d, D, R>, HidWriter<'d, D, W>) { + state: &'d mut S, + mut init_func: F, +) -> ( + UsbDevice<'d, D>, + impl UsbReader<'d, D, R>, + impl UsbWriter<'d, D, W>, +) +where + D: Driver<'d>, + T: HidReaderWriterSplit<'d, D, R, W> + 'd, + F: FnMut(&mut Builder<'d, D>, &'d mut S, embassy_usb::class::hid::Config<'d>) -> T + 'd, +{ let hid_config = embassy_usb::class::hid::Config { report_descriptor, request_handler: None, - poll_ms: match input_consistency_mode { - InputConsistencyMode::Original => 8, - InputConsistencyMode::ConsistencyHack - | InputConsistencyMode::SuperHack - | InputConsistencyMode::PC => 1, + poll_ms: if let ControllerMode::XInput = controller_mode { + 1 + } else { + match input_consistency_mode { + InputConsistencyMode::Original => 8, + InputConsistencyMode::ConsistencyHack + | InputConsistencyMode::SuperHack + | InputConsistencyMode::PC => 1, + } }, max_packet_size_in: W as u16, max_packet_size_out: R as u16, }; - let hid: HidReaderWriter<'d, D, R, W> = - HidReaderWriter::<'_, D, R, W>::new(&mut builder, state, hid_config); + let hid = init_func(&mut builder, state, hid_config); let usb = builder.build(); @@ -151,13 +175,16 @@ fn mk_hid_reader_writer<'d, D: Driver<'d>, const R: usize, const W: usize>( (usb, reader, writer) } -fn mk_usb_transfer_futures<'d, D, H, Rq, const R: usize, const W: usize>( +#[allow(clippy::too_many_arguments)] +fn mk_usb_transfer_futures<'d, S, D, H, F, Rq, T, const R: usize, const W: usize>( input_consistency_mode: InputConsistencyMode, + controller_mode: ControllerMode, usb_config: &UsbConfig, request_handler: &'d mut Rq, builder: Builder<'d, D>, - state: &'d mut State<'d>, mut hid_report_builder: H, + state: &'d mut S, + init_func: F, ) -> ( impl Future + 'd, impl Future + 'd, @@ -167,12 +194,16 @@ where D: Driver<'d> + 'd, H: HidReportBuilder + 'd, Rq: RequestHandler, + T: HidReaderWriterSplit<'d, D, R, W> + 'd, + F: FnMut(&mut Builder<'d, D>, &'d mut S, embassy_usb::class::hid::Config<'d>) -> T + 'd, { - let (mut usb, reader, mut writer) = mk_hid_reader_writer::<_, R, W>( + let (mut usb, reader, mut writer) = mk_hid_reader_writer( input_consistency_mode, + controller_mode, usb_config.report_descriptor, builder, state, + init_func, ); let usb_fut = async move { @@ -200,12 +231,14 @@ where // From the console's perspective, we are basically a laggy adapter, taking // a minimum of 333 extra us to send a report every time it's polled, but it // works to our advantage. - match input_consistency_mode { - InputConsistencyMode::SuperHack | InputConsistencyMode::ConsistencyHack => { - // "Ticker at home", so we can use this for both consistency and SuperHack mode - Timer::at(rate_limit_end_time).await; + if controller_mode != ControllerMode::XInput { + match input_consistency_mode { + InputConsistencyMode::SuperHack | InputConsistencyMode::ConsistencyHack => { + // "Ticker at home", so we can use this for both consistency and SuperHack mode + Timer::at(rate_limit_end_time).await; + } + InputConsistencyMode::Original | InputConsistencyMode::PC => {} } - InputConsistencyMode::Original | InputConsistencyMode::PC => {} } writer.ready().await; @@ -224,6 +257,7 @@ where debug!("Report written in {}us", micros); if input_consistency_mode != InputConsistencyMode::Original && input_consistency_mode != InputConsistencyMode::PC + && controller_mode != ControllerMode::XInput { while rate_limit_end_time < currtime { rate_limit_end_time += Duration::from_micros(8333); @@ -291,20 +325,37 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: EmbassyDriver<'stati trace!("Start of config"); let mut usb_config = embassy_usb::Config::new(config.vid, config.pid); usb_config.manufacturer = Some("Naxdy"); - usb_config.product = Some(match input_consistency_mode { - InputConsistencyMode::Original => "NaxGCC (OG Mode)", - InputConsistencyMode::ConsistencyHack => "NaxGCC (Consistency Mode)", - InputConsistencyMode::SuperHack => "NaxGCC (SuperHack Mode)", - InputConsistencyMode::PC => "NaxGCC (PC Mode)", + usb_config.product = Some(if controller_mode == ControllerMode::XInput { + "NaxGCC (XInput Mode)" + } else { + match input_consistency_mode { + InputConsistencyMode::Original => "NaxGCC (OG Mode)", + InputConsistencyMode::ConsistencyHack => "NaxGCC (Consistency Mode)", + InputConsistencyMode::SuperHack => "NaxGCC (SuperHack Mode)", + InputConsistencyMode::PC => "NaxGCC (PC Mode)", + } }); usb_config.serial_number = Some(serial); usb_config.max_power = 200; usb_config.max_packet_size_0 = 64; - usb_config.device_class = 0; + usb_config.device_class = if controller_mode == ControllerMode::XInput { + 0xff + } else { + 0 + }; usb_config.device_protocol = 0; usb_config.self_powered = false; - usb_config.device_sub_class = 0; + usb_config.device_sub_class = if controller_mode == ControllerMode::XInput { + 0xff + } else { + 0 + }; usb_config.supports_remote_wakeup = true; + usb_config.device_release = if controller_mode == ControllerMode::XInput { + 0x0572 + } else { + 0x0010 + }; let mut config_descriptor = [0; 256]; let mut bos_descriptor = [0; 256]; @@ -313,8 +364,6 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: EmbassyDriver<'stati let mut device_handler = MyDeviceHandler::new(); - let mut state = State::new(); - let mut builder = Builder::new( driver, usb_config, @@ -337,29 +386,55 @@ pub async fn usb_transfer_task(raw_serial: [u8; 8], driver: EmbassyDriver<'stati match controller_mode { ControllerMode::GcAdapter => { + let mut state = State::new(); + let mut request_handler = GccRequestHandler; - let (usb_fut_wrapped, in_fut, out_fut) = mk_usb_transfer_futures::<_, _, _, 5, 37>( + let (usb_fut_wrapped, in_fut, out_fut) = mk_usb_transfer_futures( input_consistency_mode, + controller_mode, &config, &mut request_handler, builder, - &mut state, GcReportBuilder::default(), + &mut state, + HidReaderWriter::<_, 5, 37>::new, ); join(usb_fut_wrapped, join(in_fut, out_fut)).await; } ControllerMode::Procon => { + let mut state = State::new(); + let mut request_handler = ProconRequestHandler; - let (usb_fut_wrapped, in_fut, out_fut) = mk_usb_transfer_futures::<_, _, _, 64, 64>( + let (usb_fut_wrapped, in_fut, out_fut) = mk_usb_transfer_futures( input_consistency_mode, + controller_mode, &config, &mut request_handler, builder, - &mut state, ProconReportBuilder::default(), + &mut state, + HidReaderWriter::<_, 64, 64>::new, + ); + + join(usb_fut_wrapped, join(in_fut, out_fut)).await; + } + ControllerMode::XInput => { + let mut state = XInputState::new(); + + let mut request_handler = XInputRequestHandler; + + let (usb_fut_wrapped, in_fut, out_fut) = mk_usb_transfer_futures( + input_consistency_mode, + controller_mode, + &config, + &mut request_handler, + builder, + XInputReportBuilder, + &mut state, + XInputReaderWriter::<_, 32, 32>::new, ); join(usb_fut_wrapped, join(in_fut, out_fut)).await; -- 2.47.0