fix(drivers/pf-gamepad): the right stick is Z/Rz — as declared, it was dead
Found on glass, first real streaming session: everything worked except the right stick, and Steam correctly showed "Xbox One S Controller". `XBOX_RDESC` declared the right stick as `Rx`/`Ry`. `xinputhid`, which translates our HID collection into XUSB, maps `Z`/`Rz` to the right stick and does not treat `Rx`/`Ry` as one, so those two axes reached nothing. Two usage bytes. Left and right were declared identically here — same collection, same globals, same size and count — so the usages were the entire difference, which is what makes the diagnosis airtight rather than plausible. Note `DUALSENSE_RDESC`, a real capture, also uses `Z`/`Rz` for its right stick and puts the TRIGGERS on `Rx`/`Ry`; that is most likely where the original mistake came from. ⚠️ Byte offsets are unchanged — still 16×2 at bit 5.0 — so `xbox_proto`'s layout tests and the host-side packing are untouched. This is a pure relabelling. 🛑 THE REAL LESSON IS THE HARNESS, AND IT IS FIXED HERE TOO. This survived every bench measurement because `dualsense-windows-test` drove LS-X and the A button and left the other five analogue axes at zero. `XInputGetState` read `RX [0..0]`, which I read as "the devtest doesn't move it" — true, and useless: a harness that exercises one axis cannot tell "this axis is not mapped" from "nothing is driving it", and the two are indistinguishable in every consumer. The devtest now sweeps all six axes on distinct phases and ramps both triggers, so one run shows which axes arrive AND that they are not crosstalking onto each other's bytes. MEASURED ON .173, same run shape before and after, devtest sweeping all six axes: before: LX [-11264..24576] LY [-32768..31744] RX [0..0] RY [-1..-1] LT [0..248] RT [7..255] after: LX [-8192..26624] LY [-32768..31744] RX [-32768..31744] RY [-24576..10240] LT [0..248] RT [7..255] VERIFIED * `cargo test -p pf-inject --lib` 104/104 on Windows; `xbox` subset 11/11 on macOS — the layout tests still pass because nothing moved. * Driver rebuilds and signs; the descriptor is still 223 bytes so the `wReportLength` const assert is undisturbed. * `cargo fmt --all --check` clean. NOT VERIFIED * Not yet re-tested in a real streaming session — that is the next on-glass run. * ⚠️ A leftover finding from the same session, unrelated to this fix and NOT investigated: the session's pad devnode SURVIVES client disconnect and keeps the `Global\pfds-boot-0` bootstrap mailbox, so a devtest run afterwards fails with `Zugriff verweigert (0x80070005)` and silently measures the stale pad instead. Restarting the service releases it. Worth its own look.
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user