From 5b1ab9f290c92097e77b6bf698d1b1d52259c2f0 Mon Sep 17 00:00:00 2001
From: Naxdy <naxdy@naxdy.org>
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      | 138 ++++++++---
 8 files changed, 734 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<T: RequestHandler>(self, use_report_ids: bool, handler: &mut T) -> !;
+
+    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError>;
+}
+
+/// 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<T: RequestHandler>(self, use_report_ids: bool, handler: &mut T) -> ! {
+        self.run(use_report_ids, handler).await
+    }
+
+    async fn read(&mut self, buf: &mut [u8]) -> Result<usize, ReadError> {
+        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..c33aecd
--- /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) * 257,
+            stick_left_y: (value.stick_y as i16 - 127) * 257,
+            stick_right_x: (value.cstick_x as i16 - 127) * 257,
+            stick_right_y: (value.cstick_y as i16 - 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<usize> {
+        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<embassy_usb::class::hid::ReportId>) -> Option<u32> {
+        let _ = id;
+        None
+    }
+
+    fn set_idle_ms(&mut self, id: Option<embassy_usb::class::hid::ReportId>, 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<T: RequestHandler>(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<usize, ReadError> {
+        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<XInputControl<'d>>,
+    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<ReportId, ()> {
+    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<OutResponse> {
+        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<InResponse<'a>> {
+        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..33ce7ff 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<ControllerMode> 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<Output = ()> + 'd,
     impl Future<Output = ()> + 'd,
@@ -167,12 +194,16 @@ where
     D: Driver<'d> + 'd,
     H: HidReportBuilder<W> + '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;
@@ -291,20 +324,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 +363,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 +385,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;