feat(gamepad): pure-user-mode Windows DualShock 4 + Xbox 360 (drop ViGEm) + installer + multi-pad
audit / cargo-audit (push) Successful in 17s
apple / swift (push) Successful in 57s
android / android (push) Successful in 4m36s
ci / web (push) Successful in 34s
ci / docs-site (push) Successful in 52s
release / apple (push) Successful in 7m31s
ci / rust (push) Successful in 8m37s
ci / bench (push) Successful in 4m39s
decky / build-publish (push) Successful in 11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 7s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
deb / build-publish (push) Successful in 2m35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m18s
flatpak / build-publish (push) Successful in 4m0s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m31s
docker / deploy-docs (push) Successful in 19s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m22s
windows-host / package (push) Successful in 2m56s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m13s
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m15s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 59s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m3s

Windows virtual gamepads now have zero external dependencies - ViGEmBus is removed.

- DualShock 4: Windows UMDF backend (inject/dualshock4_windows.rs + dualshock4_proto.rs),
  reusing the DualSense SwDeviceCreate game-detection identity fix. The one UMDF driver serves
  the DS5 or DS4 identity/descriptor/features/strings per a device_type byte the host stamps into
  shared memory. Driver also gains IOCTL_HID_GET_STRING and a 41-byte calibration feature.
- Xbox 360: a new UMDF2 XUSB companion driver (packaging/windows/xusb-driver/) that registers
  GUID_DEVINTERFACE_XUSB and answers the buffered XInput IOCTLs from a shared section, so classic
  XInputGetState/SetState work with no kernel bus driver. inject/gamepad_windows.rs is rewritten
  to drive it and the vigem-client dependency is removed. Xbox One folds to the 360 XInput path.
- Installer: vendor + pnputil-install the three UMDF drivers (packaging/windows/gamepad-drivers/
  + install-gamepad-drivers.ps1, wired into pack-host-installer.ps1 + punktfunk-host.iss).
- Multi-pad: the host stamps each pad index into the device Location (pszDeviceLocation); the
  driver reads it via WdfDeviceAllocAndQueryProperty to map its own *-shm-<index>, with
  UmdfHostProcessSharing=ProcessSharingDisabled giving each pad its own host (per-pad statics).

Validated live on the Windows host: Cyberpunk native DualSense detection, DS4 identity + descriptor,
XInputGetState + rumble round-trip, two pads -> two distinct XInput slots, and a full installer build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:34:22 +02:00
parent f208f3d92e
commit b0c82333d2
29 changed files with 2389 additions and 288 deletions
+255 -20
View File
@@ -11,7 +11,7 @@
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
use core::ffi::c_void;
use core::sync::atomic::{AtomicPtr, Ordering};
use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
use wdk_sys::{
NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
@@ -41,6 +41,7 @@ const IOCTL_HID_GET_REPORT_DESCRIPTOR: u32 = hid_ctl(1);
const IOCTL_HID_READ_REPORT: u32 = hid_ctl(2);
const IOCTL_HID_WRITE_REPORT: u32 = hid_ctl(3);
const IOCTL_HID_GET_DEVICE_ATTRIBUTES: u32 = hid_ctl(9);
const IOCTL_HID_GET_STRING: u32 = hid_ctl(4);
const IOCTL_UMDF_HID_SET_FEATURE: u32 = hid_ctl(20);
const IOCTL_UMDF_HID_GET_FEATURE: u32 = hid_ctl(21);
const IOCTL_UMDF_HID_SET_OUTPUT_REPORT: u32 = hid_ctl(22);
@@ -57,6 +58,8 @@ const WdfSynchronizationScopeInheritFromParent: i32 = 1; // WDF_SYNCHRONIZATION_
const DS_VID: u16 = 0x054C;
const DS_PID: u16 = 0x0CE6;
const DS_VER: u16 = 0x0100;
/// DualShock 4 v2 product id — served (same VID/version) when the host stamps device_type=1.
const DS4_PID: u16 = 0x09CC;
// Sony DualSense USB HID report descriptor (273 bytes), verbatim from inputtino (== inject/dualsense.rs).
// NOTE: inject/dualsense.rs comments this as "232 bytes" — that comment is wrong; it is 273.
@@ -84,10 +87,10 @@ static DUALSENSE_RDESC: [u8; 273] = [
// Feature reports hid-playstation / Steam read during init (each array's first byte is the report id).
#[rustfmt::skip]
static DS_FEATURE_CALIBRATION: [u8; 42] = [ // 0x05 motion calibration
static DS_FEATURE_CALIBRATION: [u8; 41] = [ // 0x05 motion calibration: 1 id + 40 data (descriptor declares feature 0x05 as 0x95 0x28 = 40)
0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x27, 0xF0, 0xD8, 0x10, 0x27, 0xF0, 0xD8, 0x10,
0x27, 0xF0, 0xD8, 0xF4, 0x01, 0xF4, 0x01, 0x10, 0x27, 0xF0, 0xD8, 0x10, 0x27, 0xF0, 0xD8, 0x10,
0x27, 0xF0, 0xD8, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x27, 0xF0, 0xD8, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00,
];
#[rustfmt::skip]
static DS_FEATURE_PAIRING: [u8; 20] = [ // 0x09 pairing info (MAC at 1..7)
@@ -102,16 +105,85 @@ static DS_FEATURE_FIRMWARE: [u8; 64] = [ // 0x20 firmware info
0x14, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
// HID descriptor (9 bytes, packed): len, type=0x21, bcdHID=0x0100, country=0, numDesc=1,
// then {reportType=0x22, wReportLength=273 (0x0111)}.
// ---- DualShock 4 v2 assets (served when the host stamps device_type=1) ----
// Sony DualShock 4 v2 USB HID report descriptor (507 bytes), verbatim from inject/dualshock4.rs.
#[rustfmt::skip]
static DS4_RDESC: [u8; 507] = [
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31,
0x09, 0x32, 0x09, 0x35, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95,
0x04, 0x81, 0x02, 0x09, 0x39, 0x15, 0x00, 0x25, 0x07, 0x35, 0x00, 0x46,
0x3B, 0x01, 0x65, 0x14, 0x75, 0x04, 0x95, 0x01, 0x81, 0x42, 0x65, 0x00,
0x05, 0x09, 0x19, 0x01, 0x29, 0x0E, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01,
0x95, 0x0E, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x20, 0x75, 0x06, 0x95,
0x01, 0x15, 0x00, 0x25, 0x7F, 0x81, 0x02, 0x05, 0x01, 0x09, 0x33, 0x09,
0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x02,
0x06, 0x00, 0xFF, 0x09, 0x21, 0x95, 0x36, 0x81, 0x02, 0x85, 0x05, 0x09,
0x22, 0x95, 0x1F, 0x91, 0x02, 0x85, 0x04, 0x09, 0x23, 0x95, 0x24, 0xB1,
0x02, 0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xB1, 0x02, 0x85, 0x08, 0x09,
0x25, 0x95, 0x03, 0xB1, 0x02, 0x85, 0x10, 0x09, 0x26, 0x95, 0x04, 0xB1,
0x02, 0x85, 0x11, 0x09, 0x27, 0x95, 0x02, 0xB1, 0x02, 0x85, 0x12, 0x06,
0x02, 0xFF, 0x09, 0x21, 0x95, 0x0F, 0xB1, 0x02, 0x85, 0x13, 0x09, 0x22,
0x95, 0x16, 0xB1, 0x02, 0x85, 0x14, 0x06, 0x05, 0xFF, 0x09, 0x20, 0x95,
0x10, 0xB1, 0x02, 0x85, 0x15, 0x09, 0x21, 0x95, 0x2C, 0xB1, 0x02, 0x06,
0x80, 0xFF, 0x85, 0x80, 0x09, 0x20, 0x95, 0x06, 0xB1, 0x02, 0x85, 0x81,
0x09, 0x21, 0x95, 0x06, 0xB1, 0x02, 0x85, 0x82, 0x09, 0x22, 0x95, 0x05,
0xB1, 0x02, 0x85, 0x83, 0x09, 0x23, 0x95, 0x01, 0xB1, 0x02, 0x85, 0x84,
0x09, 0x24, 0x95, 0x04, 0xB1, 0x02, 0x85, 0x85, 0x09, 0x25, 0x95, 0x06,
0xB1, 0x02, 0x85, 0x86, 0x09, 0x26, 0x95, 0x06, 0xB1, 0x02, 0x85, 0x87,
0x09, 0x27, 0x95, 0x23, 0xB1, 0x02, 0x85, 0x88, 0x09, 0x28, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0x89, 0x09, 0x29, 0x95, 0x02, 0xB1, 0x02, 0x85, 0x90,
0x09, 0x30, 0x95, 0x05, 0xB1, 0x02, 0x85, 0x91, 0x09, 0x31, 0x95, 0x03,
0xB1, 0x02, 0x85, 0x92, 0x09, 0x32, 0x95, 0x03, 0xB1, 0x02, 0x85, 0x93,
0x09, 0x33, 0x95, 0x0C, 0xB1, 0x02, 0x85, 0x94, 0x09, 0x34, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0xA0, 0x09, 0x40, 0x95, 0x06, 0xB1, 0x02, 0x85, 0xA1,
0x09, 0x41, 0x95, 0x01, 0xB1, 0x02, 0x85, 0xA2, 0x09, 0x42, 0x95, 0x01,
0xB1, 0x02, 0x85, 0xA3, 0x09, 0x43, 0x95, 0x30, 0xB1, 0x02, 0x85, 0xA4,
0x09, 0x44, 0x95, 0x0D, 0xB1, 0x02, 0x85, 0xF0, 0x09, 0x47, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0xF1, 0x09, 0x48, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF2,
0x09, 0x49, 0x95, 0x0F, 0xB1, 0x02, 0x85, 0xA7, 0x09, 0x4A, 0x95, 0x01,
0xB1, 0x02, 0x85, 0xA8, 0x09, 0x4B, 0x95, 0x01, 0xB1, 0x02, 0x85, 0xA9,
0x09, 0x4C, 0x95, 0x08, 0xB1, 0x02, 0x85, 0xAA, 0x09, 0x4E, 0x95, 0x01,
0xB1, 0x02, 0x85, 0xAB, 0x09, 0x4F, 0x95, 0x39, 0xB1, 0x02, 0x85, 0xAC,
0x09, 0x50, 0x95, 0x39, 0xB1, 0x02, 0x85, 0xAD, 0x09, 0x51, 0x95, 0x0B,
0xB1, 0x02, 0x85, 0xAE, 0x09, 0x52, 0x95, 0x01, 0xB1, 0x02, 0x85, 0xAF,
0x09, 0x53, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xB0, 0x09, 0x54, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0xE0, 0x09, 0x57, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xB3,
0x09, 0x55, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xB4, 0x09, 0x55, 0x95, 0x3F,
0xB1, 0x02, 0x85, 0xB5, 0x09, 0x56, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xD0,
0x09, 0x58, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xD4, 0x09, 0x59, 0x95, 0x3F,
0xB1, 0x02, 0xC0,
];
// DS4 feature reports games read during init (each array's first byte is the report id).
#[rustfmt::skip]
static DS4_FEATURE_PAIRING: [u8; 16] = [ // 0x12 pairing info (MAC at bytes 1..7)
0x12, 0x01, 0x00, 0xEF, 0xBE, 0xAD, 0xDE, 0x08, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
#[rustfmt::skip]
static DS4_FEATURE_CALIBRATION: [u8; 37] = [ // 0x02 IMU calibration
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0xF0, 0xFF, 0x10, 0x00, 0xF0, 0xFF, 0x10,
0x00, 0xF0, 0xFF, 0x20, 0x00, 0x20, 0x00, 0x00, 0x20, 0x00, 0xE0, 0x00, 0x20, 0x00, 0xE0, 0x00,
0x20, 0x00, 0xE0, 0x00, 0x00,
];
#[rustfmt::skip]
static DS4_FEATURE_FIRMWARE: [u8; 49] = [ // 0xa3 firmware/build info
0xA3, 0x41, 0x75, 0x67, 0x20, 0x20, 0x33, 0x20, 0x32, 0x30, 0x31, 0x33, 0x00, 0x00, 0x00, 0x00,
0x00, 0x30, 0x37, 0x3A, 0x30, 0x31, 0x3A, 0x31, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
];
// HID descriptor (9 bytes, packed): len, type=0x21, bcdHID=0x0100, country=0, numDesc=1, then
// {reportType=0x22, wReportLength}. DualSense = 273 (0x0111); DualShock 4 = 507 (0x01FB).
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];
// HID_DEVICE_ATTRIBUTES (32 bytes): Size(u32)=32, VendorID, ProductID, VersionNumber, Reserved[11].
fn hid_attrs() -> [u8; 32] {
// `ds4` selects the DualShock 4 product id (same VID/version).
fn hid_attrs(ds4: bool) -> [u8; 32] {
let mut a = [0u8; 32];
a[0..4].copy_from_slice(&32u32.to_le_bytes());
a[4..6].copy_from_slice(&DS_VID.to_le_bytes());
a[6..8].copy_from_slice(&DS_PID.to_le_bytes());
a[6..8].copy_from_slice(&(if ds4 { DS4_PID } else { DS_PID }).to_le_bytes());
a[8..10].copy_from_slice(&DS_VER.to_le_bytes());
a
}
@@ -128,8 +200,24 @@ const NEUTRAL_REPORT: [u8; 64] = {
r[8] = 0x08; // buttons[0]: low nibble = dpad hat (8 = neutral), high nibble = face buttons (0)
r
};
fn neutral_report() -> [u8; 64] {
NEUTRAL_REPORT
// Neutral DualShock 4 input report 0x01: sticks centered (0x80); the dpad hat is in byte 5 (low
// nibble), so a neutral hat (8) lands there instead of byte 8.
const DS4_NEUTRAL_REPORT: [u8; 64] = {
let mut r = [0u8; 64];
r[0] = 0x01; // report id
r[1] = 0x80; // LX
r[2] = 0x80; // LY
r[3] = 0x80; // RX
r[4] = 0x80; // RY
r[5] = 0x08; // buttons[0]: low nibble = dpad hat (8 = neutral), high nibble = face buttons (0)
r
};
fn neutral_report(ds4: bool) -> [u8; 64] {
if ds4 {
DS4_NEUTRAL_REPORT
} else {
NEUTRAL_REPORT
}
}
static MANUAL_QUEUE: AtomicPtr<WDFQUEUE__> = AtomicPtr::new(core::ptr::null_mut());
@@ -198,6 +286,59 @@ pub unsafe extern "system" fn driver_entry(
}
}
/// The pad index this device serves (which `pfds-shm-<index>` section to map). The host stamps it into
/// the device Location (`pszDeviceLocation`); the driver reads it in EvtDeviceAdd. With
/// `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) each pad gets its own WUDFHost, so this
/// static is per-pad — the basis for multi-pad.
static SHM_INDEX: AtomicU32 = AtomicU32::new(0);
/// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (not re-exported at the wdk_sys root).
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
/// Read the pad index the host stamped into the device Location (a NUL-terminated UTF-16 decimal
/// string). Defaults to 0 (single-pad) if absent.
fn query_shm_index(device: WDFDEVICE) -> u32 {
let mut mem: WDFMEMORY = core::ptr::null_mut();
// SAFETY: device valid; property = LocationInformation; pool ignored in UMDF; mem receives the handle.
let st = unsafe {
call_unsafe_wdf_function_binding!(
WdfDeviceAllocAndQueryProperty,
device,
DEVICE_PROPERTY_LOCATION_INFORMATION,
0,
WDF_NO_OBJECT_ATTRIBUTES,
&mut mem
)
};
if !nt_success(st) || mem.is_null() {
return 0;
}
let mut len: usize = 0;
// SAFETY: mem valid.
let buf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut len) }
as *const u16;
if buf.is_null() {
return 0;
}
let mut idx: u32 = 0;
let mut any = false;
for i in 0..(len / 2).min(8) {
// SAFETY: buf valid for len bytes; i < len/2.
let c = unsafe { *buf.add(i) };
if c == 0 {
break;
}
if (0x30..=0x39).contains(&c) {
idx = idx.wrapping_mul(10).wrapping_add((c - 0x30) as u32);
any = true;
}
}
if any {
idx
} else {
0
}
}
extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INIT) -> NTSTATUS {
log("[pf-ds] EvtDeviceAdd");
@@ -220,6 +361,10 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
return st;
}
let shm_idx = query_shm_index(device);
SHM_INDEX.store(shm_idx, Ordering::Relaxed);
dbglog!("[pf-ds] shm index = {shm_idx}");
// Default parallel queue handling all IOCTLs.
// SAFETY: zeroed config then fields set; Size matches the struct.
let mut qcfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
@@ -317,9 +462,18 @@ extern "C" fn evt_io_device_control(
dbglog!("[pf-ds] ioctl 0x{ioctl:08x} out={_output_len} in={_input_len}");
}
let status: NTSTATUS = match ioctl {
IOCTL_HID_GET_DEVICE_DESCRIPTOR => copy_to_output(request, &HID_DESC),
IOCTL_HID_GET_DEVICE_ATTRIBUTES => copy_to_output(request, &hid_attrs()),
IOCTL_HID_GET_REPORT_DESCRIPTOR => copy_to_output(request, &DUALSENSE_RDESC),
IOCTL_HID_GET_DEVICE_DESCRIPTOR => {
copy_to_output(request, if device_type() == 1 { &DS4_HID_DESC } else { &HID_DESC })
}
IOCTL_HID_GET_DEVICE_ATTRIBUTES => copy_to_output(request, &hid_attrs(device_type() == 1)),
IOCTL_HID_GET_REPORT_DESCRIPTOR => copy_to_output(
request,
if device_type() == 1 {
&DS4_RDESC[..]
} else {
&DUALSENSE_RDESC[..]
},
),
IOCTL_HID_READ_REPORT => {
let mq: WDFQUEUE = MANUAL_QUEUE.load(Ordering::SeqCst);
// SAFETY: request valid; mq is the manual queue created in EvtDeviceAdd.
@@ -341,7 +495,10 @@ extern "C" fn evt_io_device_control(
STATUS_SUCCESS
}
IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(request),
IOCTL_UMDF_HID_GET_INPUT_REPORT => copy_to_output(request, &neutral_report()),
IOCTL_UMDF_HID_GET_INPUT_REPORT => {
copy_to_output(request, &neutral_report(device_type() == 1))
}
IOCTL_HID_GET_STRING => on_get_string(request),
_ => STATUS_NOT_IMPLEMENTED,
};
@@ -479,11 +636,15 @@ fn on_get_feature(request: WDFREQUEST) -> NTSTATUS {
}
// SAFETY: inbuf valid for >=1 byte.
let report_id = unsafe { *inbuf };
let blob: &[u8] = match report_id {
0x05 => &DS_FEATURE_CALIBRATION,
0x09 => &DS_FEATURE_PAIRING,
0x20 => &DS_FEATURE_FIRMWARE,
other => {
// DualSense uses feature ids 0x05/0x09/0x20; DualShock 4 uses 0x02/0x12/0xa3.
let blob: &[u8] = match (device_type() == 1, report_id) {
(false, 0x05) => &DS_FEATURE_CALIBRATION,
(false, 0x09) => &DS_FEATURE_PAIRING,
(false, 0x20) => &DS_FEATURE_FIRMWARE,
(true, 0x02) => &DS4_FEATURE_CALIBRATION,
(true, 0x12) => &DS4_FEATURE_PAIRING,
(true, 0xA3) => &DS4_FEATURE_FIRMWARE,
(_, other) => {
dbglog!("[pf-ds] GET_FEATURE unknown report id 0x{other:02x}");
return STATUS_INVALID_PARAMETER;
}
@@ -491,12 +652,70 @@ fn on_get_feature(request: WDFREQUEST) -> NTSTATUS {
copy_to_output(request, blob)
}
// IOCTL_HID_GET_STRING: the input is a ULONG whose low word is the string id and whose high word is
// the language id. Reply with the requested device string as a NUL-terminated UTF-16 buffer. Native
// PS5 / Steam code reads these (HidD_GetProductString / HidD_GetSerialNumberString — the serial is one
// way they tell USB from BT); the old default returned STATUS_NOT_IMPLEMENTED, leaving them blank.
// Observed live on this device, Windows polls ids 0x0E/0x0F/0x10 (lang 0x0409) cyclically — the
// manufacturer/product/serial slots — NOT the 0/1/2 HID_STRING_ID_* constants; we map both forms.
fn on_get_string(request: WDFREQUEST) -> NTSTATUS {
let mut inmem: WDFMEMORY = core::ptr::null_mut();
// SAFETY: request valid.
let st = unsafe {
call_unsafe_wdf_function_binding!(WdfRequestRetrieveInputMemory, request, &mut inmem)
};
if !nt_success(st) {
return st;
}
let mut inlen: usize = 0;
// SAFETY: inmem valid.
let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
as *const u8;
// SAFETY: inbuf is valid for inlen bytes; read the 4-byte id value when present.
let id_val: u32 = if !inbuf.is_null() && inlen >= 4 {
unsafe { core::ptr::read_unaligned(inbuf as *const u32) }
} else {
0
};
let string_id = id_val & 0xFFFF;
let ds4 = device_type() == 1;
dbglog!("[pf-ds] GET_STRING id=0x{string_id:04x} (raw 0x{id_val:08x}) ds4={ds4}");
let s: &str = match string_id {
0 | 0x000e => {
if ds4 {
"Sony Computer Entertainment"
} else {
"Sony Interactive Entertainment"
}
}
2 | 0x0010 => {
if ds4 {
"DEADBEEF0001"
} else {
"35533AD6E774"
}
}
_ => {
if ds4 {
"Wireless Controller"
} else {
"DualSense Wireless Controller"
}
}
};
let mut wide: Vec<u16> = s.encode_utf16().collect();
wide.push(0); // NUL terminator
// SAFETY: reinterpret the UTF-16 buffer as bytes for the byte-oriented copy_to_output.
let bytes = unsafe { core::slice::from_raw_parts(wide.as_ptr() as *const u8, wide.len() * 2) };
copy_to_output(request, bytes)
}
// Open + map the host's shared-memory section (Global\pfds-shm-0) and run `f` against the mapped base
// if it exists with a valid magic, then unmap. NOT cached: re-mapped per access so the driver always
// sees the current section (UMDF groups all devices in one WUDFHost, and the host may recreate the
// section across restarts — a cached view would go stale). ~125 maps/s from the timer = negligible.
fn with_shm<F: FnOnce(*mut u8)>(f: F) {
let name: Vec<u16> = "Global\\pfds-shm-0"
let name: Vec<u16> = format!("Global\\pfds-shm-{}", SHM_INDEX.load(Ordering::Relaxed))
.encode_utf16()
.chain(std::iter::once(0))
.collect();
@@ -516,7 +735,10 @@ fn with_shm<F: FnOnce(*mut u8)>(f: F) {
let magic = unsafe { core::ptr::read_unaligned(view as *const u32) };
if magic == SHM_MAGIC {
if !LOGGED_SHM.swap(true, Ordering::Relaxed) {
dbglog!("[pf-ds] control: shared memory mapped (Global\\pfds-shm-0)");
dbglog!(
"[pf-ds] control: shared memory mapped (Global\\pfds-shm-{})",
SHM_INDEX.load(Ordering::Relaxed)
);
}
f(view);
}
@@ -524,6 +746,19 @@ fn with_shm<F: FnOnce(*mut u8)>(f: F) {
unsafe { UnmapViewOfFile(view as *const c_void) };
}
/// The host's device-type selector from shared memory (`device_type` byte @140): 0 = DualSense
/// (default), 1 = DualShock 4. Read fresh on each enumeration query — cheap, and the host stamps the
/// section before `SwDeviceCreate`, so it's set by the time hidclass asks for the descriptor /
/// attributes. Defaults to DualSense if the section isn't mapped yet (magic absent).
fn device_type() -> u8 {
let mut t = 0u8;
with_shm(|view| {
// SAFETY: view points at a mapped 256-byte section; the device-type byte is at offset 140.
t = unsafe { *view.add(140) };
});
t
}
extern "C" fn evt_timer(timer: WDFTIMER) {
// Pull the latest host input report from shared memory (if the host has connected).
with_shm(|view| {