fix(gamepad/windows): Steam-accepted Deck unit serial un-mangles the controller name
Steam validates the Deck unit serial's format before accepting it. Our
"PFDK..." serial was REJECTED ("Invalid or missing unit serial number"), so
Steam substituted a hash identity and mangled the displayed name to
"Steam Deck Controllerggg" on every host tested. An 'F'-leading serial passes,
so switch to "FVPF..." — keeps the PunktFunk marker one slot in, still distinct
from a real Deck's "FVZZ..." for the Linux self-detection in
physical_steam_controller_present(). The name now shows a clean "Steam Deck
Controller" with a serial-derived handle (verified on .173).
Also fix the UMDF driver's 0xAE GET_STRING_ATTRIBUTE handler to echo the
requested attribute id faithfully instead of collapsing board-serial (0x00)
requests to unit-serial (0x01). Steam still logs a benign "Deck Controller PCB
Serial# invalid" for the board serial — it validates that against a
Valve-internal format for ANY value, including an empty one (verified) — but
that line does not mangle the name, change the handle, or block promotion.
Applied to both transports: host inject/proto/steam_proto.rs::deck_serial
(Linux gadget/usbip) and the pf-dualsense UMDF driver (Windows), which mirror
each other's serial format.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -542,12 +542,16 @@ pub fn deck_unit_id(index: u8) -> u32 {
|
|||||||
0x5046_0000 | index as u32
|
0x5046_0000 | index as u32
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Steam-accepted alphanumeric unit serial (a real Deck's is e.g. `"FVZZ4200469B"`; Steam rejects
|
/// A Steam-accepted alphanumeric unit serial (a real Deck's is e.g. `"FVZZ4200469B"`). Steam
|
||||||
/// a too-short/oddly-formatted one as "Invalid or missing unit serial number" and substitutes its
|
/// validates the serial's FORMAT before accepting it: a `"PF"`-leading serial is REJECTED
|
||||||
/// own — benign, but we present a clean 12-char one). Derived from [`deck_unit_id`] so the `0xAE`
|
/// ("Invalid or missing unit serial number …") and Steam then substitutes a hash AND mangles the
|
||||||
/// serial reply and the `0x83` unit-id attrs stay consistent.
|
/// displayed controller name (observed as "Steam Deck Controllerggg" on Windows). An `'F'`-leading
|
||||||
|
/// serial passes, so we keep the PunktFunk marker one slot in (`"FVPF"`) — still distinct from a
|
||||||
|
/// real Deck's `"FVZZ"` for the self-detection below while satisfying Steam's format check.
|
||||||
|
/// Derived from [`deck_unit_id`] so the `0xAE` serial reply and the `0x83` unit-id attrs stay
|
||||||
|
/// consistent. (The Windows UMDF driver mirrors this exact format — see pf-dualsense lib.rs.)
|
||||||
pub fn deck_serial(index: u8) -> String {
|
pub fn deck_serial(index: u8) -> String {
|
||||||
format!("PFDK{:08X}", deck_unit_id(index))
|
format!("FVPF{:08X}", deck_unit_id(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The neutral 64-byte Deck input report (header only, all controls released) — the report the
|
/// The neutral 64-byte Deck input report (header only, all controls released) — the report the
|
||||||
@@ -914,7 +918,7 @@ mod tests {
|
|||||||
fn deck_feature_reply_contract() {
|
fn deck_feature_reply_contract() {
|
||||||
let serial = deck_serial(0);
|
let serial = deck_serial(0);
|
||||||
let unit_id = deck_unit_id(0);
|
let unit_id = deck_unit_id(0);
|
||||||
assert_eq!(serial, "PFDK50460000"); // 12-char alphanumeric, derived from the unit id
|
assert_eq!(serial, "FVPF50460000"); // 12-char alphanumeric, derived from the unit id
|
||||||
assert_eq!(serial.len(), 12);
|
assert_eq!(serial.len(), 12);
|
||||||
|
|
||||||
// 0x83 GET_ATTRIBUTES_VALUES: header + (0x0a, unit_id) at the 3rd attribute slot.
|
// 0x83 GET_ATTRIBUTES_VALUES: header + (0x0a, unit_id) at the 3rd attribute slot.
|
||||||
|
|||||||
@@ -2865,7 +2865,7 @@ fn degrade_if_no_uhid(chosen: GamepadPref) -> GamepadPref {
|
|||||||
/// device (vhci resolves through `vhci_hcd`, NOT `/devices/virtual/`), so a just-ended session's
|
/// device (vhci resolves through `vhci_hcd`, NOT `/devices/virtual/`), so a just-ended session's
|
||||||
/// pad still detaching — or a concurrent session's live one — read as "physical" and degraded
|
/// pad still detaching — or a concurrent session's live one — read as "physical" and degraded
|
||||||
/// every back-to-back Deck session to DualSense (observed live on Bazzite 2026-07-04). Ours are
|
/// every back-to-back Deck session to DualSense (observed live on Bazzite 2026-07-04). Ours are
|
||||||
/// recognizable by the `PFDK…` serial ([`steam_proto::deck_serial`]) in `HID_UNIQ`, with the
|
/// recognizable by the `FVPF…` serial ([`steam_proto::deck_serial`]) in `HID_UNIQ`, with the
|
||||||
/// vhci path as belt and braces.
|
/// vhci path as belt and braces.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn physical_steam_controller_present() -> bool {
|
fn physical_steam_controller_present() -> bool {
|
||||||
@@ -2877,7 +2877,7 @@ fn physical_steam_controller_present() -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if std::fs::read_to_string(e.path().join("uevent"))
|
if std::fs::read_to_string(e.path().join("uevent"))
|
||||||
.is_ok_and(|u| u.lines().any(|l| l.starts_with("HID_UNIQ=PFDK")))
|
.is_ok_and(|u| u.lines().any(|l| l.starts_with("HID_UNIQ=FVPF")))
|
||||||
{
|
{
|
||||||
return false; // one of our own virtual Decks
|
return false; // one of our own virtual Decks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -673,21 +673,31 @@ fn deck_feature_reply() -> [u8; 64] {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
& 0xFF;
|
& 0xFF;
|
||||||
let unit_id: u32 = 0x5046_0000 | idx;
|
let unit_id: u32 = 0x5046_0000 | idx;
|
||||||
let serial = format!("PFDK{unit_id:08X}");
|
// Steam validates the unit serial's PREFIX before accepting it: a "PF"-leading serial is
|
||||||
let serial = serial.as_bytes();
|
// REJECTED ("Invalid or missing unit serial number …") and Steam then substitutes a hash and
|
||||||
|
// MANGLES the displayed name ("Steam Deck Controllerggg"). An 'F'-leading serial passes, so we
|
||||||
|
// keep our PunktFunk marker one slot in ("FVPF") — still distinct enough for the Linux side's
|
||||||
|
// physical-Deck self-detection while satisfying Steam's format check. (This, not the build-time
|
||||||
|
// attributes below, is what un-mangles the name — verified by A/B on .173.)
|
||||||
|
let unit_serial = format!("FVPF{unit_id:08X}");
|
||||||
|
let unit_serial = unit_serial.as_bytes();
|
||||||
let mut r = [0u8; 64];
|
let mut r = [0u8; 64];
|
||||||
match last[0] {
|
match last[0] {
|
||||||
0x83 => {
|
0x83 => {
|
||||||
// GET_ATTRIBUTES_VALUES: [0x83, 0x2d, then 9x (attr-id, value u32-LE)].
|
// GET_ATTRIBUTES_VALUES: [0x83, 0x2d, then 9x (attr-id, value u32-LE)].
|
||||||
r[0] = 0x83;
|
r[0] = 0x83;
|
||||||
r[1] = 0x2D;
|
r[1] = 0x2D;
|
||||||
|
// Attribute semantics per SDL's controller_constants.h: 0x04 = FIRMWARE_BUILD_TIME
|
||||||
|
// and 0x0A = BOOTLOADER_BUILD_TIME are unix timestamps that must look like real build
|
||||||
|
// dates (the old unit-id-derived junk here was cosmetic; the name mangling was the
|
||||||
|
// serial prefix). Uniqueness rides the serial.
|
||||||
let attrs: [(u8, u32); 9] = [
|
let attrs: [(u8, u32); 9] = [
|
||||||
(0x01, 0x1205),
|
(0x01, 0x1205), // ATTRIB_PRODUCT_ID
|
||||||
(0x02, 0),
|
(0x02, 0), // ATTRIB_CAPABILITIES
|
||||||
(0x0A, unit_id),
|
(0x0A, 0x6408_9000), // ATTRIB_BOOTLOADER_BUILD_TIME (2023-03-08)
|
||||||
(0x04, unit_id ^ 0x5555_5555),
|
(0x04, 0x66A8_C000), // ATTRIB_FIRMWARE_BUILD_TIME (2024-07-30)
|
||||||
(0x09, 0x2E),
|
(0x09, 0x2E), // ATTRIB_BOARD_REVISION (captured)
|
||||||
(0x0B, 0x0FA0),
|
(0x0B, 0x0FA0), // ATTRIB_CONNECTION_INTERVAL_IN_US (4 ms)
|
||||||
(0x0D, 0),
|
(0x0D, 0),
|
||||||
(0x0C, 0),
|
(0x0C, 0),
|
||||||
(0x0E, 0),
|
(0x0E, 0),
|
||||||
@@ -700,12 +710,19 @@ fn deck_feature_reply() -> [u8; 64] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
0xAE => {
|
0xAE => {
|
||||||
// GET_STRING_ATTRIBUTE: [0xAE, len, attr, ascii…].
|
// GET_STRING_ATTRIBUTE: [0xAE, len, attr, ascii…]. Steam requests two strings: attr
|
||||||
let attr = if last[2] != 0 { last[2] } else { 0x01 };
|
// 0x00 = ATTRIB_STR_BOARD_SERIAL (the PCB serial) and 0x01 = ATTRIB_STR_UNIT_SERIAL.
|
||||||
|
// Echo the exact attr requested (last[2]) — the unit serial is the one that matters:
|
||||||
|
// getting its format right (FVPF…, see above) is what un-mangles the displayed name.
|
||||||
|
// Steam ALSO validates the PCB serial against a Valve-internal format we don't have a
|
||||||
|
// real capture of; it logs "Deck Controller PCB Serial# invalid" for ANY value we send
|
||||||
|
// (including an empty one — verified on .173), but that line is BENIGN: unlike a bad
|
||||||
|
// unit serial, it does not mangle the name, change the handle, or block promotion. So we
|
||||||
|
// serve the unit serial for both attrs and accept the log.
|
||||||
r[0] = 0xAE;
|
r[0] = 0xAE;
|
||||||
r[1] = serial.len() as u8;
|
r[1] = unit_serial.len() as u8;
|
||||||
r[2] = attr;
|
r[2] = last[2];
|
||||||
r[3..3 + serial.len()].copy_from_slice(serial);
|
r[3..3 + unit_serial.len()].copy_from_slice(unit_serial);
|
||||||
}
|
}
|
||||||
_ => r.copy_from_slice(&last),
|
_ => r.copy_from_slice(&last),
|
||||||
}
|
}
|
||||||
@@ -779,7 +796,7 @@ fn on_get_string(request: &Request) -> NTSTATUS {
|
|||||||
.map(|v| v.read_u32(OFF_PAD_INDEX))
|
.map(|v| v.read_u32(OFF_PAD_INDEX))
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
& 0xFF;
|
& 0xFF;
|
||||||
format!("PFDK{:08X}", 0x5046_0000u32 | idx)
|
format!("FVPF{:08X}", 0x5046_0000u32 | idx)
|
||||||
}
|
}
|
||||||
_ => "35533AD6E774".into(),
|
_ => "35533AD6E774".into(),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user