Files
punktfunk/tools/win-input-matrix/src/main.rs
T
enricobuehler 94c2f62490 test(tools): a GameInput probe — and it cannot see our promoted Xbox pad
`win-input-matrix` covered four of the five rows and said so; GameInput was the gap, because it has
no binding in the `windows` crate and needs hand-written COM. This adds it: `--gameinput` reports
whether GameInput has a reading, and `--gi-rumble l,h,lt,rt [--gi-pid PID]` drives
`SetRumbleState`. Every vtable slot is taken from the SDK header, not guessed — a COM vtable is
positional, so a wrong slot calls a different method with the wrong signature.

WHY RUMBLE AND NOT JUST ENUMERATION. `XINPUT_VIBRATION` has 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 GameInput the only API
that can settle `design/trigger-rumble-plane.md` §2.1's open question — the `enable`-mask bit
assignment for the two trigger actuators, where bits 2/3 (the handles) are measured and bits 0/1
(the triggers) are inferred from field order and nothing else.

TWO THINGS MEASURED ON .173, 2026-08-09:

1.  GameInput's device enumeration is ASYNCHRONOUS, and the first `GetCurrentReading` reliably
   returns nothing even with pads actively reporting. This is the GameInput analogue of `wake_wgi`:
   the API looks like a query and is really a cache someone else fills. A bounded poll fixes it.
   ⚠️ Focus is NOT the cause, and the header rules it out rather than my guessing:
   `GameInputDefaultFocusPolicy` is 0 and every `GameInputFocusPolicy` flag is a RESTRICTION, so the
   default already admits background input. Do not "fix" this with `SetFocusPolicy`.

2. 🛑 **GameInput never sees our pad.** Hunting by product id for six seconds with the pad live and
   sweeping, it enumerated `054C:0CE6` (DualSense) and `3434:D031` (8BitDo) — both plain HID pads —
   and never `045E:02FD`, ours, while classic XInput was reading ours live in the same moment.

⇒ THE TRIGGER ENABLE BITS REMAIN CONJECTURE, but for a better reason than before: it is not that
nobody has tried, it is that on this box NOTHING CAN DELIVER a four-motor rumble to our pad. XInput
structurally cannot; GameInput can but does not see it.

⚠️ The obvious suspicion is that `xinputhid` claiming the HID collection exclusively is what hides
the pad from GameInput — which would mean promotion costs us the API most Game-Pass-era titles use,
a trade we have shipped by default. **That is NOT established here.** The decisive control is cheap
and has not been run: power on the REAL Xbox Elite, which Microsoft's own driver promotes the same
way, and see whether GameInput enumerates it. If a real promoted Xbox pad is also absent, this is a
property of GameInput in a non-interactive session and not our defect — the same shape as the WGI
`ts=0` row, which a real Elite reproduced.

VERIFIED
  * `cargo fmt --check` clean; `cargo clippy --target x86_64-pc-windows-msvc --all-targets
    -- -D warnings` clean (cross-checked from macOS).
  * Builds and runs on .173; `GameInputCreate` succeeds, readings arrive after the poll, and
    `SetRumbleState` is accepted.
  * The runtime is loaded by name, so a box without GameInput reports "unavailable" rather than
    failing to link or crashing.

NOT VERIFIED
  * That `SetRumbleState` reaches ANY pad's motors — it was accepted for the DualSense but nothing
    observable was checked on that device, and it never reached ours.
  * `GameInputDeviceInfo` is read only for `vendorId`/`productId` (offsets 4 and 6). The rest of the
    struct has variable-size members whose layout would have to be mirrored exactly; nothing here
    needs them. `supportedRumbleMotors` is in there and would answer "does GameInput think this pad
    has trigger motors" — worth adding if this line of enquiry continues.
2026-08-09 22:55:17 +02:00

609 lines
26 KiB
Rust

//! Which Windows input APIs can see this gamepad?
//!
//! WHY THIS EXISTS. The Xbox-pad-on-Windows programme
//! (`punktfunk-planning/design/xbox-pad-windows-handoff.md`) is a five-row matrix — classic
//! XInput, WGI `Gamepad`, WGI `RawGameController`, GameInput, and the HID/DirectInput/Steam
//! family — and **nothing in this tree measured any of it**. Every reading in that document came
//! from ad-hoc off-tree tools, which is why several of them could not be reproduced or A/B'd
//! afterwards, and why one of them turned out to be a false positive. This makes the matrix a
//! command you can run twice and diff.
//!
//! ⚠️⚠️ **THE FALSE-POSITIVE TRAP, and why `--watch` exists.** A test box usually has REAL pads on
//! it. A real Xbox pad owns XInput slot 0 and appears in WGI, so "I can see a pad" proves nothing.
//! This already burned one session: `XInputGetState(0)` read `rc=0 LX=-885` with the virtual pad
//! live *and* with it killed — slot 0 was always the real Elite.
//! ⇒ **ALWAYS take a baseline with your pad STOPPED and diff it**, and identify entries by name and
//! vendor/product id, never by slot index alone.
//! `--watch N` is the second half of that discipline: it samples repeatedly and reports whether a
//! device's timestamps ADVANCE. An entry that enumerates but never moves is the exact failure mode
//! this programme is chasing — WGI listing a gamepad that reports nothing is arguably worse than
//! not listing it, because a title that binds the first gamepad latches a dead one.
//!
//! GAP: **GameInput is not covered here.** It has no binding in the `windows` crate and needs
//! hand-written COM vtables; it is measured separately for now. Everything else is.
#[cfg(not(windows))]
fn main() {
eprintln!("win-input-matrix is Windows-only.");
std::process::exit(2);
}
#[cfg(windows)]
mod gameinput;
#[cfg(windows)]
mod imp {
use std::time::Duration;
use windows::Foundation::EventHandler;
use windows::Gaming::Input::{Gamepad, IGameController, RawGameController};
use windows::Win32::Devices::DeviceAndDriverInstallation::{
DIGCF_DEVICEINTERFACE, DIGCF_PRESENT, HDEVINFO, SP_DEVICE_INTERFACE_DATA,
SP_DEVICE_INTERFACE_DETAIL_DATA_W, SetupDiDestroyDeviceInfoList,
SetupDiEnumDeviceInterfaces, SetupDiGetClassDevsW, SetupDiGetDeviceInterfaceDetailW,
};
use windows::Win32::Foundation::ERROR_NO_MORE_ITEMS;
use windows::Win32::System::Com::CoIncrementMTAUsage;
use windows::Win32::UI::Input::XboxController::{
XINPUT_STATE, XINPUT_VIBRATION, XInputGetState, XInputSetState,
};
// `Interface` brings `cast()` into scope, which is how a WinRT `Gamepad` is correlated to the
// `RawGameController` that knows its name.
use windows::core::{GUID, Interface};
/// `GUID_DEVINTERFACE_XUSB` — the interface class `xinput1_4` enumerates. This is the one that
/// matters: XInput does not read HID at all, it walks this class.
const GUID_DEVINTERFACE_XUSB: GUID = GUID::from_u128(0xec87f1e3_c13b_4100_b5f7_8b84d54260cb);
/// `GUID_DEVINTERFACE_HID` — what Steam, SDL/hidapi, RawInput, DirectInput and joy.cpl walk.
const GUID_DEVINTERFACE_HID: GUID = GUID::from_u128(0x4d1e55b2_f16f_11cf_88cb_001111000030);
/// Every PRESENT device interface in `class`. Present-only on purpose: the registry lists
/// long-dead devnodes too, and "is it there right now" is the whole question.
fn interfaces(class: GUID) -> Vec<String> {
let mut out = Vec::new();
// SAFETY: `class` is a valid GUID; we pass no enumerator and no owner window. The returned
// handle is destroyed unconditionally below.
let set: HDEVINFO = match unsafe {
SetupDiGetClassDevsW(
Some(&class),
None,
None,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE,
)
} {
Ok(h) => h,
Err(_) => return out,
};
let mut index = 0u32;
loop {
let mut ifdata = SP_DEVICE_INTERFACE_DATA {
cbSize: size_of::<SP_DEVICE_INTERFACE_DATA>() as u32,
..Default::default()
};
// SAFETY: `set` is a live device-info set; `ifdata.cbSize` is initialised as the API
// requires. A failure here means "no more items", which ends the loop.
let ok = unsafe { SetupDiEnumDeviceInterfaces(set, None, &class, index, &mut ifdata) }
.is_ok();
if !ok {
break;
}
index += 1;
// Two-call dance: ask for the required byte count, then fetch into a buffer of that
// size. The detail struct is variable-length (a trailing WCHAR path), so it cannot be
// stack-allocated by type alone.
let mut needed = 0u32;
// SAFETY: passing a null detail pointer with a null size is the documented way to
// query the required length; it always "fails" with ERROR_INSUFFICIENT_BUFFER.
let _ = unsafe {
SetupDiGetDeviceInterfaceDetailW(set, &ifdata, None, 0, Some(&mut needed), None)
};
if needed == 0 {
continue;
}
let mut buf = vec![0u8; needed as usize];
let detail = buf.as_mut_ptr() as *mut SP_DEVICE_INTERFACE_DETAIL_DATA_W;
// SAFETY: `buf` is `needed` bytes, the size the API just asked for. `cbSize` must be
// the size of the FIXED part of the struct (not the buffer) — 8 on x64.
unsafe {
(*detail).cbSize = 8;
}
// SAFETY: `detail` points into `buf`, which lives until the end of this iteration and
// is exactly the length the API requested.
if unsafe {
SetupDiGetDeviceInterfaceDetailW(set, &ifdata, Some(detail), needed, None, None)
}
.is_err()
{
continue;
}
// SAFETY: on success the API wrote a NUL-terminated wide string into `DevicePath`.
let path = unsafe {
let p = (*detail).DevicePath.as_ptr();
let mut len = 0usize;
while *p.add(len) != 0 {
len += 1;
}
String::from_utf16_lossy(std::slice::from_raw_parts(p, len))
};
out.push(path);
}
// SAFETY: `set` came from SetupDiGetClassDevsW and is not used again.
let _ = unsafe { SetupDiDestroyDeviceInfoList(set) };
let _ = ERROR_NO_MORE_ITEMS;
out
}
/// 🛑 **DO NOT DELETE THIS — without it the whole WGI half of the matrix reads zero.**
///
/// `Gamepad::Gamepads()` and `RawGameController::RawGameControllers()` are not queries; they
/// return a cache that WGI's device-watcher fills in. In a GUI app something else has already
/// started that watcher, so the cache looks like a query and everyone writes code as if it
/// were one. In a bare console process nothing has, and both collections come back **EMPTY
/// even with real controllers attached** — measured here on 2026-08-09: a DualSense sitting in
/// the HID interface class, `RawGameControllers` count=0.
///
/// Subscribing to the Added events is what starts the watcher. The handlers deliberately do
/// nothing; registering them is the entire point. The sleep gives the watcher a beat to
/// enumerate before the first read.
///
/// ⚠️ This is a live trap for the readings in `design/xbox-pad-windows-handoff.md`: an
/// off-tree probe without this would report "WGI cannot see the pad" when WGI could not see
/// ANYTHING, which is a very different conclusion.
fn wake_wgi() {
let gp_tok = Gamepad::GamepadAdded(&EventHandler::<Gamepad>::new(|_, _| Ok(())));
let raw_tok = RawGameController::RawGameControllerAdded(
&EventHandler::<RawGameController>::new(|_, _| Ok(())),
);
if gp_tok.is_err() || raw_tok.is_err() {
eprintln!("warning: could not subscribe to WGI Added events; counts may read zero");
}
std::thread::sleep(Duration::from_millis(1500));
}
/// Drive rumble into an XInput slot and hold it, so the other end of the pipe can be watched.
///
/// This is the WP0 probe from `design/trigger-rumble-plane.md`: does anything Windows-side ever
/// write an output report back to a synthesized `045E:0B13`? For the HID backend the chain
/// under test is `XInputSetState` → `xinputhid` → a HID output report on our collection →
/// `on_output_report` → the shm out-ring → `parse_xbox_output`, and the observable is the
/// devtest printing `rumble from game`. Run this with the devtest live and watch its stdout.
///
/// ⚠️ `XINPUT_VIBRATION` has exactly TWO members, so this can only ever drive the two handle
/// motors — it can never source TRIGGER rumble. That is a property of the API, not of our
/// plumbing, and it is why the trigger plane needs its own transport.
fn rumble(slot: u32, seconds: u64) {
println!("== RUMBLE PROBE: XInputSetState(slot {slot}) for {seconds}s ==");
let v = XINPUT_VIBRATION {
wLeftMotorSpeed: 0xFFFF,
wRightMotorSpeed: 0x8000,
};
// SAFETY: `v` is a valid, fully-initialised XINPUT_VIBRATION.
let rc = unsafe { XInputSetState(slot, &v) };
println!(
" set low=0xFFFF high=0x8000 -> rc={rc}{}",
if rc == 0 {
" (accepted)"
} else {
" (REJECTED)"
}
);
if rc != 0 {
println!(" (slot not connected — nothing downstream can be concluded)");
return;
}
std::thread::sleep(Duration::from_secs(seconds));
let off = XINPUT_VIBRATION::default();
// SAFETY: as above.
let rc2 = unsafe { XInputSetState(slot, &off) };
println!(" clear low=0 high=0 -> rc={rc2}");
println!(" ⇒ now check the devtest stdout for `rumble from game`.");
}
fn xinput() {
println!("== classic XInput (xinput1_4 walks GUID_DEVINTERFACE_XUSB) ==");
for slot in 0..4u32 {
let mut st = XINPUT_STATE::default();
// SAFETY: `st` is a valid, fully-initialised XINPUT_STATE for the call to fill in.
let rc = unsafe { XInputGetState(slot, &mut st) };
if rc == 0 {
let g = st.Gamepad;
println!(
" slot {slot}: rc=0 packet={} buttons=0x{:04X} LT={} RT={} LX={} LY={} RX={} RY={}",
st.dwPacketNumber,
g.wButtons.0,
g.bLeftTrigger,
g.bRightTrigger,
g.sThumbLX,
g.sThumbLY,
g.sThumbRX,
g.sThumbRY
);
} else {
println!(
" slot {slot}: rc={rc}{}",
if rc == 1167 {
" (ERROR_DEVICE_NOT_CONNECTED)"
} else {
""
}
);
}
}
}
/// One WGI sample, for the mute detector.
struct Sample {
label: String,
ts: u64,
axes: Vec<f64>,
}
fn wgi_gamepads() -> Vec<Sample> {
let mut out = Vec::new();
let Ok(list) = Gamepad::Gamepads() else {
return out;
};
let n = list.Size().unwrap_or(0);
for i in 0..n {
let Ok(gp) = list.GetAt(i) else { continue };
// Correlate to a RawGameController purely to get a human-readable name — a bare
// `Gamepad` has none, and identifying entries by index is how false positives happen.
let label = gp
.cast::<IGameController>()
.ok()
.and_then(|c| RawGameController::FromGameController(&c).ok())
.and_then(|r| r.DisplayName().ok())
.map(|h| h.to_string())
.unwrap_or_else(|| format!("<gamepad {i}>"));
let (ts, axes) = match gp.GetCurrentReading() {
Ok(r) => (
r.Timestamp,
vec![
r.LeftThumbstickX,
r.LeftThumbstickY,
r.RightThumbstickX,
r.RightThumbstickY,
r.LeftTrigger,
r.RightTrigger,
],
),
Err(_) => (0, Vec::new()),
};
out.push(Sample { label, ts, axes });
}
out
}
fn wgi_raw() -> Vec<Sample> {
let mut out = Vec::new();
let Ok(list) = RawGameController::RawGameControllers() else {
return out;
};
let n = list.Size().unwrap_or(0);
for i in 0..n {
let Ok(rc) = list.GetAt(i) else { continue };
let name = rc
.DisplayName()
.map(|h| h.to_string())
.unwrap_or_else(|_| "<unnamed>".into());
let vid = rc.HardwareVendorId().unwrap_or(0);
let pid = rc.HardwareProductId().unwrap_or(0);
let nb = rc.ButtonCount().unwrap_or(0).max(0) as usize;
let ns = rc.SwitchCount().unwrap_or(0).max(0) as usize;
let na = rc.AxisCount().unwrap_or(0).max(0) as usize;
let mut buttons = vec![false; nb];
let mut switches = vec![Default::default(); ns];
let mut axes = vec![0f64; na];
let ts = rc
.GetCurrentReading(&mut buttons, &mut switches, &mut axes)
.unwrap_or(0);
out.push(Sample {
label: format!("{name} [{vid:04X}:{pid:04X}] buttons={nb} switches={ns} axes={na}"),
ts,
axes,
});
}
out
}
fn print_samples(title: &str, s: &[Sample]) {
println!("== {title} == count={}", s.len());
for (i, e) in s.iter().enumerate() {
let axes = e
.axes
.iter()
.map(|v| format!("{v:.4}"))
.collect::<Vec<_>>()
.join(",");
println!(" [{i}] ts={} {} axes=[{axes}]", e.ts, e.label);
}
if s.is_empty() {
println!(" (none)");
}
}
/// Sample XInput over the whole watch window and report the RANGE each axis covered.
///
/// A single `XInputGetState` call cannot tell "translated correctly" from "stuck at zero" —
/// a sweeping stick reads 0 every time it crosses centre. `dwPacketNumber` advancing proves
/// the state is changing at all; the min/max spread proves the AXES specifically are, which is
/// the half that can fail on its own while buttons work.
struct XiTrack {
first_packet: u32,
last_packet: u32,
lx: (i16, i16),
ly: (i16, i16),
rx: (i16, i16),
ry: (i16, i16),
buttons: u16,
lt: (u8, u8),
rt: (u8, u8),
}
fn xinput_watch(rounds: usize) {
println!("\n== XINPUT WATCH ({rounds} samples) — do PACKETS advance and AXES move? ==");
for slot in 0..4u32 {
let mut t: Option<XiTrack> = None;
for _ in 0..rounds {
let mut st = XINPUT_STATE::default();
// SAFETY: `st` is a valid, fully-initialised XINPUT_STATE.
if unsafe { XInputGetState(slot, &mut st) } != 0 {
break;
}
let g = st.Gamepad;
match &mut t {
None => {
t = Some(XiTrack {
first_packet: st.dwPacketNumber,
last_packet: st.dwPacketNumber,
lx: (g.sThumbLX, g.sThumbLX),
ly: (g.sThumbLY, g.sThumbLY),
rx: (g.sThumbRX, g.sThumbRX),
ry: (g.sThumbRY, g.sThumbRY),
buttons: g.wButtons.0,
lt: (g.bLeftTrigger, g.bLeftTrigger),
rt: (g.bRightTrigger, g.bRightTrigger),
});
}
Some(t) => {
t.last_packet = st.dwPacketNumber;
t.lx = (t.lx.0.min(g.sThumbLX), t.lx.1.max(g.sThumbLX));
t.ly = (t.ly.0.min(g.sThumbLY), t.ly.1.max(g.sThumbLY));
t.rx = (t.rx.0.min(g.sThumbRX), t.rx.1.max(g.sThumbRX));
t.ry = (t.ry.0.min(g.sThumbRY), t.ry.1.max(g.sThumbRY));
t.buttons |= g.wButtons.0;
t.lt = (t.lt.0.min(g.bLeftTrigger), t.lt.1.max(g.bLeftTrigger));
t.rt = (t.rt.0.min(g.bRightTrigger), t.rt.1.max(g.bRightTrigger));
}
}
std::thread::sleep(Duration::from_millis(120));
}
match t {
None => println!(" slot {slot}: not connected"),
Some(t) => {
let moved = t.last_packet != t.first_packet;
let axes_moved = t.lx.0 != t.lx.1
|| t.ly.0 != t.ly.1
|| t.rx.0 != t.rx.1
|| t.ry.0 != t.ry.1
|| t.lt.0 != t.lt.1
|| t.rt.0 != t.rt.1;
println!(
" slot {slot}: packets {}..{} ({}), buttons seen 0x{:04X}",
t.first_packet,
t.last_packet,
if moved { "ADVANCING" } else { "FROZEN" },
t.buttons
);
println!(
" LX [{}..{}] LY [{}..{}] RX [{}..{}] RY [{}..{}] LT [{}..{}] RT [{}..{}] -> axes {}",
t.lx.0,
t.lx.1,
t.ly.0,
t.ly.1,
t.rx.0,
t.rx.1,
t.ry.0,
t.ry.1,
t.lt.0,
t.lt.1,
t.rt.0,
t.rt.1,
if axes_moved { "MOVING" } else { "STUCK" }
);
}
}
}
}
/// Flag every device whose current reading differs from the baseline one. Once a device has
/// moved it stays flagged — a pad that twitches once in twenty samples is still LIVE.
fn mark_moved(base: &[Sample], now: &[Sample], moved: &mut [bool]) {
for (i, e) in now.iter().enumerate() {
if let Some(b) = base.get(i)
&& (b.ts != e.ts || b.axes != e.axes)
&& let Some(m) = moved.get_mut(i)
{
*m = true;
}
}
}
/// Sample repeatedly and report, per device, whether anything ever MOVED. This is the
/// enumerated-but-mute detector: `ts` frozen across every sample means the API lists a pad
/// that is not reporting.
fn watch(rounds: usize) {
println!("\n== WATCH ({rounds} rounds, 200 ms apart) — does anything actually MOVE? ==");
let mut first_gp: Option<Vec<Sample>> = None;
let mut first_raw: Option<Vec<Sample>> = None;
let mut moved_gp: Vec<bool> = Vec::new();
let mut moved_raw: Vec<bool> = Vec::new();
for _ in 0..rounds {
let gp = wgi_gamepads();
let raw = wgi_raw();
match &first_gp {
None => {
moved_gp = vec![false; gp.len()];
first_gp = Some(gp);
}
Some(base) => mark_moved(base, &gp, &mut moved_gp),
}
match &first_raw {
None => {
moved_raw = vec![false; raw.len()];
first_raw = Some(raw);
}
Some(base) => mark_moved(base, &raw, &mut moved_raw),
}
std::thread::sleep(Duration::from_millis(200));
}
for (label, base, moved) in [
("WGI Gamepad", first_gp, moved_gp),
("WGI RawGameController", first_raw, moved_raw),
] {
println!(" {label}:");
let Some(base) = base else { continue };
if base.is_empty() {
println!(" (none)");
}
for (i, e) in base.iter().enumerate() {
println!(
" [{i}] {}{}",
e.label,
if *moved.get(i).unwrap_or(&false) {
"LIVE (readings changed)"
} else {
"MUTE (ts and axes frozen for every sample)"
}
);
}
}
}
pub fn run() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut rounds = 0usize;
let mut rumble_slot: Option<u32> = None;
let mut gameinput_report = false;
let mut gi_rumble: Option<String> = None;
let mut gi_pid: Option<u16> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--watch" => {
rounds = args.get(i + 1).and_then(|v| v.parse().ok()).unwrap_or(20);
i += 1;
}
"--gameinput" => gameinput_report = true,
"--gi-pid" => {
gi_pid = args
.get(i + 1)
.and_then(|v| u16::from_str_radix(v.trim_start_matches("0x"), 16).ok());
i += 1;
}
"--gi-rumble" => {
gi_rumble = args.get(i + 1).cloned();
i += 1;
}
"--rumble" => {
rumble_slot = Some(args.get(i + 1).and_then(|v| v.parse().ok()).unwrap_or(0));
i += 1;
}
"--help" | "-h" => {
println!(
"win-input-matrix [--watch N] [--rumble SLOT]\n\n \
--watch N sample WGI N times and report LIVE vs MUTE per device\n \
--rumble SLOT drive XInputSetState into that slot for 3 s (WP0 probe:\n \
does anything write an output report back to our pad?)\n\n\
ALWAYS take a baseline with your virtual pad STOPPED and diff it: a real\n\
pad on the box owns XInput slot 0 and shows up in WGI."
);
return;
}
other => eprintln!("(ignoring unknown argument {other})"),
}
i += 1;
}
// WinRT needs an initialised apartment. CoIncrementMTAUsage keeps an MTA alive for the
// life of the process without committing this thread to a specific apartment.
// SAFETY: no arguments to get wrong. The cookie is a plain handle value and is dropped on
// purpose — decrementing would tear the MTA down again, and we want it up for the whole
// process.
match unsafe { CoIncrementMTAUsage() } {
Ok(_cookie) => {}
Err(e) => eprintln!("warning: could not start an MTA, WGI calls may fail: {e}"),
}
wake_wgi();
xinput();
println!();
print_samples("WGI Gamepad", &wgi_gamepads());
println!();
print_samples("WGI RawGameController", &wgi_raw());
println!("\n== XUSB device interfaces (GUID_DEVINTERFACE_XUSB, present only) ==");
let xusb = interfaces(GUID_DEVINTERFACE_XUSB);
if xusb.is_empty() {
println!(" (none)");
}
for p in &xusb {
println!(" {p}");
}
println!("\n== HID device interfaces (what Steam/SDL/DirectInput/joy.cpl walk) ==");
let hid = interfaces(GUID_DEVINTERFACE_HID);
println!(" {} present; those matching a gamepad vendor:", hid.len());
for p in &hid {
let lower = p.to_ascii_lowercase();
if lower.contains("vid_045e")
|| lower.contains("vid_054c")
|| lower.contains("punktfunk")
{
println!(" {p}");
}
}
if rounds > 0 {
watch(rounds);
xinput_watch(rounds);
}
if let Some(slot) = rumble_slot {
println!();
rumble(slot, 3);
}
if gameinput_report || gi_rumble.is_some() {
println!("\n== GameInput ==");
match crate::gameinput::GameInput::create() {
Err(e) => println!(" unavailable: {e}"),
Ok(gi) => {
gi.report();
if let Some(spec) = &gi_rumble {
let v: Vec<f32> = spec
.split(',')
.map(|p| p.trim().parse().unwrap_or(0.0))
.collect();
let p = crate::gameinput::GameInputRumbleParams {
lowFrequency: v.first().copied().unwrap_or(0.0),
highFrequency: v.get(1).copied().unwrap_or(0.0),
leftTrigger: v.get(2).copied().unwrap_or(0.0),
rightTrigger: v.get(3).copied().unwrap_or(0.0),
};
gi.rumble(p, std::time::Duration::from_secs(3), gi_pid);
}
}
}
}
}
}
#[cfg(windows)]
fn main() {
imp::run();
}