diff --git a/crates/punktfunk-host/src/devtest.rs b/crates/punktfunk-host/src/devtest.rs index b719cbab..525bbb57 100644 --- a/crates/punktfunk-host/src/devtest.rs +++ b/crates/punktfunk-host/src/devtest.rs @@ -420,17 +420,28 @@ pub fn dualsense_windows_test(args: &[String]) -> Result<()> { } else { 0 }; - let lx = (((i % 64) - 32) * 1024) as i16; // sweep left stick X + // 🛑 Sweep EVERY analogue axis, each on its own phase, and ramp both triggers. + // + // This used to drive LS-X alone and leave the other five at zero, which makes + // the harness unable to tell "this axis is not mapped" from "nothing is driving + // it" — the two look identical in any consumer. That is exactly how a DEAD + // RIGHT STICK survived every bench measurement of the Windows HID Xbox pad and + // was found only on glass (2026-08-09): `XInputGetState` read `RX [0..0]` and it + // was written off as "the devtest doesn't move it", which was true and useless. + // Distinct phases mean one run tells you which axes arrive AND that they are not + // crosstalking onto each other's bytes. + let phase = |off: i32| ((((i + off) % 64) - 32) * 1024) as i16; + let trig = ((i % 32) * 8).clamp(0, 255) as u8; mgr.handle(&GamepadEvent::State(GamepadFrame { index: idx as i16, active_mask: 1 << idx, buttons, - left_trigger: 0, - right_trigger: 0, - ls_x: lx, - ls_y: 0, - rs_x: 0, - rs_y: 0, + left_trigger: trig, + right_trigger: 255 - trig, + ls_x: phase(0), + ls_y: phase(16), + rs_x: phase(32), + rs_y: phase(48), })); } std::thread::sleep(Duration::from_millis(15)); diff --git a/packaging/windows/drivers/pf-gamepad/src/lib.rs b/packaging/windows/drivers/pf-gamepad/src/lib.rs index 3411826d..86a06166 100644 --- a/packaging/windows/drivers/pf-gamepad/src/lib.rs +++ b/packaging/windows/drivers/pf-gamepad/src/lib.rs @@ -376,10 +376,21 @@ static XBOX_RDESC: [u8; 223] = [ 0x75, 0x10, // Report Size (16) 0x81, 0x02, // Input (Data,Var,Abs) 0xC0, // End Collection + // 🛑 THE RIGHT STICK IS `Z`/`Rz`, NOT `Rx`/`Ry`. This declared `Rx`/`Ry` until 2026-08-09 and + // the right stick was DEAD: measured on `.173`, with every axis sweeping on its own phase, + // `LX`/`LY`/`LT`/`RT` all reached XInput and `RX [0..0] RY [-1..-1]` never moved. Left and right + // were declared identically here apart from these two usage bytes, so the usages are the whole + // difference — `xinputhid`, which translates this collection into XUSB, maps `Z`/`Rz` to the + // right stick and does not treat `Rx`/`Ry` as one. `DUALSENSE_RDESC` above (a real capture) uses + // `Z`/`Rz` for its right stick too; the PS pads put the TRIGGERS on `Rx`/`Ry`, which is probably + // where the original mistake came from. + // ⚠️ This survived every bench measurement because the devtest only ever swept LS-X — the axis + // that worked — so `RX [0..0]` read as "nothing is driving it". It was found on glass. The + // devtest now sweeps all six axes on distinct phases so the harness can tell those two apart. 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) - 0x09, 0x33, // Usage (Rx) — right stick X - 0x09, 0x34, // Usage (Ry) — right stick Y + 0x09, 0x32, // Usage (Z) — right stick X + 0x09, 0x35, // Usage (Rz) — right stick Y 0x15, 0x00, // Logical Minimum (0) 0x27, 0xFF, 0xFF, 0x00, 0x00, // Logical Maximum (65535) 0x95, 0x02, // Report Count (2)