//! The GameInput row of the matrix, and the only API that can drive TRIGGER rumble. //! //! WHY THIS IS HAND-WRITTEN COM. GameInput has no binding in the `windows` crate, so the vtables //! below are declared by hand. Every slot index is taken from the SDK header //! `Windows Kits\10\Include\10.0.26100.0\um\GameInput.h`, not from guesswork — a COM vtable is //! positional, so a wrong slot calls a different method with the wrong signature and corrupts the //! stack. Only the slots up to the ones actually called are declared; anything past them is simply //! absent from the struct, which is sound because a vtable is only ever read through the offsets we //! name and we never call beyond the last declared entry. //! //! ⭐ WHY IT MATTERS BEYOND ENUMERATION. `XINPUT_VIBRATION` has exactly two members, so classic //! XInput can never exercise an Xbox pad's two IMPULSE-TRIGGER motors. `GameInputRumbleParams` has //! four — `lowFrequency`, `highFrequency`, `leftTrigger`, `rightTrigger` — which makes this the one //! path that can settle the open question in `design/trigger-rumble-plane.md` §2.1: the `enable`- //! mask bit assignment for the two trigger actuators in the pad's HID output report `0x03` is //! CONJECTURE (bits 2/3 = the handles are measured; bits 0/1 = the triggers are inferred from field //! order and nothing else). //! //! The experiment `--gi-rumble` exists for: drive four DISTINCT magnitudes, then read what the pad //! actually decoded. Four distinct values make the mapping self-identifying — a channel that comes //! back zero had its enable bit guessed wrong, and a channel that comes back holding another's //! value is a swap. //! //! The runtime is loaded by name rather than linked, so this builds with no import library and //! degrades to a clean "GameInput not present" on a box without it. #![allow(non_snake_case)] use std::ffi::c_void; use windows::Win32::Foundation::{FreeLibrary, HMODULE}; use windows::Win32::System::LibraryLoader::{GetProcAddress, LoadLibraryW}; use windows::core::{HRESULT, PCSTR, PCWSTR}; /// `GameInputKind` values we use (GameInput.h). const GAME_INPUT_KIND_GAMEPAD: u32 = 0x0004_0000; const GAME_INPUT_KIND_CONTROLLER: u32 = 0x0000_000E; /// The four rumble channels, 0.0..=1.0 each. Layout verbatim from GameInput.h. #[repr(C)] #[derive(Clone, Copy, Default, Debug)] pub struct GameInputRumbleParams { pub lowFrequency: f32, pub highFrequency: f32, pub leftTrigger: f32, pub rightTrigger: f32, } /// `IGameInput`, declared only as far as `GetCurrentReading` (slot 4). #[repr(C)] struct IGameInputVtbl { QueryInterface: unsafe extern "system" fn(*mut c_void, *const u8, *mut *mut c_void) -> HRESULT, AddRef: unsafe extern "system" fn(*mut c_void) -> u32, Release: unsafe extern "system" fn(*mut c_void) -> u32, GetCurrentTimestamp: unsafe extern "system" fn(*mut c_void) -> u64, GetCurrentReading: unsafe extern "system" fn(*mut c_void, u32, *mut c_void, *mut *mut c_void) -> HRESULT, } /// `IGameInputReading`, declared only as far as `GetDevice` (slot 6). #[repr(C)] struct IGameInputReadingVtbl { QueryInterface: unsafe extern "system" fn(*mut c_void, *const u8, *mut *mut c_void) -> HRESULT, AddRef: unsafe extern "system" fn(*mut c_void) -> u32, Release: unsafe extern "system" fn(*mut c_void) -> u32, GetInputKind: unsafe extern "system" fn(*mut c_void) -> u32, GetSequenceNumber: unsafe extern "system" fn(*mut c_void, u32) -> u64, GetTimestamp: unsafe extern "system" fn(*mut c_void) -> u64, GetDevice: unsafe extern "system" fn(*mut c_void, *mut *mut c_void), } /// `IGameInputDevice`, declared only as far as `SetRumbleState` (slot 10). #[repr(C)] struct IGameInputDeviceVtbl { QueryInterface: unsafe extern "system" fn(*mut c_void, *const u8, *mut *mut c_void) -> HRESULT, AddRef: unsafe extern "system" fn(*mut c_void) -> u32, Release: unsafe extern "system" fn(*mut c_void) -> u32, GetDeviceInfo: unsafe extern "system" fn(*mut c_void) -> *const c_void, GetDeviceStatus: unsafe extern "system" fn(*mut c_void) -> u32, GetBatteryState: unsafe extern "system" fn(*mut c_void, *mut c_void), CreateForceFeedbackEffect: unsafe extern "system" fn(*mut c_void, u32, *const c_void, *mut *mut c_void) -> HRESULT, IsForceFeedbackMotorPoweredOn: unsafe extern "system" fn(*mut c_void, u32) -> i32, SetForceFeedbackMotorGain: unsafe extern "system" fn(*mut c_void, u32, f32), SetHapticMotorState: unsafe extern "system" fn(*mut c_void, u32, *const c_void), SetRumbleState: unsafe extern "system" fn(*mut c_void, *const GameInputRumbleParams), } /// A loaded GameInput runtime plus the root object. Dropping it releases both. pub struct GameInput { module: HMODULE, root: *mut c_void, } impl Drop for GameInput { fn drop(&mut self) { // SAFETY: `root` came from GameInputCreate and is released exactly once; `module` came from // LoadLibraryW. Freeing the module after the object is the required order. unsafe { if !self.root.is_null() { let vtbl = *(self.root as *mut *const IGameInputVtbl); ((*vtbl).Release)(self.root); } let _ = FreeLibrary(self.module); } } } impl GameInput { /// Load `gameinput.dll` and create the root object. `Err` carries a human reason — a box /// without the runtime is a legitimate outcome, not a crash. pub fn create() -> Result { let name: Vec = "gameinput.dll\0".encode_utf16().collect(); // SAFETY: `name` is a NUL-terminated wide string that outlives the call. let module = unsafe { LoadLibraryW(PCWSTR(name.as_ptr())) } .map_err(|e| format!("gameinput.dll not loadable: {e}"))?; // SAFETY: `module` is live; the name is a NUL-terminated byte string. let proc = unsafe { GetProcAddress(module, PCSTR(c"GameInputCreate".as_ptr() as *const u8)) } .ok_or_else(|| "gameinput.dll has no GameInputCreate export".to_string())?; // SAFETY: the export's documented signature is // `HRESULT GameInputCreate(IGameInput**)` — GameInput.h, `STDAPI GameInputCreate`. let create: unsafe extern "system" fn(*mut *mut c_void) -> HRESULT = unsafe { std::mem::transmute(proc) }; let mut root: *mut c_void = std::ptr::null_mut(); // SAFETY: `root` is a valid out-param slot. let hr = unsafe { create(&mut root) }; if hr.is_err() || root.is_null() { // SAFETY: nothing was created; drop the module by hand since we have no object yet. unsafe { let _ = FreeLibrary(module); } return Err(format!("GameInputCreate failed: {hr:?}")); } Ok(Self { module, root }) } /// The current reading for `kind`, if any device is producing one. fn reading(&self, kind: u32) -> Option<*mut c_void> { let mut reading: *mut c_void = std::ptr::null_mut(); // SAFETY: `self.root` is a live IGameInput; slot 4 is GetCurrentReading with this exact // signature (GameInput.h). A null `device` means "any device", which is what we want. let hr = unsafe { let vtbl = *(self.root as *mut *const IGameInputVtbl); ((*vtbl).GetCurrentReading)(self.root, kind, std::ptr::null_mut(), &mut reading) }; (hr.is_ok() && !reading.is_null()).then_some(reading) } /// Poll for a reading, because GameInput's device enumeration is ASYNCHRONOUS. /// /// A freshly created `IGameInput` has not finished enumerating yet, so the first /// `GetCurrentReading` reliably returns nothing even with a pad actively reporting — measured /// on `.173` 2026-08-09, where a sweeping devtest pad and a live DualSense both read "no /// reading" on the first call. This is the GameInput analogue of `wake_wgi`: the API looks like /// a query and is really a cache someone else fills. /// /// ⚠️ Focus is NOT the cause and was ruled out from the header: `GameInputDefaultFocusPolicy` /// is 0 and every `GameInputFocusPolicy` flag is a RESTRICTION /// (`GameInputDisableBackgroundInput`, `GameInputExclusiveForegroundInput`, …), so the default /// already admits background input. Do not "fix" this by calling `SetFocusPolicy`. fn reading_wait(&self, kind: u32, timeout: std::time::Duration) -> Option<*mut c_void> { let deadline = std::time::Instant::now() + timeout; loop { if let Some(r) = self.reading(kind) { return Some(r); } if std::time::Instant::now() >= deadline { return None; } std::thread::sleep(std::time::Duration::from_millis(100)); } } /// `(vendorId, productId)` for a device, read straight off `GameInputDeviceInfo`. /// /// The struct begins `uint32_t infoSize; uint16_t vendorId; uint16_t productId; …` /// (GameInput.h), so the two ids sit at byte offsets 4 and 6. Only those two are read — the /// rest of the struct carries variable-size members whose layout we would have to mirror /// exactly, and nothing here needs them. fn device_ids(dev: *mut c_void) -> (u16, u16) { // SAFETY: `dev` is a live IGameInputDevice; slot 3 is GetDeviceInfo, which returns a // pointer to a struct owned by the runtime and valid for the device's lifetime. unsafe { let vtbl = *(dev as *mut *const IGameInputDeviceVtbl); let info = ((*vtbl).GetDeviceInfo)(dev) as *const u8; if info.is_null() { return (0, 0); } ( u16::from_le_bytes([*info.add(4), *info.add(5)]), u16::from_le_bytes([*info.add(6), *info.add(7)]), ) } } /// Take the device off a reading (AddRef'd), releasing the reading. fn device_of(r: *mut c_void) -> *mut c_void { let mut dev: *mut c_void = std::ptr::null_mut(); // SAFETY: `r` is a live IGameInputReading; slot 6 is GetDevice (returns void, hands back // an AddRef'd device), slot 2 is Release. unsafe { let vtbl = *(r as *mut *const IGameInputReadingVtbl); ((*vtbl).GetDevice)(r, &mut dev); ((*vtbl).Release)(r); } dev } /// Hunt for a device with `pid`, polling because several devices take turns reporting and /// `GetCurrentReading(kind, null, …)` hands back whichever one spoke most recently. A box with /// a chatty pad on it (a DualSense streams continuously) will otherwise never yield ours. fn find_device(&self, pid: u16, timeout: std::time::Duration) -> Option<*mut c_void> { let deadline = std::time::Instant::now() + timeout; let mut seen: Vec<(u16, u16)> = Vec::new(); loop { for kind in [GAME_INPUT_KIND_GAMEPAD, GAME_INPUT_KIND_CONTROLLER] { if let Some(r) = self.reading(kind) { let dev = Self::device_of(r); if !dev.is_null() { let ids = Self::device_ids(dev); if !seen.contains(&ids) { seen.push(ids); println!(" saw device {:04X}:{:04X}", ids.0, ids.1); } if ids.1 == pid { return Some(dev); } // SAFETY: not our target; drop our reference. unsafe { let v = *(dev as *mut *const IGameInputDeviceVtbl); ((*v).Release)(dev); } } } } if std::time::Instant::now() >= deadline { return None; } std::thread::sleep(std::time::Duration::from_millis(60)); } } /// Does GameInput see a gamepad at all? This is the matrix row. pub fn report(&self) { for (kind, label) in [ (GAME_INPUT_KIND_GAMEPAD, "Gamepad"), (GAME_INPUT_KIND_CONTROLLER, "Controller"), ] { match self.reading_wait(kind, std::time::Duration::from_secs(3)) { Some(r) => { println!(" {label:<11}: reading available (a device is producing input)"); // SAFETY: `r` is a live IGameInputReading we own a reference to. unsafe { let vtbl = *(r as *mut *const IGameInputReadingVtbl); ((*vtbl).Release)(r); } } None => println!(" {label:<11}: no reading"), } } } /// Drive `SetRumbleState` on whatever device is currently reporting. /// /// Returns whether a device was found and driven. The values are deliberately the caller's to /// choose: the whole point of the probe is sending four DISTINCT magnitudes so the pad's decoded /// output identifies the channel mapping by itself. pub fn rumble( &self, params: GameInputRumbleParams, hold: std::time::Duration, target_pid: Option, ) -> bool { // A gamepad reading is the right one to hang this off: it is the kind an Xbox pad produces, // and the device it names is the one a game would rumble. let wait = std::time::Duration::from_secs(6); let dev = match target_pid { Some(pid) => { println!(" hunting for PID {pid:04X} …"); match self.find_device(pid, wait) { Some(d) => d, None => { println!(" never saw PID {pid:04X} — nothing to rumble"); return false; } } } None => { let Some(r) = self .reading_wait(GAME_INPUT_KIND_GAMEPAD, wait) .or_else(|| self.reading_wait(GAME_INPUT_KIND_CONTROLLER, wait)) else { println!(" no GameInput reading — nothing to rumble"); return false; }; Self::device_of(r) } }; if dev.is_null() { println!(" reading had no device"); return false; } let ids = Self::device_ids(dev); println!(" driving {:04X}:{:04X}", ids.0, ids.1); println!( " SetRumbleState(low={:.2} high={:.2} lt={:.2} rt={:.2}) for {:?}", params.lowFrequency, params.highFrequency, params.leftTrigger, params.rightTrigger, hold ); // SAFETY: `dev` is a live IGameInputDevice; slot 10 is SetRumbleState, which returns void // and takes a const pointer to the four-float struct above. unsafe { let vtbl = *(dev as *mut *const IGameInputDeviceVtbl); ((*vtbl).SetRumbleState)(dev, ¶ms); } std::thread::sleep(hold); let off = GameInputRumbleParams::default(); // SAFETY: as above; stopping is the same call with zeroes. unsafe { let vtbl = *(dev as *mut *const IGameInputDeviceVtbl); ((*vtbl).SetRumbleState)(dev, &off); ((*vtbl).Release)(dev); } println!(" cleared."); true } }