feat(pads/windows): three Xbox identities — Wireless, One S and Elite Series 2

Until now there was one Xbox identity, `device_type = 4` / `045E:0B13`, and Windows folded a
client's `XboxOne` request onto it because the only Windows Xbox backend was the XUSB companion,
which presents one fixed 360 identity and cannot vary it. The HID backend can, so the fold goes and
two identities join it:

  devtype 4  045E:0B13  pf_xboxwireless  Xbox Wireless Controller
  devtype 5  045E:02FD  pf_xboxones      Xbox Wireless Controller (One S)
  devtype 6  045E:0B22  pf_xboxelite     Xbox Elite Wireless Controller Series 2

`GamepadPref::XboxElite` takes wire byte 11 — the first unassigned one, and the round-trip test
previously asserted `from_u8(11) == Auto` with a comment saying assigning it must update that; the
sentinel moved to 12. The C ABI mirror and the generated header moved with it.

 ALL THREE SHARE ONE REPORT DESCRIPTOR, deliberately. In HID terms they are the same pad; the
descriptor is the report shape, not the identity. §3 of the handoff records that our single
hand-written descriptor already cost three separate bugs, and inventing two more would multiply
that debt for no measured gain. They differ in VID/PID, product string, hardware id and Device
Manager description only.

⚠️ All three install `pfGamepadXbox`, the section that attaches the `xinputhid` bus filter. That
was the open risk: Microsoft's `xinputhid.inf` promotes by an explicit hardware-id allow-list
containing `02D1, 02DD, 02E3, 02EA, 0B00, 0B0A, 0B13, 02FF` — and NEITHER `02FD` NOR `0B22` is on
it. Measured on .173: promotion does not care, because it comes from our own AddReg rather than
from matching Microsoft's ids. All three gain `IG_00`, register an XUSB interface, and are read
live by classic XInput. Had this gone the other way the two new identities would have been strictly
worse than the one they joined.

The XUSB escape hatch needed a runtime degrade to stay honest. `pick_gamepad` is compile-time only,
so with `PUNKTFUNK_XBOX_BACKEND=xusb` the host would have resolved and echoed `xboxelite` in its
`Welcome` while actually building a 360 pad. `degrade_xbox_identity` folds the identity back at
runtime, mirroring `degrade_if_no_uhid`.

VERIFIED ON WINDOWS (.173 — none of this compiles on macOS; the driver needs the WDK and the rest
is `cfg(windows)`):
  * `cargo test -p pf-inject --lib` 104/104 — including `hwid_matches_inf`,
    `hwid_devtype_table_matches_the_driver` and `only_the_xbox_identity_installs_the_xinputhid_section`,
    all now sweeping the whole identity set and asserting the section split in both directions.
  * `cargo test -p punktfunk-core --lib gamepad` 7/7; `cargo check -p punktfunk-host` clean.
  * Driver builds and signs; the descriptor/`wReportLength` const asserts still hold with the
    descriptor shared three ways.
  * ON GLASS, per identity, via the new `--xboxones` / `--xboxelite` devtest legs: each gets its own
    devnode (`PF_XBOX_0` / `PF_XBOX_ONES_0` / `PF_XBOX_ELITE_0`), each HID child gains `IG_00`, each
    registers an XUSB interface, and XInput reads each live (packets advancing, `buttons=0x1000`).
  * macOS: `cargo fmt --all --check` clean in both workspaces.

NOT VERIFIED / NOT DONE
  * **Elite paddles are NOT implemented.** `BTN_PADDLE1..4` would need descriptor buttons, and once
    `xinputhid` promotes the pad it claims the HID collection exclusively — XInput has no paddle
    fields and the HID consumers that do may be locked out, so the buttons would likely reach
    nobody. The decisive measurement is cheap and named in the code: hold a paddle bit set and see
    whether a user-mode HID reader still gets reports. Until then the Edge remains the only virtual
    pad with native back-button slots and nothing should be advertised otherwise.
  * **No client picker offers the Elite**, and none can auto-detect it — SDL3's `GamepadType` has no
    Elite variant. It is reachable today only via `PUNKTFUNK_GAMEPAD=xboxelite` or a hand-edited
    client setting. All five clients ship the same curated six options by deliberate parity, so
    adding one is a cross-client UX change, not part of this.
  * Nothing here has run in a real streaming session; every measurement came from the devtest.
This commit is contained in:
2026-08-09 21:41:53 +02:00
parent bd5735b803
commit 4f9071b980
13 changed files with 572 additions and 124 deletions
+3
View File
@@ -276,6 +276,9 @@ impl PadInfo {
GamepadPref::DualSenseEdge => "DualSense Edge",
GamepadPref::DualShock4 => "DualShock 4",
GamepadPref::XboxOne => "Xbox One",
// Unreachable from `pref_for_type` today — SDL has no Elite `GamepadType` — but a
// pinned setting can carry it, and an empty label there reads as a plain Xbox pad.
GamepadPref::XboxElite => "Xbox Elite Series 2",
GamepadPref::SteamDeck => "Steam Deck",
GamepadPref::SteamController => "Steam Controller",
GamepadPref::SteamController2 => "Steam Controller 2",
+18
View File
@@ -804,6 +804,24 @@ pub mod gamepad {
/// `XBOX_INPUT_REPORT_LEN` (16). The driver serves per-identity report lengths because
/// hidclass sizes its buffer from the descriptor and refuses an over-long source.
pub const DEVTYPE_XBOX: u8 = 4;
/// `device_type` = Xbox One S controller over Bluetooth (`VID_045E&PID_02FD`).
///
/// ⭐ **Shares [`DEVTYPE_XBOX`]'s report descriptor, byte for byte.** All three Xbox identities
/// are the same pad in HID terms — same axes, same trigger pair, same hat, same 15 buttons,
/// same rumble output report — and differ ONLY in VID/PID, product string and INF model line.
/// The descriptor is the report SHAPE; the identity is what the OS keys mappings off. Giving
/// each identity its own hand-written descriptor would triple a debt that has already cost
/// three separate bugs (see the `XBOX_RDESC` provenance block in the driver).
pub const DEVTYPE_XBOX_ONE_S: u8 = 5;
/// `device_type` = Xbox Elite Wireless Controller Series 2 (`VID_045E&PID_0B22`) — the
/// hardware `tools/hid-descriptor-dump` captured on `.173`.
///
/// ⚠️ The four paddles are NOT in this identity's report yet. See [`DEVTYPE_XBOX_ONE_S`] for
/// why the descriptor is shared, and `design/xbox-pad-windows-handoff.md` §4 WP-C for the
/// unresolved tension: once the pad is promoted, `xinputhid` claims the HID collection
/// exclusively, so extra buttons declared here may be invisible to every consumer anyway.
/// That needs measuring before it is built.
pub const DEVTYPE_XBOX_ELITE: u8 = 6;
/// The value a gamepad driver writes into its section's `driver_proto` field once it attaches —
/// the host's positive "driver is alive on this section" signal (health check + version audit).
@@ -1031,8 +1031,15 @@ mod drain_tests {
WinDsIdentity::dualsense_edge().hwid,
super::super::dualshock4_windows::DS4_HWID,
super::super::steam_deck_windows::DECK_HWID,
super::super::xbox_windows::XBOX_HWID,
] {
]
.into_iter()
// Every Xbox identity, not just the first — a new one added to the table without its INF
// model line is exactly the "pad exists, never starts, never answers a proof" failure.
.chain(
super::super::xbox_windows::XBOX_IDENTITIES
.iter()
.map(|i| i.hwid),
) {
let want = hwid.to_ascii_lowercase();
let rooted = format!("root\\{want}");
assert!(
@@ -1046,7 +1053,7 @@ mod drain_tests {
}
}
/// The Xbox identity must install its OWN section, and the PlayStation/Deck identities must
/// EVERY Xbox identity must install its OWN section, and the PlayStation/Deck identities must
/// not install that one.
///
/// `pfGamepadXbox` attaches Microsoft's `xinputhid` as an upper filter and sets
@@ -1057,7 +1064,10 @@ mod drain_tests {
/// exclusively and would take a working pad away from Steam and SDL.
///
/// Merging the two sections back together is a one-line edit that looks like tidying and is
/// not, so assert the split rather than trusting a comment to survive.
/// not, so assert the split rather than trusting a comment to survive. Both directions matter,
/// and so does the count: a new Xbox identity whose model line was pasted from a PlayStation
/// one installs `pfGamepad`, enumerates perfectly, and is simply never promoted — a silent
/// half-failure that reads on glass as "XInput doesn't see it", the original field symptom.
#[test]
fn only_the_xbox_identity_installs_the_xinputhid_section() {
let inx = concat!(
@@ -1065,9 +1075,12 @@ mod drain_tests {
"/../../packaging/windows/drivers/pf-gamepad/pf_gamepad.inx"
);
let inf = std::fs::read_to_string(inx).expect("read pf_gamepad.inx");
let xbox = super::super::xbox_windows::XBOX_HWID.to_ascii_lowercase();
let xbox: Vec<String> = super::super::xbox_windows::XBOX_IDENTITIES
.iter()
.map(|i| i.hwid.to_ascii_lowercase())
.collect();
let mut saw_xbox_model = false;
let mut seen: Vec<&str> = Vec::new();
for line in inf.lines().map(str::trim).filter(|l| !l.starts_with(';')) {
let Some((_, rhs)) = line.split_once('=') else {
continue;
@@ -1083,28 +1096,36 @@ mod drain_tests {
.split(',')
.map(|i| i.trim().to_ascii_lowercase())
.collect();
let mentions_xbox = ids.iter().any(|i| i.contains(&xbox));
if mentions_xbox {
saw_xbox_model = true;
assert_ne!(
section, "pfGamepad",
"the Xbox model line installs the SHARED section, so the xinputhid filter \
would be attached to every PlayStation and Deck pad too"
);
} else {
// `contains`, not `==`: the model lines carry both the bare id and its `root\` twin.
let matched: Vec<&str> = xbox
.iter()
.filter(|x| ids.iter().any(|i| i.contains(x.as_str())))
.map(String::as_str)
.collect();
if matched.is_empty() {
assert_eq!(
section, "pfGamepad",
"a non-Xbox model line ({ids:?}) installs {section:?}; if that section carries \
the xinputhid filter, this pad is about to be handed to Microsoft's Xbox \
translator"
);
} else {
seen.extend(matched);
assert_ne!(
section, "pfGamepad",
"an Xbox model line ({ids:?}) installs the SHARED section, so either the \
xinputhid filter would be attached to every PlayStation and Deck pad too, or \
this Xbox pad silently never gets promoted"
);
}
}
assert!(
saw_xbox_model,
"no [Models] line mentions {xbox:?} — the parse went vacuous; fix it rather than \
deleting the assert"
);
for want in &xbox {
assert!(
seen.contains(&want.as_str()),
"no [Models] line mentions {want:?} — either the identity has no INF line at all, \
or the parse went vacuous; fix that rather than deleting the assert"
);
}
}
/// The driver reads its HID identity back off the same hardware id — that mapping is what
@@ -1146,7 +1167,7 @@ mod drain_tests {
.collect();
assert_eq!(
entries.len(),
5,
7,
"parsed {entries:?} out of the driver's table — the shape changed and this test went \
vacuous; fix the parse rather than deleting the assert"
);
@@ -1173,11 +1194,16 @@ mod drain_tests {
super::super::steam_deck_windows::DECK_HWID,
pf_driver_proto::gamepad::DEVTYPE_STEAMDECK,
),
(
super::super::xbox_windows::XBOX_HWID,
pf_driver_proto::gamepad::DEVTYPE_XBOX,
),
] {
]
.into_iter()
// All three Xbox identities: they share a report descriptor, so a hwid→devtype slip does
// NOT show up as a mangled report the way the Deck's did — it shows up as the wrong PID and
// the wrong product string, i.e. an Elite that Steam maps as a Series X|S pad.
.chain(
super::super::xbox_windows::XBOX_IDENTITIES
.iter()
.map(|i| (i.hwid, i.devtype)),
) {
let want = hwid.to_ascii_lowercase();
let got = entries.iter().find(|(id, _)| *id == want);
assert_eq!(
@@ -1,5 +1,6 @@
//! Virtual Xbox Wireless Controller on Windows via the UMDF HID minidriver (device-type 4) — the
//! HID-visible alternative to [`super::gamepad_windows`]'s XUSB companion.
//! Virtual Xbox pads on Windows via the UMDF HID minidriver — Xbox Wireless (device-type 4),
//! Xbox One S (5) and Xbox Elite Series 2 (6), the HID-visible alternative to
//! [`super::gamepad_windows`]'s XUSB companion.
//!
//! **Why this exists.** `pf-xusb` registers only `GUID_DEVINTERFACE_XUSB` and exposes no HID
//! collection, so Steam's hidapi enumeration, DirectInput, `joy.cpl` and WGI/GameInput cannot see
@@ -10,15 +11,16 @@
//! and install path the PlayStation pads already ship on.
//!
//! Transport is identical to the PS/Deck pads: a `SwDeviceCreate` devnode plus the sealed
//! shared-memory channel, with `device_type = 4` stamped before the magic so the driver resolves
//! the Xbox identity before hidclass asks it for descriptors. The codec is
//! [`super::xbox_proto`]; the report it writes mirrors the driver's `XBOX_RDESC` byte for byte.
//! shared-memory channel, with the identity's `device_type` stamped before the magic so the driver
//! resolves it before hidclass asks for descriptors. The codec is
//! [`super::xbox_proto`]; the report it writes mirrors the driver's `XBOX_RDESC` byte for byte
//! **one descriptor, all three identities** (see `WinXboxIdentity` below).
//!
//! ⚠️ **The synthesized USB identity is a BLUETOOTH Xbox pad (`045E:0B13`) on purpose.** The wired
//! ids the rest of the tree uses (`045E:028E` X-Box 360, `045E:02EA` Xbox One S USB) are
//! vendor-class XUSB/GIP devices that expose no HID interface on real hardware — a HID child
//! claiming one is a device that has never existed, and Windows' inbox promotion would have nothing
//! to match. See `pf-gamepad`'s `XBOX_PID` for the alternate to try if `0B13` is not promoted.
//! ⚠️ **Every synthesized USB identity here is a BLUETOOTH Xbox pad on purpose.** The wired ids the
//! rest of the tree uses (`045E:028E` X-Box 360, `045E:02EA` Xbox One S USB) are vendor-class
//! XUSB/GIP devices that expose no HID interface on real hardware — a HID child claiming one is a
//! device that has never existed, and Windows' inbox promotion would have nothing to match. The
//! Bluetooth ids (`0B13` / `02FD` / `0B22`) are the Xbox pads that genuinely ARE HID.
//!
//! ⚠️ **No rich plane.** An Xbox pad has no touchpad, no lightbar, no adaptive triggers and no
//! IMU in its HID contract, so `apply_rich` / `clear_rich` / `neutralize_gyro` are deliberately
@@ -36,14 +38,106 @@ use anyhow::Result;
use punktfunk_core::quic::RichInput;
use std::time::Duration;
/// The hardware id this pad's devnode carries. Must be one `pf_gamepad.inx` declares — a package
/// rename must never touch it (`dualsense_windows::tests::hwid_matches_inf` enforces that).
pub(super) const XBOX_HWID: &str = "pf_xboxwireless";
/// One of the Xbox identities this backend can present. Mirrors `WinDsIdentity`
/// (`super::dualsense_windows`) for the PlayStation family: the whole transport (section layout,
/// report codec, output parse, INF install section) is shared, and only the PnP identity plus the
/// `device_type` stamp differ.
///
/// ⭐ **All three share ONE report descriptor in the driver** — `XBOX_RDESC`. In HID terms they are
/// the same pad: same stick pairs, same trigger pair, same hat, same 15 buttons, same rumble output
/// report. A descriptor is the report SHAPE; the identity is what SDL/Steam/Windows key their stock
/// mappings off, and that travels in the VID/PID below. See the `XBOX_RDESC` provenance block in
/// `packaging/windows/drivers/pf-gamepad/src/lib.rs` for why inventing two more hand-written
/// descriptors would be a net loss.
pub(super) struct WinXboxIdentity {
/// `device_type` stamped into the section — the driver picks its VID/PID and product string
/// off it, before hidclass asks anything.
pub devtype: u8,
/// PnP instance-id prefix — distinct namespaces per identity, so two Xbox models never reuse
/// the same devnode shell.
pub instance_prefix: &'static str,
/// The INF-matched hardware id. Must be one `pf_gamepad.inx` declares, on a model line that
/// installs `pfGamepadXbox` — a package rename must never touch it
/// (`dualsense_windows::tests::hwid_matches_inf` and
/// `only_the_xbox_identity_installs_the_xinputhid_section` enforce both halves).
pub hwid: &'static str,
/// The USB VID&PID token synthesized onto the devnode so hidclass derives the real-pad HID
/// child ids (`HID\VID_045E&PID_xxxx`) — the identity SDL/RawInput/WGI read, and the one
/// Windows' own Xbox INFs match when they decide whether to promote a HID gamepad.
pub usb_vid_pid: &'static str,
/// Device Manager description.
pub description: &'static str,
}
/// The USB VID&PID token synthesized onto the devnode so hidclass derives the real-pad HID child
/// ids (`HID\VID_045E&PID_0B13`) — the identity SDL/RawInput/WGI read, and the one Windows' own
/// Xbox INFs match when they decide whether to promote a HID gamepad to an Xbox-profile pad.
const XBOX_USB_VID_PID: &str = "VID_045E&PID_0B13";
impl WinXboxIdentity {
/// Xbox Wireless Controller (Series X|S) over Bluetooth, `045E:0B13` — the default.
///
/// ⭐ Its PID is on Microsoft's `xinputhid.inf` allow-list twice (measured on `.173`,
/// 2026-08-09). That is not what promotes OUR pad — a software devnode matches no allow-list
/// entry, so `pfGamepadXbox`'s `AddReg` writes the two registry values those sections would
/// have written — but it is why this identity, not one of the two below, stays the default.
pub(super) const fn wireless() -> WinXboxIdentity {
WinXboxIdentity {
devtype: pf_driver_proto::gamepad::DEVTYPE_XBOX,
instance_prefix: "pf_xbox",
hwid: "pf_xboxwireless",
usb_vid_pid: "VID_045E&PID_0B13",
description: "Punktfunk Virtual Xbox Wireless Controller",
}
}
/// Xbox One S controller over Bluetooth, `045E:02FD`.
///
/// ⚠️ `02FD` appears in `xinputhid.inf` only as a `BTHENUM` (classic-BT bus) id — it has **no**
/// stage-2 `HID\…&IG_00` model line, unlike `0B13`. Promotion here rides entirely on our own
/// `AddReg`, so it should behave identically; but if a servicing update ever makes promotion
/// depend on Microsoft's list again, this is the identity that loses it first. UNVERIFIED on
/// glass.
pub(super) const fn one_s() -> WinXboxIdentity {
WinXboxIdentity {
devtype: pf_driver_proto::gamepad::DEVTYPE_XBOX_ONE_S,
instance_prefix: "pf_xbox_ones",
hwid: "pf_xboxones",
usb_vid_pid: "VID_045E&PID_02FD",
description: "Punktfunk Virtual Xbox One S Controller",
}
}
/// Xbox Elite Wireless Controller Series 2, `045E:0B22` — the pad
/// `tools/hid-descriptor-dump` captured on `.173`, so the one identity here whose real hardware
/// has been measured directly.
///
/// ⚠️ **No paddles yet.** `BTN_PADDLE1..4` still fold/drop for this identity exactly as for the
/// other two; the Elite is merely the first Xbox pad that *could* carry them natively. Adding
/// them is blocked on a measurement, not on effort — once `xinputhid` promotes the pad it
/// claims the HID collection exclusively, so extra buttons declared in the descriptor may be
/// invisible to every consumer anyway (handoff §3.6). Measure before building.
pub(super) const fn elite() -> WinXboxIdentity {
WinXboxIdentity {
devtype: pf_driver_proto::gamepad::DEVTYPE_XBOX_ELITE,
instance_prefix: "pf_xbox_elite",
hwid: "pf_xboxelite",
usb_vid_pid: "VID_045E&PID_0B22",
description: "Punktfunk Virtual Xbox Elite Wireless Controller Series 2",
}
}
}
/// Every Xbox identity this backend can build, in wire order (`device_type` 4, 5, 6).
///
/// A table rather than three loose constructors because the INF tests sweep it: each entry's
/// `hwid` must appear in `pf_gamepad.inx` **on a `pfGamepadXbox` model line**, and every non-Xbox
/// model line must NOT be on that section. A new identity added here without its INF line fails
/// those tests instead of failing on a user's box.
///
/// `static`, not `const`, on purpose: [`XboxWinProto`] holds a `&'static WinXboxIdentity`, and a
/// `const` is inlined at each use site — `&CONST[i]` would depend on rvalue static promotion to
/// come out `'static` at all.
pub(super) static XBOX_IDENTITIES: [WinXboxIdentity; 3] = [
WinXboxIdentity::wireless(),
WinXboxIdentity::one_s(),
WinXboxIdentity::elite(),
];
/// A single virtual Xbox pad: the `SwDeviceCreate`'d `pf_xbox_<index>` devnode plus the sealed
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
@@ -61,9 +155,9 @@ pub struct XboxWinPad {
}
impl XboxWinPad {
/// Create the sealed channel, stamp `device_type = Xbox` FIRST + the pad index + the neutral
/// report + the magic LAST, then spawn the devnode under the Bluetooth Xbox identity.
fn open(index: u8) -> Result<XboxWinPad> {
/// Create the sealed channel, stamp `device_type` FIRST + the pad index + the neutral report +
/// the magic LAST, then spawn the devnode under `id`'s Bluetooth Xbox identity.
fn open(index: u8, id: &WinXboxIdentity) -> Result<XboxWinPad> {
let boot_name = pf_driver_proto::gamepad::pad_boot_name(index);
let mut channel = PadChannel::create(boot_name.clone(), SHM_SIZE)?;
let base = channel.data_base();
@@ -71,7 +165,7 @@ impl XboxWinPad {
// device_type MUST land before the magic — the driver reads it the moment it attaches, and
// a late stamp enumerates the pad with the default DualSense identity (the Deck's bug).
unsafe {
*base.add(OFF_DEVTYPE) = pf_driver_proto::gamepad::DEVTYPE_XBOX;
*base.add(OFF_DEVTYPE) = id.devtype;
std::ptr::write_unaligned(base.add(OFF_PAD_INDEX) as *mut u32, index as u32);
// Ring capability `2` = "this host drains the v2.2 long ring" (see the DualSense open).
std::ptr::write_unaligned(base.add(OFF_OUT_RING_VER) as *mut u32, 2);
@@ -81,17 +175,20 @@ impl XboxWinPad {
);
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
}
let inst = format!("pf_xbox_{index}");
let inst = format!("{}_{index}", id.instance_prefix);
let (hsw, instance_id) = create_swdevice(&SwDeviceProfile {
instance: &inst,
// Per-FAMILY tag, like "PFDS" for the whole PlayStation family: the three Xbox
// identities share it because only one of them can ever hold a given pad index (the
// router keeps a live device in its owning manager), so their containers never collide.
container_tag: 0x5046_5842, // "PFXB"
container_index: index,
hwid: XBOX_HWID,
usb_vid_pid: XBOX_USB_VID_PID,
hwid: id.hwid,
usb_vid_pid: id.usb_vid_pid,
// A Bluetooth pad is not a USB composite device, so there is no interface number to
// synthesize — unlike the Deck, whose Steam promotion gate needs `&MI_02`.
usb_mi: None,
description: "Punktfunk Virtual Xbox Wireless Controller",
description: id.description,
})?; // Propagate — swallowing latched the slot to a pad with no devnode (see the DS4 twin).
channel.bind_devnode(
index as u32,
@@ -99,14 +196,14 @@ impl XboxWinPad {
super::gamepad_raii::ProofTransport::HidFeatureReport,
);
let _sw = Some(super::gamepad_raii::SwDevice::new(hsw));
// Bounded eager delivery — the driver must read `device_type = 4` before hidclass asks it
// for descriptors, or the pad enumerates as a DualSense.
// Bounded eager delivery — the driver must read the `device_type` stamp before hidclass
// asks it for descriptors, or the pad enumerates as a DualSense.
channel.deliver_eager(Duration::from_millis(1500));
Ok(XboxWinPad {
_sw,
channel,
attach: super::gamepad_raii::DriverAttach::new(
"pf_xboxwireless",
id.hwid,
"pf_gamepad.inf", // one driver package serves every identity
"C:\\Windows\\ServiceProfiles\\LocalService\\AppData\\Local\\Temp\\pf_gamepad-driver.log",
boot_name,
@@ -190,8 +287,37 @@ fn parse_xbox_output(bytes: &[u8]) -> Option<(u16, u16, u16, u16)> {
/// The Windows-Xbox half of the shared stateful manager (see [`PadProto`]). Lifecycle (slot table,
/// unplug sweep, heartbeat, rumble dedup) lives in [`UhidManager`], exactly as for the PS pads.
#[derive(Default)]
pub struct XboxWinProto;
///
/// The identity is a field rather than three separate proto types because nothing else about the
/// backend varies: same codec, same output parse, same rumble plane. `Default` is the Xbox Wireless
/// Controller, so `XboxWindowsManager::new()` keeps its previous meaning exactly.
pub struct XboxWinProto {
identity: &'static WinXboxIdentity,
}
impl Default for XboxWinProto {
fn default() -> XboxWinProto {
XboxWinProto {
identity: &XBOX_IDENTITIES[0],
}
}
}
impl XboxWinProto {
/// The Xbox One S identity (`045E:02FD`) — `UhidManager::with_backend(XboxWinProto::one_s())`.
pub fn one_s() -> XboxWinProto {
XboxWinProto {
identity: &XBOX_IDENTITIES[1],
}
}
/// The Xbox Elite Series 2 identity (`045E:0B22`).
pub fn elite() -> XboxWinProto {
XboxWinProto {
identity: &XBOX_IDENTITIES[2],
}
}
}
impl PadProto for XboxWinProto {
type Pad = XboxWinPad;
@@ -202,10 +328,12 @@ impl PadProto for XboxWinProto {
" (install/repair: punktfunk-host.exe driver install --gamepad)";
fn open(&mut self, idx: u8) -> Result<XboxWinPad> {
let p = XboxWinPad::open(idx)?;
let p = XboxWinPad::open(idx, self.identity)?;
tracing::info!(
index = idx,
"virtual Xbox Wireless Controller created (Windows UMDF HID identity 045E:0B13)"
identity = self.identity.usb_vid_pid,
description = self.identity.description,
"virtual Xbox pad created (Windows UMDF HID)"
);
Ok(p)
}
+8 -5
View File
@@ -474,8 +474,9 @@ pub mod uhid_abi;
#[cfg(any(target_os = "linux", target_os = "windows"))]
#[path = "inject/uhid_manager.rs"]
pub mod uhid_manager;
/// Transport-independent Xbox Wireless Controller HID codec — the report the `pf-gamepad` UMDF
/// driver serves under device-type 4, giving an Xbox pad the HID footing `pf-xusb` never had
/// Transport-independent Xbox HID codec — the report the `pf-gamepad` UMDF driver serves under
/// device-types 4, 5 and 6 (Xbox Wireless / One S / Elite Series 2, which share one descriptor and
/// differ only in VID/PID), giving an Xbox pad the HID footing `pf-xusb` never had
/// (Steam / WGI / GameInput / DirectInput cannot see an XUSB-interface-only device).
///
/// Deliberately NOT cfg-gated to linux/windows like its siblings: it is pure byte-packing with no
@@ -484,9 +485,11 @@ pub mod uhid_manager;
/// has until a Windows box is reachable.
#[path = "inject/proto/xbox_proto.rs"]
pub mod xbox_proto;
/// Windows: virtual Xbox Wireless Controller via the same UMDF minidriver (device-type 4) — the
/// HID-visible alternative to [`gamepad_windows`]'s XUSB companion, which Steam / WGI / GameInput /
/// DirectInput cannot enumerate at all because it registers only the XUSB device interface.
/// Windows: virtual Xbox pads via the same UMDF minidriver — Xbox Wireless (device-type 4),
/// Xbox One S (5) and Xbox Elite Series 2 (6), the HID-visible alternative to
/// [`gamepad_windows`]'s XUSB companion, which Steam / WGI / GameInput / DirectInput cannot
/// enumerate at all because it registers only the XUSB device interface. The three identities
/// share one report descriptor and differ only in VID/PID, product string and INF model line.
#[cfg(target_os = "windows")]
#[path = "inject/windows/xbox_windows.rs"]
pub mod xbox_windows;
+6
View File
@@ -1219,6 +1219,11 @@ pub const PUNKTFUNK_GAMEPAD_STEAMCONTROLLER2: u32 = 9;
/// topology and four controller slots. Used by capture clients that own the physical Puck;
/// ordinary wired/BLE SC2 capture remains `STEAMCONTROLLER2`.
pub const PUNKTFUNK_GAMEPAD_STEAMCONTROLLER2_PUCK: u32 = 10;
/// Xbox Elite Wireless Controller Series 2 (`045E:0B22`, Bluetooth): a Windows-only HID identity
/// through the UMDF minidriver, so glyphs and the device name read Elite. Folds to X-Box 360
/// elsewhere. ⚠️ Identity only — the four paddles still fold/drop exactly as on the other X-Box
/// classes (`DUALSENSEEDGE` is the pad with native back-button slots).
pub const PUNKTFUNK_GAMEPAD_XBOXELITE: u32 = 11;
/// Extended `InputEvent` gamepad button bits for embedders building raw events: the four back grips
/// (Steam L4/L5/R4/R5 ≙ Xbox-Elite P1P4) + the misc/capture button, in Moonlight's
@@ -1344,6 +1349,7 @@ const _: () = {
assert!(
PUNKTFUNK_GAMEPAD_STEAMCONTROLLER2_PUCK == GamepadPref::SteamController2Puck.to_u8() as u32
);
assert!(PUNKTFUNK_GAMEPAD_XBOXELITE == GamepadPref::XboxElite.to_u8() as u32);
// Extended button bits mirror the wire `input::gamepad` constants.
assert!(PUNKTFUNK_GAMEPAD_BTN_PADDLE1 == g::BTN_PADDLE1);
assert!(PUNKTFUNK_GAMEPAD_BTN_PADDLE2 == g::BTN_PADDLE2);
+60 -11
View File
@@ -140,8 +140,8 @@ impl CompositorPref {
/// otherwise the host falls back and reports the real choice in `Welcome`. The wire form is a single
/// byte (`0 = Auto`, `1 = Xbox360`, `2 = DualSense`, `3 = XboxOne`, `4 = DualShock4`,
/// `5 = SteamController`, `6 = SteamDeck`, `7 = DualSenseEdge`, `8 = SwitchPro`,
/// `9 = SteamController2`, `10 = SteamController2Puck`), appended to `Hello`/`Welcome` — older
/// peers simply omit/ignore it (an unknown byte degrades to `Auto`).
/// `9 = SteamController2`, `10 = SteamController2Puck`, `11 = XboxElite`), appended to
/// `Hello`/`Welcome` — older peers simply omit/ignore it (an unknown byte degrades to `Auto`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GamepadPref {
/// Let the host pick (its `PUNKTFUNK_GAMEPAD` env var, else X-Box 360).
@@ -151,9 +151,11 @@ pub enum GamepadPref {
Xbox360,
/// UHID DualSense (kernel `hid-playstation`) — adaptive triggers, lightbar, touchpad, motion.
DualSense,
/// uinput X-Box One / Series pad the X-Box 360 backend with the One/Series USB identity
/// (VID/PID/name), so games show One/Series glyphs. XInput-identical otherwise (impulse-trigger
/// rumble is unreachable through any virtual pad, so there's no game-visible gain over `Xbox360`).
/// X-Box One / Series pad. On Linux, the X-Box 360 uinput backend with the One/Series USB
/// identity (VID/PID/name), so games show One/Series glyphs XInput-identical otherwise. On
/// Windows it is a distinct HID identity (`045E:02FD`, Bluetooth Xbox One S) through the UMDF
/// minidriver; it used to fold to `Xbox360` there, because the only Windows Xbox backend was
/// the XUSB companion, which presents one fixed 360 identity and cannot vary it.
XboxOne,
/// UHID DualShock 4 (kernel `hid-playstation`, ≥ 6.2) — lightbar, touchpad, motion, rumble. Like
/// `DualSense` minus adaptive triggers / player LEDs / mute. Needs Linux UHID on the host.
@@ -186,6 +188,21 @@ pub enum GamepadPref {
/// native seven-interface Puck topology (CDC pair, four controller slots, management HID)
/// rather than relabelling its reports as a wired `1302`.
SteamController2Puck,
/// Xbox Elite Wireless Controller Series 2 (Microsoft `045E:0B22`, Bluetooth) — a Windows-only
/// HID identity through the UMDF minidriver, so glyphs and the Device Manager name read Elite.
///
/// ⚠️ **Glyphs and identity only, today.** The four paddles (`BTN_PADDLE1..4`) still fold or
/// drop exactly as on the other Xbox classes; the Elite is merely the first Xbox identity that
/// *could* carry them natively. Wiring them up is blocked on a measurement, not on effort —
/// once Windows promotes the pad, `xinputhid` claims its HID collection exclusively, so extra
/// buttons declared in the report descriptor may reach no consumer at all
/// (`design/xbox-pad-windows-handoff.md` §3.6). Do not advertise paddle support off this
/// variant until that is measured; `DualSenseEdge` stays the only virtual pad with native
/// back-button slots.
///
/// Folds to `Xbox360` everywhere but Windows: there is no Linux uinput Elite identity
/// (`PadIdentity` has 360 and One S only).
XboxElite,
}
impl GamepadPref {
@@ -211,7 +228,8 @@ impl GamepadPref {
pub const fn has_motion(self) -> bool {
match self {
GamepadPref::Auto => true, // unknown; assume it can, see above
GamepadPref::Xbox360 | GamepadPref::XboxOne => false,
// No Xbox pad has a gyro in its HID contract — Elite Series 2 included.
GamepadPref::Xbox360 | GamepadPref::XboxOne | GamepadPref::XboxElite => false,
GamepadPref::DualSense
| GamepadPref::DualShock4
| GamepadPref::DualSenseEdge
@@ -225,7 +243,7 @@ impl GamepadPref {
/// Wire byte. `0 = Auto`, `1 = Xbox360`, `2 = DualSense`, `3 = XboxOne`, `4 = DualShock4`,
/// `5 = SteamController`, `6 = SteamDeck`, `7 = DualSenseEdge`, `8 = SwitchPro`,
/// `9 = SteamController2`, `10 = SteamController2Puck`.
/// `9 = SteamController2`, `10 = SteamController2Puck`, `11 = XboxElite`.
pub const fn to_u8(self) -> u8 {
match self {
GamepadPref::Auto => 0,
@@ -239,6 +257,7 @@ impl GamepadPref {
GamepadPref::SwitchPro => 8,
GamepadPref::SteamController2 => 9,
GamepadPref::SteamController2Puck => 10,
GamepadPref::XboxElite => 11,
}
}
@@ -256,6 +275,7 @@ impl GamepadPref {
8 => GamepadPref::SwitchPro,
9 => GamepadPref::SteamController2,
10 => GamepadPref::SteamController2Puck,
11 => GamepadPref::XboxElite,
_ => GamepadPref::Auto,
}
}
@@ -270,6 +290,10 @@ impl GamepadPref {
"xboxone" | "xbox-one" | "xone" | "xbox1" | "series" | "xboxseries" => {
GamepadPref::XboxOne
}
// "elite" is unambiguous here — the DualSense Edge answers to "edge", never "elite".
"xboxelite" | "xbox-elite" | "elite" | "xboxelite2" | "elite2" => {
GamepadPref::XboxElite
}
"dualshock4" | "dualshock" | "ds4" | "ps4" => GamepadPref::DualShock4,
"steamdeck" | "steam-deck" | "deck" => GamepadPref::SteamDeck,
"steamcontroller" | "steam-controller" | "steamcon" => GamepadPref::SteamController,
@@ -289,7 +313,7 @@ impl GamepadPref {
/// Canonical lowercase identifier (`"auto"`, `"xbox360"`, `"dualsense"`, `"xboxone"`,
/// `"dualshock4"`, `"steamcontroller"`, `"steamdeck"`, `"dualsenseedge"`, `"switchpro"`,
/// `"steamcontroller2"`, `"steamcontroller2puck"`).
/// `"steamcontroller2"`, `"steamcontroller2puck"`, `"xboxelite"`).
pub fn as_str(self) -> &'static str {
match self {
GamepadPref::Auto => "auto",
@@ -303,6 +327,7 @@ impl GamepadPref {
GamepadPref::SwitchPro => "switchpro",
GamepadPref::SteamController2 => "steamcontroller2",
GamepadPref::SteamController2Puck => "steamcontroller2puck",
GamepadPref::XboxElite => "xboxelite",
}
}
}
@@ -833,7 +858,11 @@ mod tests {
/// into a host that drops every one.
#[test]
fn only_the_xbox_classes_lack_a_motion_plane() {
for p in [GamepadPref::Xbox360, GamepadPref::XboxOne] {
for p in [
GamepadPref::Xbox360,
GamepadPref::XboxOne,
GamepadPref::XboxElite,
] {
assert!(
!p.has_motion(),
"{} should have no motion plane",
@@ -910,11 +939,12 @@ mod tests {
GamepadPref::SwitchPro,
GamepadPref::SteamController2,
GamepadPref::SteamController2Puck,
GamepadPref::XboxElite,
] {
assert_eq!(GamepadPref::from_u8(p.to_u8()), p);
assert_eq!(GamepadPref::from_name(p.as_str()), Some(p));
}
// Every wire byte 0..=10 is assigned, distinct, and pinned (forward-compat with peers
// Every wire byte 0..=11 is assigned, distinct, and pinned (forward-compat with peers
// that only know a prefix of the range).
for (v, p) in [
(0, GamepadPref::Auto),
@@ -928,12 +958,13 @@ mod tests {
(8, GamepadPref::SwitchPro),
(9, GamepadPref::SteamController2),
(10, GamepadPref::SteamController2Puck),
(11, GamepadPref::XboxElite),
] {
assert_eq!(p.to_u8(), v);
assert_eq!(GamepadPref::from_u8(v), p);
}
// The next unassigned byte degrades to Auto today; assigning it later must update this.
assert_eq!(GamepadPref::from_u8(11), GamepadPref::Auto);
assert_eq!(GamepadPref::from_u8(12), GamepadPref::Auto);
// Aliases + unknowns.
assert_eq!(GamepadPref::from_name("PS5"), Some(GamepadPref::DualSense));
assert_eq!(GamepadPref::from_name("x360"), Some(GamepadPref::Xbox360));
@@ -964,6 +995,24 @@ mod tests {
Some(GamepadPref::XboxOne)
);
assert_eq!(GamepadPref::from_name("series"), Some(GamepadPref::XboxOne));
// The Elite's aliases, and the one that could plausibly have been stolen: "edge" is the
// DualSense Edge and must stay so — the two are different pads on different vendors.
assert_eq!(
GamepadPref::from_name("Elite"),
Some(GamepadPref::XboxElite)
);
assert_eq!(
GamepadPref::from_name("xbox-elite"),
Some(GamepadPref::XboxElite)
);
assert_eq!(
GamepadPref::from_name("elite2"),
Some(GamepadPref::XboxElite)
);
assert_eq!(
GamepadPref::from_name("edge"),
Some(GamepadPref::DualSenseEdge)
);
assert_eq!(GamepadPref::from_name("nope"), None);
// Unknown wire byte degrades to Auto (forward-compatible).
assert_eq!(GamepadPref::from_u8(200), GamepadPref::Auto);
+22
View File
@@ -368,6 +368,14 @@ pub fn dualsense_windows_test(args: &[String]) -> Result<()> {
let xbox = args.iter().any(|a| a == "--xbox");
// `--xboxhid` drives the HID Xbox backend (device-type 4) instead of `--xbox`'s XUSB companion.
let xboxhid = args.iter().any(|a| a == "--xboxhid");
// The other two HID Xbox identities (device-types 5 and 6). Same backend, same report
// descriptor — only VID/PID, product string and hardware id differ — so these legs exist for
// exactly one question each: does Windows PROMOTE that PID the way it promotes `0B13`?
// `02FD` in particular has no stage-2 `HID\…&IG_00` line in Microsoft's `xinputhid.inf`, so
// it is the one worth watching. Check for the `IG_00` token, the XUSB interface, an XInput
// slot and rumble, exactly as the `--xboxhid` run did.
let xboxones = args.iter().any(|a| a == "--xboxones");
let xboxelite = args.iter().any(|a| a == "--xboxelite");
// `--edge` drives the DualSense Edge backend (device_type 2) and additionally holds
// the R4/L4 paddles on the pressed beats, so a HID read shows the Edge bits in
// report byte 10 (0x80|0x40) next to Cross. `--deck` drives the Steam Deck backend
@@ -481,6 +489,20 @@ pub fn dualsense_windows_test(args: &[String]) -> Result<()> {
crate::inject::xbox_windows::XboxWindowsManager::new(),
"Xbox Wireless Controller (HID)"
);
} else if xboxones {
drive!(
crate::inject::xbox_windows::XboxWindowsManager::with_backend(
crate::inject::xbox_windows::XboxWinProto::one_s()
),
"Xbox One S Controller (HID, 045E:02FD)"
);
} else if xboxelite {
drive!(
crate::inject::xbox_windows::XboxWindowsManager::with_backend(
crate::inject::xbox_windows::XboxWinProto::elite()
),
"Xbox Elite Wireless Controller Series 2 (HID, 045E:0B22)"
);
} else if ds4 {
drive!(
crate::inject::dualshock4_windows::DualShock4WindowsManager::new(),
+65 -9
View File
@@ -41,7 +41,7 @@ pub(super) fn resolve_pad_kind(kind: GamepadPref) -> GamepadPref {
cfg!(target_os = "linux"),
cfg!(target_os = "windows"),
);
degrade_steam_on_conflict(degrade_if_no_uhid(chosen))
degrade_xbox_identity(degrade_steam_on_conflict(degrade_if_no_uhid(chosen)))
}
/// Pure selection of the session's virtual-gamepad backend: the client's explicit `pref` wins,
@@ -49,9 +49,18 @@ pub(super) fn resolve_pad_kind(kind: GamepadPref) -> GamepadPref {
///
/// `linux`/`windows` flag the host platform. DualSense and DualShock 4 each have both a Linux (UHID
/// hid-playstation) and a Windows (UMDF minidriver) backend; on any other platform such a wish degrades
/// to X-Box 360 (never an error: a session without rich pads still streams). X-Box One/Series is a
/// distinct uinput *identity* on Linux, but XInput-identical to the 360 pad on Windows (the XUSB
/// companion presents a 360 identity), so it degrades to `Xbox360` there.
/// to X-Box 360 (never an error: a session without rich pads still streams).
///
/// The X-Box identities are now distinct on BOTH platforms: a uinput identity on Linux (360 /
/// One S), and a UMDF HID identity on Windows (360 → `045E:0B13`, One → `045E:02FD`, Elite →
/// `045E:0B22`). The Windows fold of One/Series into the 360 pad is gone with the reason for it —
/// it existed because the only Windows X-Box backend was the XUSB companion, which presents one
/// fixed 360 identity and cannot vary it. The Elite has no Linux identity (`PadIdentity` stops at
/// One S), so it folds there.
///
/// ⚠️ **This is compile-time only.** `PUNKTFUNK_XBOX_BACKEND=xusb` puts Windows back on the
/// companion at RUNTIME, which un-varies the identity again — that is [`degrade_xbox_identity`]'s
/// job, not this function's.
fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool) -> GamepadPref {
let want = match pref {
GamepadPref::Auto => env
@@ -63,9 +72,12 @@ fn pick_gamepad(pref: GamepadPref, env: Option<&str>, linux: bool, windows: bool
// DualSense / DualShock 4: Linux UHID hid-playstation, or the Windows UMDF minidriver backend.
GamepadPref::DualSense if linux || windows => GamepadPref::DualSense,
GamepadPref::DualShock4 if linux || windows => GamepadPref::DualShock4,
// One/Series: a real, distinct uinput identity on Linux; folded into the 360 backend on
// Windows (XInput can't tell them apart anyway).
GamepadPref::XboxOne if linux => GamepadPref::XboxOne,
// One/Series: a real, distinct uinput identity on Linux, and — since the HID X-Box backend
// became the default — a distinct UMDF HID identity (`045E:02FD`) on Windows too.
GamepadPref::XboxOne if linux || windows => GamepadPref::XboxOne,
// Elite Series 2: Windows-only (UMDF device-type 6, `045E:0B22`). There is no Linux uinput
// Elite identity to fold onto, so it takes the `_` arm and lands on the 360 pad there.
GamepadPref::XboxElite if windows => GamepadPref::XboxElite,
// Steam Deck / classic Steam Controller: Linux UHID hid-steam (Windows Steam devices
// are the N4 spike).
GamepadPref::SteamDeck if linux => GamepadPref::SteamDeck,
@@ -221,6 +233,36 @@ fn degrade_steam_on_conflict(chosen: GamepadPref) -> GamepadPref {
chosen
}
/// Runtime degrade for the two non-default Windows X-Box identities (One S / Elite Series 2): with
/// `PUNKTFUNK_XBOX_BACKEND=xusb` the session runs the XUSB companion, which presents ONE fixed
/// X-Box 360 identity and has no way to vary VID/PID — so the pad a player gets is a 360 pad no
/// matter what was asked for. Fold here so the `Welcome` echo says so.
///
/// This is a runtime check and [`pick_gamepad`] is a compile-time one, which is exactly the split
/// [`degrade_if_no_uhid`] already draws. Without it, asking for an Elite under the escape hatch
/// resolves to `xboxelite`, echoes `xboxelite`, and builds a 360 pad — the class of silent lie
/// `pad_motion_reaches` and the fold-logging in [`resolve_gamepad`] exist to prevent.
///
/// A no-op on every non-Windows host: `XboxElite` never survives `pick_gamepad` there, and
/// `XboxOne` is a genuine uinput identity on Linux.
#[cfg(target_os = "windows")]
fn degrade_xbox_identity(chosen: GamepadPref) -> GamepadPref {
if matches!(chosen, GamepadPref::XboxOne | GamepadPref::XboxElite) && !windows_xbox_hid() {
tracing::warn!(
wanted = chosen.as_str(),
"PUNKTFUNK_XBOX_BACKEND=xusb selects the XUSB companion, which has one fixed X-Box 360 \
identity falling back to the 360 pad"
);
return GamepadPref::Xbox360;
}
chosen
}
#[cfg(not(target_os = "windows"))]
fn degrade_xbox_identity(chosen: GamepadPref) -> GamepadPref {
chosen
}
/// Whether an Xbox-family pad should be built as a real **HID** device
/// ([`crate::inject::xbox_windows`]) instead of the **XUSB** companion
/// ([`crate::inject::gamepad`]). Windows only. **HID is the default**; set
@@ -278,6 +320,9 @@ pub(super) fn resolve_gamepad(pref: GamepadPref) -> GamepadPref {
// Steam controller — its own Steam Input would then manage two Decks (confirmed conflict-prone on
// a Deck-as-host). `PUNKTFUNK_STEAM_FORCE=1` overrides.
let chosen = degrade_steam_on_conflict(chosen);
// The XUSB escape hatch can only present a 360 identity, so the One S / Elite wishes fold when
// `PUNKTFUNK_XBOX_BACKEND=xusb` is set.
let chosen = degrade_xbox_identity(chosen);
match pref {
GamepadPref::Auto => {
// The operator's env knob deserves a diagnostic when it didn't drive the
@@ -374,10 +419,21 @@ mod tests {
assert_eq!(pick_gamepad(Auto, Some("ps4"), true, false), DualShock4);
assert_eq!(pick_gamepad(DualShock4, None, false, true), DualShock4);
assert_eq!(pick_gamepad(DualShock4, None, false, false), Xbox360);
// X-Box One: a distinct uinput identity on Linux, folded into the 360 pad on Windows.
// X-Box One: a distinct uinput identity on Linux AND a distinct UMDF HID identity
// (`045E:02FD`) on Windows. The old Windows fold to Xbox360 is deliberately gone — it
// existed only because the XUSB companion has one fixed 360 identity, and the HID backend
// is the default now. `degrade_xbox_identity` puts the fold back when the escape hatch
// `PUNKTFUNK_XBOX_BACKEND=xusb` is set; that is a runtime check this pure one can't make.
assert_eq!(pick_gamepad(XboxOne, None, true, false), XboxOne);
assert_eq!(pick_gamepad(Auto, Some("series"), true, false), XboxOne);
assert_eq!(pick_gamepad(XboxOne, None, false, true), Xbox360);
assert_eq!(pick_gamepad(XboxOne, None, false, true), XboxOne);
assert_eq!(pick_gamepad(XboxOne, None, false, false), Xbox360);
// X-Box Elite Series 2: Windows-only (UMDF device-type 6). No Linux uinput Elite identity
// exists, so it folds to the 360 pad there rather than pretending.
assert_eq!(pick_gamepad(XboxElite, None, false, true), XboxElite);
assert_eq!(pick_gamepad(Auto, Some("elite"), false, true), XboxElite);
assert_eq!(pick_gamepad(XboxElite, None, true, false), Xbox360);
assert_eq!(pick_gamepad(XboxElite, None, false, false), Xbox360);
// Steam Deck: native on Linux (UHID/usbip/gadget) AND Windows (UMDF device-type 3,
// Steam-Input-promoted via MI_02 — gamepad-new-types N4); Xbox360 elsewhere.
+54 -10
View File
@@ -126,9 +126,19 @@ struct Pads {
/// The HID-visible Xbox pad ([`crate::inject::xbox_windows`]) — used INSTEAD of `xbox360`'s
/// XUSB companion when [`super::gamepad::windows_xbox_hid`] says so. Never both at once: two
/// devices for one wire pad is the "the game sees two controllers" bug.
///
/// Three managers because the HID backend now has three IDENTITIES (Xbox Wireless `045E:0B13`,
/// Xbox One S `045E:02FD`, Elite Series 2 `045E:0B22`) and a manager is bound to one at
/// construction. They are otherwise the same backend — same codec, same report descriptor,
/// same rumble plane — so the split is purely so a mixed session can present, say, a Series
/// pad on slot 0 and an Elite on slot 1.
#[cfg(target_os = "windows")]
xbox_hid: Option<crate::inject::xbox_windows::XboxWindowsManager>,
#[cfg(target_os = "windows")]
xbox_one_hid: Option<crate::inject::xbox_windows::XboxWindowsManager>,
#[cfg(target_os = "windows")]
xbox_elite_hid: Option<crate::inject::xbox_windows::XboxWindowsManager>,
#[cfg(target_os = "windows")]
dualsense_edge_win: Option<crate::inject::dualsense_edge_windows::DualSenseEdgeWindowsManager>,
#[cfg(target_os = "windows")]
dualshock4_win: Option<crate::inject::dualshock4_windows::DualShock4WindowsManager>,
@@ -172,6 +182,10 @@ impl Pads {
#[cfg(target_os = "windows")]
xbox_hid: None,
#[cfg(target_os = "windows")]
xbox_one_hid: None,
#[cfg(target_os = "windows")]
xbox_elite_hid: None,
#[cfg(target_os = "windows")]
dualsense_edge_win: None,
#[cfg(target_os = "windows")]
dualshock4_win: None,
@@ -298,17 +312,38 @@ impl Pads {
.steamdeck_win
.get_or_insert_with(crate::inject::steam_deck_windows::SteamDeckWindowsManager::new)
.handle(ev),
// The Xbox pad, as a real HID device rather than the XUSB companion. This is now the
// The Xbox pads, as real HID devices rather than the XUSB companion. This is now the
// DEFAULT (see `windows_xbox_hid`; `PUNKTFUNK_XBOX_BACKEND=xusb` reverts it). It is no
// longer a trade: with the `xinputhid` bus filter the INF attaches, the HID pad keeps
// classic XInput AND gains everything XUSB never had — Steam, SDL, RawInput,
// DirectInput, `joy.cpl`, WGI — plus rumble, which the XUSB path could not source.
//
// Three arms, one per identity. The `windows_xbox_hid()` guard stays on each: with the
// escape hatch set, `degrade_xbox_identity` has already folded One/Elite to Xbox360, so
// only Xbox360 can reach here and it must fall through to the XUSB companion below.
#[cfg(target_os = "windows")]
GamepadPref::Xbox360 | GamepadPref::XboxOne if super::gamepad::windows_xbox_hid() => {
self.xbox_hid
.get_or_insert_with(crate::inject::xbox_windows::XboxWindowsManager::new)
.handle(ev)
}
GamepadPref::Xbox360 if super::gamepad::windows_xbox_hid() => self
.xbox_hid
.get_or_insert_with(crate::inject::xbox_windows::XboxWindowsManager::new)
.handle(ev),
#[cfg(target_os = "windows")]
GamepadPref::XboxOne if super::gamepad::windows_xbox_hid() => self
.xbox_one_hid
.get_or_insert_with(|| {
crate::inject::xbox_windows::XboxWindowsManager::with_backend(
crate::inject::xbox_windows::XboxWinProto::one_s(),
)
})
.handle(ev),
#[cfg(target_os = "windows")]
GamepadPref::XboxElite if super::gamepad::windows_xbox_hid() => self
.xbox_elite_hid
.get_or_insert_with(|| {
crate::inject::xbox_windows::XboxWindowsManager::with_backend(
crate::inject::xbox_windows::XboxWinProto::elite(),
)
})
.handle(ev),
_ => self
.xbox360
.get_or_insert_with(crate::inject::gamepad::GamepadManager::new)
@@ -431,7 +466,8 @@ impl Pads {
/// player LEDs / adaptive triggers) for the UHID/UMDF pads. The `&mut` closure re-borrows
/// satisfy `FnMut` for each backend.
///
/// Only the Windows HID Xbox backend (`xbox_hid`) can ever report non-zero trigger levels — no
/// Only the Windows HID Xbox backends (`xbox_hid` and its two identity siblings) can ever
/// report non-zero trigger levels — no
/// other backend's source packet has a field for them (see `PadFeedback::rumble`), so they pass
/// zeros and the v3 datagram they produce is a v2 datagram with a zero tail.
fn pump(
@@ -474,9 +510,17 @@ impl Pads {
}
#[cfg(target_os = "windows")]
{
if let Some(m) = &mut self.xbox_hid {
// Rumble only — an Xbox pad has no rich-feedback plane (no lightbar / adaptive
// triggers), same as its XUSB sibling above.
// All three HID Xbox identities. Rumble only — an Xbox pad has no rich-feedback plane
// (no lightbar / adaptive triggers), same as its XUSB sibling above. Missing one of
// these is silent: the pad works and simply never rumbles.
for m in [
&mut self.xbox_hid,
&mut self.xbox_one_hid,
&mut self.xbox_elite_hid,
]
.into_iter()
.flatten()
{
m.pump(&mut rumble, &mut hidout);
}
if let Some(m) = &mut self.dualsense_win {
+6
View File
@@ -236,6 +236,12 @@
// ordinary wired/BLE SC2 capture remains `STEAMCONTROLLER2`.
#define PUNKTFUNK_GAMEPAD_STEAMCONTROLLER2_PUCK 10
// Xbox Elite Wireless Controller Series 2 (`045E:0B22`, Bluetooth): a Windows-only HID identity
// through the UMDF minidriver, so glyphs and the device name read Elite. Folds to X-Box 360
// elsewhere. ⚠️ Identity only — the four paddles still fold/drop exactly as on the other X-Box
// classes (`DUALSENSEEDGE` is the pad with native back-button slots).
#define PUNKTFUNK_GAMEPAD_XBOXELITE 11
// Extended `InputEvent` gamepad button bits for embedders building raw events: the four back grips
// (Steam L4/L5/R4/R5 ≙ Xbox-Elite P1P4) + the misc/capture button, in Moonlight's
// `buttonFlags2 << 16` namespace. Mirror `input::gamepad::BTN_PADDLE1..4` / `BTN_MISC1`.
@@ -1,7 +1,8 @@
;/*++
; punktfunk virtual gamepads — UMDF2 HID minidriver INF.
; One package, four hardware ids: DualSense, DualShock 4, DualSense Edge, Steam Deck — which is why
; the package is called pf_gamepad and not pf_dualsense (it never was one identity).
; One package, seven hardware ids: DualSense, DualShock 4, DualSense Edge, Steam Deck, and three
; Xbox pads (Wireless / One S / Elite Series 2) — which is why the package is called pf_gamepad and
; not pf_dualsense (it never was one identity).
;
; ⚠️ The HARDWARE IDS below deliberately keep their old names (`pf_dualsense`, `pf_dualshock4`,
; `pf_dualsenseedge`, `pf_steamdeck`). They are the binding contract with every devnode the host
@@ -34,10 +35,12 @@ pf_gamepad.dll=1
[pf.NT$ARCH$.10.0...22000]
; Hardware ids: `root\pf_dualsense` for a root-enumerated devnode (devgen/devcon tests); `pf_dualsense`
; for the host's SwDeviceCreate'd DualSense (the `root\` prefix is reserved for root enumeration, so
; SwDeviceCreate rejects it with E_INVALIDARG); `pf_dualshock4` / `pf_dualsenseedge` / `pf_steamdeck`
; for the host's other virtual pads — ONE driver binds all of them (every model line below installs
; the same `pfGamepad` section) and serves the matching HID identity per the device_type byte the
; host stamps into shared memory.
; SwDeviceCreate rejects it with E_INVALIDARG); `pf_dualshock4` / `pf_dualsenseedge` /
; `pf_steamdeck` / `pf_xboxwireless` / `pf_xboxones` / `pf_xboxelite` for the host's other virtual
; pads — ONE driver binds all of them and serves the matching HID identity per the device_type byte
; the host stamps into shared memory. TWO install sections, though: the PlayStation/Deck ids share
; `pfGamepad`, and the three Xbox ids install `pfGamepadXbox`, which additionally attaches the
; `xinputhid` bus filter (see the ⚠️ below the Deck line).
;
; Each id carries its OWN description: Device Manager reads this string, and a single shared
; "Virtual DualSense" made an emulated DualShock 4 look like the controller-type setting had been
@@ -47,11 +50,19 @@ pf_gamepad.dll=1
%DeviceDescDS4%=pfGamepad, pf_dualshock4
%DeviceDescEdge%=pfGamepad, pf_dualsenseedge
%DeviceDescDeck%=pfGamepad, pf_steamdeck
; ⚠️ The Xbox line installs its OWN section, `pfGamepadXbox`, and must keep doing so. Every other
; identity shares `pfGamepad`; the Xbox one additionally attaches the `xinputhid` bus filter, and
; ⚠️ The Xbox lines install their OWN section, `pfGamepadXbox`, and must keep doing so. Every other
; identity shares `pfGamepad`; the Xbox ones additionally attach the `xinputhid` bus filter, and
; putting that on a DualSense / DualShock 4 / Edge / Steam Deck would hand a PlayStation pad to
; Microsoft's Xbox translator. The two sections are otherwise identical — keep them in step.
; (`only_the_xbox_identity_installs_the_xinputhid_section`, in pf-inject, asserts both directions.)
;
; The three Xbox identities differ ONLY in hardware id, Device Manager description and the VID/PID
; + product string the driver serves off the resulting device_type — they share one report
; descriptor and one install section, because in HID terms they are the same pad. See the
; `XBOX_RDESC` header in src/lib.rs for why that sharing is deliberate.
%DeviceDescXbox%=pfGamepadXbox, root\pf_xboxwireless, pf_xboxwireless
%DeviceDescXboxOneS%=pfGamepadXbox, root\pf_xboxones, pf_xboxones
%DeviceDescXboxElite%=pfGamepadXbox, root\pf_xboxelite, pf_xboxelite
[pfGamepad.NT]
CopyFiles=UMDriverCopy
@@ -177,3 +188,9 @@ DeviceDescDS4 ="Punktfunk Virtual DualShock 4"
DeviceDescEdge ="Punktfunk Virtual DualSense Edge"
DeviceDescDeck ="Punktfunk Virtual Steam Deck Controller"
DeviceDescXbox ="Punktfunk Virtual Xbox Wireless Controller"
; ⚠️ This one deliberately does NOT match the product string the driver serves for device_type 5.
; A real Xbox One S pad reports "Xbox Wireless Controller" over Bluetooth, exactly like the Series
; X|S pad above — the PID is the only thing that separates them on the wire. Device Manager,
; however, has to let a human tell our two virtual pads apart, and this string is ours to choose.
DeviceDescXboxOneS ="Punktfunk Virtual Xbox One S Controller"
DeviceDescXboxElite="Punktfunk Virtual Xbox Elite Wireless Controller Series 2"
+93 -23
View File
@@ -2,7 +2,9 @@
//
// A Rust port of the WDK `vhidmini2` UMDF2 sample, reconfigured to present a Sony DualSense
// (VID 054C / PID 0CE6), DualShock 4 (device_type=1), DualSense Edge (device_type=2), Steam Deck
// (device_type=3) or Xbox Wireless Controller (device_type=4, VID 045E / PID 0B13) using the
// (device_type=3), Xbox Wireless Controller (device_type=4, VID 045E / PID 0B13), Xbox One S
// (device_type=5, 045E / 02FD) or Xbox Elite Wireless Controller Series 2 (device_type=6,
// 045E / 0B22) using the
// report descriptors + feature blobs punktfunk already ships in `inject/`. Games see a genuine
// HID PS controller; the host streams input in / reads output (rumble/lightbar/triggers) back.
//
@@ -73,7 +75,7 @@ const DS_EDGE_PID: u16 = 0x0DF2;
const DECK_VID: u16 = 0x28DE;
const DECK_PID: u16 = 0x1205;
// ---- Xbox Wireless Controller identity (device_type=4) ----
// ---- Xbox identities (device_type = 4 Wireless / 5 One S / 6 Elite Series 2) ----
//
// WHY THIS EXISTS (field 2026-08-09, `punktfunk-field-windows-pad-dead-0260`): the OTHER Windows
// Xbox backend — `pf-xusb` — registers ONLY `GUID_DEVINTERFACE_XUSB` and has no HID collection at
@@ -89,17 +91,37 @@ const DECK_PID: u16 = 0x1205;
// never existed and inbox promotion has nothing to match. The Xbox pads that genuinely ARE HID are
// the Bluetooth ones, which Windows binds through HIDCLASS.
const XBOX_VID: u16 = 0x045E;
/// Xbox Wireless Controller (Series X|S), Bluetooth. Chosen over the Xbox One S BT id `0x02FD`
/// because the host's OS floor is Windows 11 22H2, where this is the current-generation identity
/// (so glyphs read "Xbox Series") and SDL's mapping database covers it.
/// Xbox Wireless Controller (Series X|S), Bluetooth — `device_type = 4`, the default Xbox identity.
/// Chosen over the Xbox One S BT id `0x02FD` because the host's OS floor is Windows 11 22H2, where
/// this is the current-generation identity (so glyphs read "Xbox Series") and SDL's mapping
/// database covers it.
///
/// ⚠️ **If on-glass shows Windows does not promote this to an Xbox-profile pad, try `0x02FD`
/// (Xbox One S BT) — it has the broadest inbox coverage.** Deliberately one named constant so that
/// experiment is a one-line change.
/// ⭐ It is also the PID Microsoft's own `xinputhid.inf` allow-lists **twice** (once as a
/// `BTHLEDevice` stage-1 id, once as a plain `HID\…&IG_00` stage-2 id) — measured off `.173`,
/// 2026-08-09. That is not what promotes OUR pad (a software devnode matches no allow-list entry;
/// `pfGamepadXbox`'s `AddReg` writes what the matching sections would have written), but it is why
/// this stays the default of the three.
const XBOX_PID: u16 = 0x0B13;
/// Alternate identity for the promotion experiment above — Xbox One S controller over Bluetooth.
#[allow(dead_code)]
/// Xbox One S controller over Bluetooth — `device_type = 5`.
///
/// ⚠️ **`02FD` appears in `xinputhid.inf` only as a `BTHENUM` (classic-BT bus) id — it has NO
/// stage-2 `HID\…&IG_00` model line.** That killed it as a "try another PID" lever for the
/// promotion work (handoff §4 B1). It does not block it as an IDENTITY, because our promotion
/// comes from the INF's own `AddReg` rather than from matching Microsoft's list — but if a future
/// Windows servicing update makes promotion depend on the allow-list again, this identity is the
/// one that loses it first. Worth re-measuring on glass before recommending it to anyone.
const XBOX_PID_ONE_S: u16 = 0x02FD;
/// Xbox Elite Wireless Controller Series 2 — `device_type = 6`. This is the pad
/// `tools/hid-descriptor-dump` captured on `.173` (`BTHLE\DEV_686CE647F191`, `REV_0521`), so it is
/// the one identity here whose real hardware we have measured directly.
const XBOX_PID_ELITE2: u16 = 0x0B22;
/// bcdDevice for every Xbox identity.
///
/// Deliberately ONE value rather than per-identity: the real Elite reports `REV_0521` (measured on
/// `.173`) but `create_swdevice` synthesizes the devnode's USB ids with a hardcoded `&REV_0100`
/// regardless, and SDL folds the version into its joystick GUID — so a version that disagrees with
/// the devnode buys nothing and risks missing a stock mapping. Revisit only with a measurement
/// that shows a consumer keying on it.
const XBOX_VER: u16 = 0x0407;
// Sony DualSense USB HID report descriptor (273 bytes), verbatim from inputtino (== inject/dualsense.rs).
@@ -271,7 +293,23 @@ static DECK_RDESC: [u8; 38] = [
0x08, 0x95, 0x40, 0xb1, 0x02, 0xc0,
];
// ---- Xbox Wireless Controller assets (served when the host stamps device_type=4) ----
// ---- Xbox assets (served when the host stamps device_type = 4, 5 or 6) ----
//
// ⭐⭐ **ONE DESCRIPTOR SERVES ALL THREE XBOX IDENTITIES, DELIBERATELY.** Xbox Wireless (4),
// Xbox One S (5) and Xbox Elite Series 2 (6) differ ONLY in VID/PID, product string and INF model
// line — in HID terms they are the same pad: same two 16-bit stick pairs, same trigger pair, same
// hat, same 15 buttons, same rumble output report. A report descriptor is the report SHAPE, not
// the identity; the identity is what SDL/Steam/Windows key their stock mappings off, and that
// travels in `hid_attrs`.
//
// This is load-bearing, not laziness. The ⚠️ block below is the record of what ONE hand-written
// descriptor has already cost: three separate bugs (no Feature report ⇒ the sealed channel never
// opened and the pad served neutral forever; no OUTPUT item ⇒ no rumble of any kind and dead
// host-side code; a layout that provably disagrees with the captured hardware). Two more
// hand-written descriptors would multiply that debt by three for no measured gain, and each would
// need its own capture, its own `wReportLength`, its own `xbox_proto` layout tests and its own
// on-glass verification. When a Linux-hidraw capture settles the real layout (handoff §3.3), it
// lands here ONCE and all three identities get it.
//
// A standards-clean Game Pad collection matching the Bluetooth Xbox layout: two 16-bit stick pairs,
// two 10-bit triggers on the Simulation page, a null-state hat, and 15 buttons. Report `0x01`,
@@ -474,6 +512,7 @@ static HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0x11, 0x01
static DS4_HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0xFB, 0x01];
static EDGE_HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0x85, 0x01];
static DECK_HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0x26, 0x00]; // 38 bytes
// Serves device_type 4, 5 AND 6 — one descriptor, three identities (see the XBOX_RDESC header).
static XBOX_HID_DESC: [u8; 9] = [0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0xDF, 0x00]; // 223 bytes
// Each `wReportLength` above is a SECOND copy of a length that already exists as its descriptor's
@@ -492,13 +531,21 @@ const _: () = assert!(declared_len(&DECK_HID_DESC) == DECK_RDESC.len());
const _: () = assert!(declared_len(&XBOX_HID_DESC) == XBOX_RDESC.len());
// HID_DEVICE_ATTRIBUTES (32 bytes): Size(u32)=32, VendorID, ProductID, VersionNumber, Reserved[11].
// `devtype` selects the identity: PS family (same Sony VID/version) or the N4-spike Deck.
// `devtype` selects the identity: PS family (same Sony VID/version), the N4-spike Deck, or one of
// the three Xbox pads (same Microsoft VID/version — only the PID differs, which is the entire
// difference between them; they share a report descriptor).
//
// ⚠️ THIS is where an Xbox identity is actually decided. Everything else in the Xbox path —
// descriptor, HID descriptor, report length, neutral report — is shared, so a new Xbox model is a
// PID here, a product string in `on_get_string`, an INF model line and nothing else.
fn hid_attrs(devtype: u8) -> [u8; 32] {
let (vid, pid, ver) = match devtype {
1 => (DS_VID, DS4_PID, DS_VER),
2 => (DS_VID, DS_EDGE_PID, DS_VER),
3 => (DECK_VID, DECK_PID, DS_VER),
4 => (XBOX_VID, XBOX_PID, XBOX_VER),
5 => (XBOX_VID, XBOX_PID_ONE_S, XBOX_VER),
6 => (XBOX_VID, XBOX_PID_ELITE2, XBOX_VER),
_ => (DS_VID, DS_PID, DS_VER),
};
let mut a = [0u8; 32];
@@ -518,10 +565,11 @@ fn hid_attrs(devtype: u8) -> [u8; 32] {
/// the caller's buffer rather than truncating — so handing hidclass 64 bytes for a 16-byte report
/// fails every single read and the pad looks dead.
///
/// Returns 64 for every pre-existing identity, so this is provably a no-op for them.
/// Returns 64 for every pre-existing identity, so this is provably a no-op for them. All three
/// Xbox identities share one descriptor, hence one report length.
fn input_report_len(devtype: u8) -> usize {
match devtype {
4 => XBOX_INPUT_REPORT_LEN,
4 | 5 | 6 => XBOX_INPUT_REPORT_LEN,
_ => 64,
}
}
@@ -578,7 +626,8 @@ fn neutral_report(devtype: u8) -> [u8; 64] {
match devtype {
1 => DS4_NEUTRAL_REPORT,
3 => DECK_NEUTRAL_REPORT,
4 => XBOX_NEUTRAL_REPORT,
// Wireless / One S / Elite Series 2 — one report shape, three identities.
4 | 5 | 6 => XBOX_NEUTRAL_REPORT,
_ => NEUTRAL_REPORT, // DualSense and Edge share the report 0x01 shape
}
}
@@ -731,8 +780,8 @@ static RING_PUBLISH: std::sync::Mutex<()> = std::sync::Mutex::new(());
/// this static is per-pad). The handshake/adoption/validation state machine lives in `pf_umdf_util`.
static CHANNEL: ChannelClient = ChannelClient::new();
/// The last observed `device_type` (0 = DualSense, 1 = DualShock 4, 2 = DualSense Edge,
/// 3 = Steam Deck) — the neutral-report shape when the channel detaches, and the fallback identity
/// while unattached.
/// 3 = Steam Deck, 4 = Xbox Wireless, 5 = Xbox One S, 6 = Xbox Elite Series 2) — the
/// neutral-report shape when the channel detaches, and the fallback identity while unattached.
static LAST_DEVTYPE: AtomicU32 = AtomicU32::new(0);
/// The identity resolved from the devnode's PnP hardware ids at `EvtDeviceAdd` ([`devtype_from_hwids`]);
/// `u32::MAX` = not resolved. See [`device_type`] for why this exists.
@@ -748,9 +797,14 @@ static TICK: AtomicU32 = AtomicU32::new(0);
/// can never disagree.
///
/// Order matters: `pf_dualsense` is a prefix of `pf_dualsenseedge`, so the Edge is tested first.
/// (No Xbox token is a prefix of another — `pf_xboxwireless` / `pf_xboxones` / `pf_xboxelite`
/// diverge at the 8th character — but `hwid_devtype_table_matches_the_driver` re-checks that for
/// every pair rather than trusting this note.)
fn devtype_from_hwids(ids: &str) -> Option<u8> {
for (token, devtype) in [
("pf_xboxwireless", 4u8),
("pf_xboxones", 5),
("pf_xboxelite", 6),
("pf_steamdeck", 3),
("pf_dualsenseedge", 2),
("pf_dualshock4", 1),
@@ -1039,15 +1093,17 @@ extern "C" fn evt_io_device_control(
1 => &DS4_HID_DESC,
2 => &EDGE_HID_DESC,
3 => &DECK_HID_DESC,
4 => &XBOX_HID_DESC,
4 | 5 | 6 => &XBOX_HID_DESC,
_ => &HID_DESC,
}),
IOCTL_HID_GET_DEVICE_ATTRIBUTES => request.copy_to_output(&hid_attrs(device_type())),
// The three Xbox identities share ONE report descriptor on purpose — see the XBOX_RDESC
// header. Only `hid_attrs` (VID/PID) and `on_get_string` (product string) tell them apart.
IOCTL_HID_GET_REPORT_DESCRIPTOR => request.copy_to_output(match device_type() {
1 => &DS4_RDESC[..],
2 => &DS_EDGE_RDESC[..],
3 => &DECK_RDESC[..],
4 => &XBOX_RDESC[..],
4 | 5 | 6 => &XBOX_RDESC[..],
_ => &DUALSENSE_RDESC[..],
}),
IOCTL_HID_WRITE_REPORT | IOCTL_UMDF_HID_SET_OUTPUT_REPORT => {
@@ -1309,7 +1365,7 @@ fn on_get_string(request: &Request) -> NTSTATUS {
0 | 0x000e => match devtype {
1 => "Sony Computer Entertainment".into(),
3 => "Valve Software".into(),
4 => "Microsoft".into(),
4 | 5 | 6 => "Microsoft".into(),
_ => "Sony Interactive Entertainment".into(),
},
// Per-pad serials (see `pad_index`): SDL reads this via HidD_GetSerialNumberString and
@@ -1322,15 +1378,29 @@ fn on_get_string(request: &Request) -> NTSTATUS {
2 => format!("35533AD6E7{:02X}", 0x75u8.wrapping_add(pad_index())),
3 => format!("FVPF{:08X}", 0x5046_0000u32 | pad_index() as u32),
// Xbox pads report a Bluetooth MAC-shaped serial; the low octet carries the pad index
// so Steam dedups multiple forwarded pads, exactly like the PS identities above.
// so Steam dedups multiple forwarded pads, exactly like the PS identities above. Each
// Xbox identity gets its OWN base octet (0x10 / 0x30 / 0x50) rather than sharing one:
// a mixed session can present a Wireless pad and an Elite at once, and two identities
// whose serials differ only by pad index are one off-by-one away from colliding — the
// failure being Steam silently treating two live pads as one device.
4 => format!("F4B0FC2A6C{:02X}", 0x10u8.wrapping_add(pad_index())),
5 => format!("F4B0FC2A6C{:02X}", 0x30u8.wrapping_add(pad_index())),
6 => format!("F4B0FC2A6C{:02X}", 0x50u8.wrapping_add(pad_index())),
_ => format!("35533AD6E7{:02X}", 0x74u8.wrapping_add(pad_index())),
},
_ => match devtype {
1 => "Wireless Controller".into(),
2 => "DualSense Edge Wireless Controller".into(),
3 => "Steam Deck Controller".into(),
4 => "Xbox Wireless Controller".into(),
// ⚠️ 4 and 5 share a product string ON PURPOSE — a real Xbox Wireless Controller
// (Series X|S, `0B13`) and a real Xbox One S pad (`02FD`) BOTH report exactly
// "Xbox Wireless Controller" over Bluetooth. The PID is what tells them apart, and
// that is what SDL/Steam/Windows key their stock mappings off. Do not "fix" this by
// inventing a distinguishing string; it would make the One S identity a device that
// has never existed. (The INF's Device Manager descriptions DO differ — that string
// is ours, not the pad's.)
4 | 5 => "Xbox Wireless Controller".into(),
6 => "Xbox Elite Wireless Controller Series 2".into(),
_ => "DualSense Wireless Controller".into(),
},
};
@@ -1343,7 +1413,7 @@ fn on_get_string(request: &Request) -> NTSTATUS {
}
/// The device-type selector: 0 = DualSense, 1 = DualShock 4, 2 = DualSense Edge, 3 = Steam Deck,
/// 4 = Xbox Wireless Controller.
/// 4 = Xbox Wireless Controller, 5 = Xbox One S, 6 = Xbox Elite Wireless Controller Series 2.
/// Read fresh on each enumeration query — cheap.
///
/// ⚠️ **The sealed section cannot answer the enumeration queries.** hidclass asks for