fix(host): Linux virtual-pad feedback access — hidraw udev rules, per-pad DS MAC, SET_REPORT acks
ci / web (push) Successful in 1m9s
ci / docs-site (push) Successful in 1m3s
ci / bench (push) Successful in 5m49s
decky / build-publish (push) Successful in 19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
docker / deploy-docs (push) Successful in 23s
arch / build-publish (push) Successful in 11m11s
android / android (push) Successful in 14m24s
deb / build-publish (push) Successful in 11m55s
ci / rust (push) Successful in 17m18s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m0s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m54s
windows-host / package (push) Successful in 15m11s
apple / swift (push) Successful in 4m43s
apple / screenshots (push) Successful in 21m4s

Root-cause fixes for "rumble + adaptive triggers never work with Linux hosts"
(the capture code itself was proven good on-hardware — see the new tests):

* 60-punktfunk.rules now grants the `input` group the VIRTUAL pads' hidraw
  nodes (DS/Edge/DS4/Switch/Deck/SC). Steam/SDL drive DualSense adaptive
  triggers, lightbar, and player LEDs exclusively over hidraw — and Steam
  without hidraw demotes a PlayStation pad to a generic evdev device, losing
  its rumble handling too. Coverage no longer depends on the distro's
  steam-devices rules + logind's active-seat uaccess ACL (which a headless/
  dedicated streaming session never gets). Verified live: nodes now come up
  root:input 0660.

* Per-pad MAC in the DualSense (0x09) and DS4 (0x12) pairing feature replies:
  hid-playstation adopts the MAC as the HID uniq and SDL/Steam dedup
  controllers by that serial — identical MACs made a second virtual pad read
  as the first one re-connecting over another transport.

* DualSense/DS4 UHID backends now ack UHID_SET_REPORT (err=0) instead of
  ignoring it, so a SET_REPORT writer no longer blocks on the kernel's 5 s
  timeout.

* New #[ignore] on-box tests play the GAME's role against a real kernel and
  pin the full feedback surface (all green on real hw): DualSense evdev-FF +
  raw hidraw output report (rumble/lightbar/LEDs/both trigger blocks verbatim,
  per-pad uniq), uinput X-Box FF upload→pump→stop-on-erase, and usbip Deck
  0xEB rumble via the controller interface (idle interfaces ACK silently,
  like real hardware).

Windows note: the UMDF driver keeps its own pairing blob copies — the shared-
MAC dedup hazard exists there too and needs a driver-side follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 21:47:13 +02:00
parent 5a384fe788
commit 5c7e0afa99
7 changed files with 557 additions and 12 deletions
@@ -42,6 +42,18 @@ pub const DS_FEATURE_FIRMWARE: &[u8] = &[ // report 0x20 (firmware info / build
0x14, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
/// The pairing reply (report `0x09`) for wire pad `pad`: [`DS_FEATURE_PAIRING`] with the MAC's low
/// octet offset by the pad index. The MAC must be **unique per pad**: `hid-playstation` adopts it
/// as the HID `uniq` (replacing whatever uniq the device was created with), and SDL/Steam dedup
/// controllers by that serial — with identical MACs a second virtual pad reads as the *first* pad
/// re-appearing over another transport and is merged/ignored.
pub fn ds_pairing_reply(pad: u8) -> [u8; 20] {
let mut r = [0u8; 20];
r.copy_from_slice(DS_FEATURE_PAIRING);
r[1] = r[1].wrapping_add(pad); // MAC lives at bytes 1..7, LSB first
r
}
/// Sony DualSense USB HID report descriptor (273 bytes), verbatim from inputtino — the exact
/// descriptor `hid-playstation` (Linux) / `hidclass` (Windows) parses to bind a DualSense.
#[rustfmt::skip]
@@ -923,4 +935,16 @@ mod tests {
assert!(fb.rumble.is_none());
assert!(fb.hidout.is_empty());
}
/// The pairing reply keeps the report id and differs across pads ONLY in the MAC low octet —
/// distinct serials so SDL/Steam never dedup two virtual pads into one controller.
#[test]
fn pairing_reply_mac_is_per_pad() {
assert_eq!(ds_pairing_reply(0).as_slice(), DS_FEATURE_PAIRING);
let (a, b) = (ds_pairing_reply(1), ds_pairing_reply(2));
assert_eq!(a[0], 0x09); // report id untouched
assert_eq!(a[1], DS_FEATURE_PAIRING[1].wrapping_add(1));
assert_eq!(b[1], DS_FEATURE_PAIRING[1].wrapping_add(2));
assert_eq!(a[2..], b[2..]); // everything but the low octet identical
}
}