fix(windows-installer): build the gamepad drivers from source in CI too
Fold the pf-dualsense (DualSense / DualShock 4) and pf-xusb (Xbox 360 / XInput)
UMDF drivers into the in-tree drivers workspace (their source had stale
../../crates/wdk-* path-deps from before the wdk vendoring reorg and could no
longer build at all) and build them from source per release, exactly like
pf-vdisplay - same anti-stale reasoning. One `cargo build --release` now builds
all three drivers against the vendored wdk-sys (incl. the bindgen 0.72 pin), and
build-gamepad-drivers.ps1 signs pf_dualsense + pf_xusb (clear FORCE_INTEGRITY ->
sign dll -> stampinf -> Inf2Cat -> sign cat) with one shared cert + .cer,
matching the layout install-gamepad-drivers.ps1 expects. pack-host-installer.ps1
builds + stages them instead of the retired checked-in binaries.
Validated on the runner: the whole workspace (pf-vdisplay + pf-dualsense +
pf-xusb) builds with CARGO_TARGET_DIR=C:\t set, and build-gamepad-drivers.ps1
produces signed pf_dualsense.{dll,inf,cat} + pf_xusb.{dll,inf,cat} + the .cer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+20
-2
@@ -63,9 +63,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
@@ -401,6 +401,15 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pf-dualsense"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"wdk",
|
||||
"wdk-build",
|
||||
"wdk-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pf-vdisplay"
|
||||
version = "0.0.1"
|
||||
@@ -414,6 +423,15 @@ dependencies = [
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pf-xusb"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"wdk",
|
||||
"wdk-build",
|
||||
"wdk-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# crates/pf-driver-proto from the main tree.
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["wdk-probe", "wdk-iddcx", "pf-vdisplay"]
|
||||
members = ["wdk-probe", "wdk-iddcx", "pf-vdisplay", "pf-dualsense", "pf-xusb"]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# pf-dualsense - punktfunk virtual DualSense (DS5) / DualShock 4 (DS4) UMDF2 HID minidriver.
|
||||
# A member of the in-tree drivers workspace (shares the vendored wdk-sys/wdk-build with the bindgen pin
|
||||
# + the crt-static .cargo/config), built from source per release like pf-vdisplay.
|
||||
[package]
|
||||
name = "pf-dualsense"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
description = "punktfunk virtual DualSense / DualShock 4 UMDF2 HID minidriver"
|
||||
|
||||
[package.metadata.wdk.driver-model]
|
||||
driver-type = "UMDF"
|
||||
umdf-version-major = 2
|
||||
target-umdf-version-minor = 31
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[build-dependencies]
|
||||
wdk-build.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wdk.workspace = true
|
||||
wdk-sys.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["hid"]
|
||||
hid = ["wdk-sys/hid"]
|
||||
nightly = ["wdk-sys/nightly", "wdk/nightly"]
|
||||
@@ -0,0 +1,83 @@
|
||||
# pf-dualsense — virtual DualSense UMDF2 HID minidriver (M0 spike)
|
||||
|
||||
A self-authored **Rust UMDF2 HID minidriver** that presents a virtual Sony **DualSense**
|
||||
(VID `054C` / PID `0CE6`) to Windows, so games drive adaptive triggers / lightbar / rumble —
|
||||
capabilities ViGEm structurally cannot deliver. This is the M0 feasibility spike for rich
|
||||
controller support in the punktfunk Windows host.
|
||||
|
||||
## Status (2026-06-21)
|
||||
|
||||
**Load + recognition: DONE.** A self-signed build **loads under Secure Boot ON** and enumerates as a
|
||||
genuine DualSense HID game controller (`Status: OK`, VID `054C`, 273-byte DualSense report descriptor,
|
||||
PID `0CE6` via `GET_DEVICE_ATTRIBUTES`). Validated live on the RTX box (`192.168.1.173`, Win11 25H2).
|
||||
|
||||
**Remaining:** the real-game `0x02` adaptive-trigger gate (Cyberpunk 2077 on the interactive desktop →
|
||||
confirm `[pf-ds] *** OUTPUT ...` in the driver log), then wire into the host (M1+).
|
||||
|
||||
## This is a reference snapshot
|
||||
|
||||
The crate's `Cargo.toml` uses path-deps into `microsoft/windows-drivers-rs`
|
||||
(`../../crates/wdk{,-sys,-build}`), so it builds **inside a `windows-drivers-rs` checkout's
|
||||
`examples/` dir**, not standalone in this repo. On the dev box it lives at
|
||||
`C:\Users\Public\m0\windows-drivers-rs\examples\pf-dualsense`. These files are checked in for
|
||||
version control / portability of the spike.
|
||||
|
||||
## Build / sign / install recipe (the one that actually loads)
|
||||
|
||||
Prereqs on the Windows box: **WDK 26100**, **LLVM 21.1.2** (pinned — newer bindgen breaks),
|
||||
`cargo-make`, Rust MSVC. A self-signed CodeSigning cert in `CurrentUser\My` + `LocalMachine\Root` +
|
||||
`TrustedPublisher`.
|
||||
|
||||
Every build needs:
|
||||
|
||||
```powershell
|
||||
$env:LIBCLANG_PATH = 'C:\Program Files\LLVM\bin'
|
||||
$env:Version_Number = '10.0.26100.0' # else wdk-build picks 10.0.28000.0 (no km/crt) and bindgen fails
|
||||
```
|
||||
|
||||
Then, in the example dir:
|
||||
|
||||
```powershell
|
||||
cargo make # -> target\debug\pf_dualsense_package\ (.inf/.cat/.dll)
|
||||
|
||||
# *** CRITICAL: clear the PE FORCE_INTEGRITY bit ***
|
||||
# windows-drivers-rs links the DLL with /INTEGRITYCHECK, which forces a CI-trusted page-hash
|
||||
# signature a self-signed cert cannot satisfy (CodeIntegrity 3004 "hash not found" /
|
||||
# 3089 VerificationError 7). SudoVDA.dll has this bit OFF. Clear bit 0x80 at PE-header offset +0x5e:
|
||||
$f = 'target\debug\pf_dualsense_package\pf_dualsense.dll'
|
||||
$b = [IO.File]::ReadAllBytes($f); $pe = [BitConverter]::ToInt32($b,0x3c); $off = $pe + 0x5e
|
||||
$dc = [BitConverter]::ToUInt16($b,$off); $bb = [BitConverter]::GetBytes([uint16]($dc -band 0xFF7F))
|
||||
$b[$off]=$bb[0]; $b[$off+1]=$bb[1]; [IO.File]::WriteAllBytes($f,$b)
|
||||
|
||||
signtool sign /fd SHA256 /sha1 <cert-thumbprint> $f
|
||||
Remove-Item target\debug\pf_dualsense_package\pf_dualsense.cat
|
||||
Inf2Cat /driver:target\debug\pf_dualsense_package /os:10_x64
|
||||
signtool sign /fd SHA256 /sha1 <cert-thumbprint> target\debug\pf_dualsense_package\pf_dualsense.cat
|
||||
|
||||
pnputil /add-driver target\debug\pf_dualsense_package\pf_dualsense.inf /install
|
||||
devgen /add /hardwareid "root\pf_dualsense" # creates the (transient, SWD) device node
|
||||
```
|
||||
|
||||
`devgen` is at `...\Windows Kits\10\Tools\10.0.26100.0\x64\devgen.exe`. SWD devgen devices clear on
|
||||
reboot (recreate after each boot). TODO: drop the post-build PE patch by stopping wdk-build emitting
|
||||
`/INTEGRITYCHECK`.
|
||||
|
||||
## The three bugs that made it work (porting a WDK C sample to Rust)
|
||||
|
||||
`WDF_*_CONFIG_INIT` / `WDF_OBJECT_ATTRIBUTES_INIT` macros set **non-zero** defaults — `mem::zeroed()`
|
||||
silently breaks them:
|
||||
|
||||
1. **FORCE_INTEGRITY** (above) — the load wall.
|
||||
2. **Timer `ExecutionLevel`** — zeroed = Invalid → `WdfTimerCreate` 0xC0200209. Set
|
||||
`ExecutionLevel/SynchronizationScope = InheritFromParent` + `AutomaticSerialization = TRUE`
|
||||
(the working vhidmini2 shape).
|
||||
3. **Queue `Settings.Parallel.NumberOfPresentedRequests`** — zeroed = 0 → a parallel queue presents
|
||||
zero requests → `EvtIoDeviceControl` never fires → no HID handshake → ~5 s timeout →
|
||||
`CM_PROB_FAILED_START`. Set to `u32::MAX`.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Uses **statics, not per-device WDF contexts** → only one device instance per WUDFHost works.
|
||||
Multi-instance needs proper device contexts.
|
||||
- Port of the WDK `vhidmini2` UMDF2 sample; DualSense identity + 273-byte descriptor + feature blobs
|
||||
`0x05`/`0x09`/`0x20` from `crates/punktfunk-host/src/inject/dualsense.rs`.
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// License: MIT OR Apache-2.0
|
||||
|
||||
//! Build script for the `sample-umdf-driver` crate.
|
||||
//!
|
||||
//! Based on the [`wdk_build::Config`] parsed from the build tree, this build
|
||||
//! script will provide `Cargo` with the necessary information to build the
|
||||
//! driver binary (ex. linker flags)
|
||||
|
||||
fn main() -> Result<(), wdk_build::ConfigError> {
|
||||
wdk_build::configure_wdk_binary_build()
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
;/*++
|
||||
; punktfunk virtual DualSense — UMDF2 HID minidriver INF (M0 spike).
|
||||
; Adapted from the WDK vhidmini2 UMDF2 sample (VhidminiUm.inx).
|
||||
; Depends on MsHidUmdf.inf (build >= 22000).
|
||||
; Install: devgen /add /hardwareid "root\pf_dualsense" (after pnputil /add-driver /install)
|
||||
;--*/
|
||||
[Version]
|
||||
Signature="$WINDOWS NT$"
|
||||
Class=HIDClass
|
||||
ClassGuid={745a17a0-74d3-11d0-b6fe-00a0c90f57da}
|
||||
Provider=%ProviderString%
|
||||
CatalogFile=pf_dualsense.cat
|
||||
PnpLockdown=1
|
||||
|
||||
[DestinationDirs]
|
||||
DefaultDestDir = 13
|
||||
|
||||
[SourceDisksNames]
|
||||
1=%Disk_Description%,,,
|
||||
|
||||
[SourceDisksFiles]
|
||||
pf_dualsense.dll=1
|
||||
|
||||
[Manufacturer]
|
||||
%ManufacturerString%=pf, NT$ARCH$.10.0...22000
|
||||
|
||||
[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` for the host's virtual DualShock 4 — the
|
||||
; same driver binds both and serves the DualSense or DS4 identity per the device_type byte the host
|
||||
; stamps into shared memory.
|
||||
%DeviceDesc%=pfDualSense, root\pf_dualsense, pf_dualsense, pf_dualshock4
|
||||
|
||||
[pfDualSense.NT]
|
||||
CopyFiles=UMDriverCopy
|
||||
Include=MsHidUmdf.inf
|
||||
Needs=MsHidUmdf.NT
|
||||
Include=WUDFRD.inf
|
||||
Needs=WUDFRD_LowerFilter.NT
|
||||
|
||||
[pfDualSense.NT.hw]
|
||||
Include=MsHidUmdf.inf
|
||||
Needs=MsHidUmdf.NT.hw
|
||||
Include=WUDFRD.inf
|
||||
Needs=WUDFRD_LowerFilter.NT.hw
|
||||
|
||||
[pfDualSense.NT.Services]
|
||||
Include=MsHidUmdf.inf
|
||||
Needs=MsHidUmdf.NT.Services
|
||||
Include=WUDFRD.inf
|
||||
Needs=WUDFRD_LowerFilter.NT.Services
|
||||
|
||||
[pfDualSense.NT.Filters]
|
||||
Include=WUDFRD.inf
|
||||
Needs=WUDFRD_LowerFilter.NT.Filters
|
||||
|
||||
[pfDualSense.NT.Wdf]
|
||||
UmdfService="pf_dualsense", pf_dualsense_Install
|
||||
UmdfServiceOrder=pf_dualsense
|
||||
UmdfKernelModeClientPolicy=AllowKernelModeClients
|
||||
UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects
|
||||
UmdfMethodNeitherAction=Copy
|
||||
UmdfFsContextUsePolicy=CanUseFsContext2
|
||||
; Each pad gets its OWN WUDFHost so the driver's per-pad statics (incl. the shm index) don't collide
|
||||
; across multiple simultaneous controllers (multi-pad).
|
||||
UmdfHostProcessSharing=ProcessSharingDisabled
|
||||
|
||||
[pf_dualsense_Install]
|
||||
UmdfLibraryVersion=$UMDFVERSION$
|
||||
ServiceBinary="%13%\pf_dualsense.dll"
|
||||
|
||||
[UMDriverCopy]
|
||||
pf_dualsense.dll
|
||||
|
||||
[Strings]
|
||||
ProviderString ="punktfunk"
|
||||
ManufacturerString ="punktfunk"
|
||||
ClassName ="HID device"
|
||||
Disk_Description ="punktfunk DualSense Installation Disk"
|
||||
DeviceDesc ="punktfunk Virtual DualSense"
|
||||
@@ -0,0 +1,789 @@
|
||||
// punktfunk virtual DualSense — UMDF2 HID minidriver (M0 spike).
|
||||
//
|
||||
// A Rust port of the WDK `vhidmini2` UMDF2 sample, reconfigured to present a Sony DualSense
|
||||
// (VID 054C / PID 0CE6) using the inputtino report descriptor + feature blobs punktfunk already
|
||||
// ships in `inject/dualsense.rs`. Its purpose for M0(b) is to (1) enumerate as a genuine DualSense
|
||||
// and (2) LOG every output report the game writes — the adaptive-trigger `0x02` gate.
|
||||
//
|
||||
// No WDF object contexts: this is a singleton virtual device, so per-device state lives in statics.
|
||||
// All WDF calls go through `call_unsafe_wdf_function_binding!`; HID/WDF structs are hand-built.
|
||||
|
||||
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
|
||||
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering};
|
||||
|
||||
use wdk_sys::{
|
||||
NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
|
||||
WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES,
|
||||
WDF_TIMER_CONFIG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFQUEUE__, WDFREQUEST, WDFTIMER,
|
||||
call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
|
||||
};
|
||||
|
||||
// ---- NTSTATUS values ----
|
||||
const STATUS_SUCCESS: NTSTATUS = 0;
|
||||
const STATUS_UNSUCCESSFUL: NTSTATUS = 0xC000_0001u32 as NTSTATUS;
|
||||
const STATUS_NOT_IMPLEMENTED: NTSTATUS = 0xC000_0002u32 as NTSTATUS;
|
||||
const STATUS_INVALID_PARAMETER: NTSTATUS = 0xC000_000Du32 as NTSTATUS;
|
||||
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
||||
|
||||
#[inline]
|
||||
fn nt_success(s: NTSTATUS) -> bool {
|
||||
s >= 0
|
||||
}
|
||||
|
||||
// ---- HID minidriver IOCTLs: CTL_CODE(FILE_DEVICE_KEYBOARD=0x0b, id, METHOD_NEITHER=3, ANY) ----
|
||||
const fn hid_ctl(id: u32) -> u32 {
|
||||
(0x0000_000b << 16) | (id << 2) | 3
|
||||
}
|
||||
const IOCTL_HID_GET_DEVICE_DESCRIPTOR: u32 = hid_ctl(0);
|
||||
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);
|
||||
const IOCTL_UMDF_HID_GET_INPUT_REPORT: u32 = hid_ctl(23);
|
||||
|
||||
// ---- WDF enum values ----
|
||||
const WdfIoQueueDispatchParallel: i32 = 2;
|
||||
const WdfIoQueueDispatchManual: i32 = 3;
|
||||
const WdfUseDefault: i32 = 2; // WDF_TRI_STATE
|
||||
const WdfExecutionLevelInheritFromParent: i32 = 1; // WDF_EXECUTION_LEVEL
|
||||
const WdfSynchronizationScopeInheritFromParent: i32 = 1; // WDF_SYNCHRONIZATION_SCOPE
|
||||
|
||||
// ---- DualSense identity ----
|
||||
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.
|
||||
#[rustfmt::skip]
|
||||
static DUALSENSE_RDESC: [u8; 273] = [
|
||||
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x01, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35,
|
||||
0x09, 0x33, 0x09, 0x34, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x06, 0x81, 0x02, 0x06,
|
||||
0x00, 0xFF, 0x09, 0x20, 0x95, 0x01, 0x81, 0x02, 0x05, 0x01, 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, 0x0F, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x0F, 0x81, 0x02, 0x06,
|
||||
0x00, 0xFF, 0x09, 0x21, 0x95, 0x0D, 0x81, 0x02, 0x06, 0x00, 0xFF, 0x09, 0x22, 0x15, 0x00, 0x26,
|
||||
0xFF, 0x00, 0x75, 0x08, 0x95, 0x34, 0x81, 0x02, 0x85, 0x02, 0x09, 0x23, 0x95, 0x2F, 0x91, 0x02,
|
||||
0x85, 0x05, 0x09, 0x33, 0x95, 0x28, 0xB1, 0x02, 0x85, 0x08, 0x09, 0x34, 0x95, 0x2F, 0xB1, 0x02,
|
||||
0x85, 0x09, 0x09, 0x24, 0x95, 0x13, 0xB1, 0x02, 0x85, 0x0A, 0x09, 0x25, 0x95, 0x1A, 0xB1, 0x02,
|
||||
0x85, 0x20, 0x09, 0x26, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x21, 0x09, 0x27, 0x95, 0x04, 0xB1, 0x02,
|
||||
0x85, 0x22, 0x09, 0x40, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x80, 0x09, 0x28, 0x95, 0x3F, 0xB1, 0x02,
|
||||
0x85, 0x81, 0x09, 0x29, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x82, 0x09, 0x2A, 0x95, 0x09, 0xB1, 0x02,
|
||||
0x85, 0x83, 0x09, 0x2B, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0x84, 0x09, 0x2C, 0x95, 0x3F, 0xB1, 0x02,
|
||||
0x85, 0x85, 0x09, 0x2D, 0x95, 0x02, 0xB1, 0x02, 0x85, 0xA0, 0x09, 0x2E, 0x95, 0x01, 0xB1, 0x02,
|
||||
0x85, 0xE0, 0x09, 0x2F, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF0, 0x09, 0x30, 0x95, 0x3F, 0xB1, 0x02,
|
||||
0x85, 0xF1, 0x09, 0x31, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF2, 0x09, 0x32, 0x95, 0x0F, 0xB1, 0x02,
|
||||
0x85, 0xF4, 0x09, 0x35, 0x95, 0x3F, 0xB1, 0x02, 0x85, 0xF5, 0x09, 0x36, 0x95, 0x03, 0xB1, 0x02,
|
||||
0xC0,
|
||||
];
|
||||
|
||||
// Feature reports hid-playstation / Steam read during init (each array's first byte is the report id).
|
||||
#[rustfmt::skip]
|
||||
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,
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
static DS_FEATURE_PAIRING: [u8; 20] = [ // 0x09 pairing info (MAC at 1..7)
|
||||
0x09, 0x74, 0xE7, 0xD6, 0x3A, 0x53, 0x35, 0x08, 0x25, 0x00, 0x1E, 0x00, 0xEE, 0x74, 0xD0, 0xBC,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
#[rustfmt::skip]
|
||||
static DS_FEATURE_FIRMWARE: [u8; 64] = [ // 0x20 firmware info
|
||||
0x20, 0x4A, 0x75, 0x6E, 0x20, 0x31, 0x39, 0x20, 0x32, 0x30, 0x32, 0x33, 0x31, 0x34, 0x3A, 0x34,
|
||||
0x37, 0x3A, 0x33, 0x34, 0x03, 0x00, 0x44, 0x00, 0x08, 0x02, 0x00, 0x01, 0x36, 0x00, 0x00, 0x01,
|
||||
0xC1, 0xC8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x01, 0x00, 0x00,
|
||||
0x14, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x01, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
|
||||
// ---- 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].
|
||||
// `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(&(if ds4 { DS4_PID } else { DS_PID }).to_le_bytes());
|
||||
a[8..10].copy_from_slice(&DS_VER.to_le_bytes());
|
||||
a
|
||||
}
|
||||
|
||||
// Neutral DualSense input report 0x01 (64 bytes): sticks centered (0x80), triggers 0, dpad neutral (8).
|
||||
const 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]=L2, r[6]=R2 = 0; r[7] = seq counter = 0
|
||||
r[8] = 0x08; // buttons[0]: low nibble = dpad hat (8 = neutral), high nibble = face buttons (0)
|
||||
r
|
||||
};
|
||||
// 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());
|
||||
/// The latest input report the host pushed (report `0x01`) via shared memory; the timer delivers it
|
||||
/// to pended game READ_REPORTs. Defaults to neutral until the host connects.
|
||||
static INPUT_REPORT: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new(NEUTRAL_REPORT);
|
||||
|
||||
// ---- user-mode shared-memory IPC with the punktfunk host ----
|
||||
// UMDF runs in WUDFHost.exe (user-mode) and hidclass blocks a control channel on the device stack
|
||||
// (custom interface CreateFile → err 31; custom IOCTL on the HID handle → err 1) and UMDF has no
|
||||
// control device, so the host channel is a named section the (privileged) host CREATES and the driver
|
||||
// OPENS. Layout (256 B): magic u32 @0 ("PFDS"), input_seq u32 @4, input_report[64] @8,
|
||||
// output_seq u32 @72, output_report[64] @76.
|
||||
const FILE_MAP_RW: u32 = 0x0002 | 0x0004; // FILE_MAP_WRITE | FILE_MAP_READ
|
||||
const SHM_MAGIC: u32 = 0x5046_4453; // "PFDS" little-endian
|
||||
const SHM_SIZE: usize = 256;
|
||||
static LOGGED_SHM: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
// kernel32 file-mapping APIs (resolved via std's kernel32 import; UMDF permits file mapping).
|
||||
unsafe extern "system" {
|
||||
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
|
||||
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
|
||||
fn UnmapViewOfFile(addr: *const c_void) -> i32;
|
||||
fn CloseHandle(h: *mut c_void) -> i32;
|
||||
}
|
||||
|
||||
fn log(s: &str) {
|
||||
if let Ok(c) = std::ffi::CString::new(s) {
|
||||
// SAFETY: c is a valid null-terminated string for the duration of the call.
|
||||
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
||||
}
|
||||
// Also append to a world-writable file — DebugView can't capture the UMDF host's output
|
||||
// across session 0, so this is how we read driver-start diagnostics.
|
||||
use std::io::Write;
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("C:\\Users\\Public\\pfds-driver.log")
|
||||
{
|
||||
let _ = writeln!(f, "{s}");
|
||||
}
|
||||
}
|
||||
macro_rules! dbglog { ($($a:tt)*) => { log(&format!($($a)*)) } }
|
||||
|
||||
#[unsafe(export_name = "DriverEntry")]
|
||||
pub unsafe extern "system" fn driver_entry(
|
||||
driver: PDRIVER_OBJECT,
|
||||
registry_path: PCUNICODE_STRING,
|
||||
) -> NTSTATUS {
|
||||
log("[pf-ds] DriverEntry");
|
||||
// SAFETY: zeroed WDF_DRIVER_CONFIG is a valid all-null config; we then set Size + the callback.
|
||||
let mut config: WDF_DRIVER_CONFIG = unsafe { core::mem::zeroed() };
|
||||
config.Size = core::mem::size_of::<WDF_DRIVER_CONFIG>() as ULONG;
|
||||
config.EvtDriverDeviceAdd = Some(evt_device_add);
|
||||
|
||||
// SAFETY: all pointers valid; driver/registry_path provided by the loader.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDriverCreate,
|
||||
driver,
|
||||
registry_path,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut config,
|
||||
WDF_NO_HANDLE.cast::<WDFDRIVER>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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");
|
||||
|
||||
// Mark as a filter (HID minidriver sits below mshidumdf.sys).
|
||||
// SAFETY: device_init is provided by the framework and non-null.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfFdoInitSetFilter, device_init) };
|
||||
|
||||
let mut device: WDFDEVICE = core::ptr::null_mut();
|
||||
// SAFETY: device_init valid; attributes allowed null; device receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceCreate,
|
||||
&mut device_init,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut device
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
dbglog!("[pf-ds] WdfDeviceCreate failed 0x{:08x}", st as u32);
|
||||
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() };
|
||||
qcfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
|
||||
qcfg.DispatchType = WdfIoQueueDispatchParallel;
|
||||
qcfg.PowerManaged = WdfUseDefault;
|
||||
qcfg.DefaultQueue = 1;
|
||||
qcfg.EvtIoDeviceControl = Some(evt_io_device_control);
|
||||
// WDF_IO_QUEUE_CONFIG_INIT sets this to (ULONG)-1 (unlimited); mem::zeroed left it 0,
|
||||
// which on a parallel queue means present ZERO requests → EvtIoDeviceControl never fires.
|
||||
qcfg.Settings.Parallel.NumberOfPresentedRequests = u32::MAX;
|
||||
let mut default_queue: WDFQUEUE = core::ptr::null_mut();
|
||||
// SAFETY: device + config valid; attributes null; queue receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfIoQueueCreate,
|
||||
device,
|
||||
&mut qcfg,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut default_queue
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
dbglog!(
|
||||
"[pf-ds] default WdfIoQueueCreate failed 0x{:08x}",
|
||||
st as u32
|
||||
);
|
||||
return st;
|
||||
}
|
||||
|
||||
// Manual queue: pended READ_REPORT requests are completed by the timer.
|
||||
// SAFETY: zeroed config then fields set.
|
||||
let mut mcfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
|
||||
mcfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
|
||||
mcfg.DispatchType = WdfIoQueueDispatchManual;
|
||||
mcfg.PowerManaged = WdfUseDefault;
|
||||
let mut manual_queue: WDFQUEUE = core::ptr::null_mut();
|
||||
// SAFETY: device + config valid; attributes null; queue receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfIoQueueCreate,
|
||||
device,
|
||||
&mut mcfg,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut manual_queue
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
dbglog!("[pf-ds] manual WdfIoQueueCreate failed 0x{:08x}", st as u32);
|
||||
return st;
|
||||
}
|
||||
MANUAL_QUEUE.store(manual_queue, Ordering::SeqCst);
|
||||
|
||||
// Periodic timer (parent = manual queue) completes pended reads with the neutral report.
|
||||
// SAFETY: zeroed config then fields set.
|
||||
let mut tcfg: WDF_TIMER_CONFIG = unsafe { core::mem::zeroed() };
|
||||
tcfg.Size = core::mem::size_of::<WDF_TIMER_CONFIG>() as ULONG;
|
||||
tcfg.EvtTimerFunc = Some(evt_timer);
|
||||
tcfg.Period = 8; // ms
|
||||
tcfg.AutomaticSerialization = 1; // TRUE — UMDF requires a serialized timer (vhidmini2 pattern)
|
||||
let mut tattr: WDF_OBJECT_ATTRIBUTES = unsafe { core::mem::zeroed() };
|
||||
tattr.Size = core::mem::size_of::<WDF_OBJECT_ATTRIBUTES>() as ULONG;
|
||||
tattr.ParentObject = manual_queue.cast();
|
||||
// mem::zeroed leaves these at 0 (Invalid) → set them like WDF_OBJECT_ATTRIBUTES_INIT
|
||||
// (matches the working vhidmini2 UMDF timer setup; avoids 0xc0200209 / 0xc00000bb).
|
||||
tattr.ExecutionLevel = WdfExecutionLevelInheritFromParent;
|
||||
tattr.SynchronizationScope = WdfSynchronizationScopeInheritFromParent;
|
||||
let mut timer: WDFTIMER = core::ptr::null_mut();
|
||||
// SAFETY: config + attributes valid; timer receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfTimerCreate, &mut tcfg, &mut tattr, &mut timer)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
dbglog!("[pf-ds] WdfTimerCreate failed 0x{:08x}", st as u32);
|
||||
return st;
|
||||
}
|
||||
// SAFETY: timer valid; -80000 == 8ms relative due time (100ns units, negative = relative).
|
||||
let _started = unsafe { call_unsafe_wdf_function_binding!(WdfTimerStart, timer, -80000i64) };
|
||||
|
||||
log("[pf-ds] device ready (DualSense 054C:0CE6)");
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
extern "C" fn evt_io_device_control(
|
||||
_queue: WDFQUEUE,
|
||||
request: WDFREQUEST,
|
||||
_output_len: usize,
|
||||
_input_len: usize,
|
||||
ioctl: ULONG,
|
||||
) {
|
||||
let mut complete = true;
|
||||
// Skip the 8ms READ_REPORT cadence so the log stays readable during a game test;
|
||||
// the 0x02 OUTPUT report (the gate) and the descriptor handshake still log.
|
||||
if ioctl != IOCTL_HID_READ_REPORT {
|
||||
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, 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.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestForwardToIoQueue, request, mq)
|
||||
};
|
||||
if nt_success(st) {
|
||||
complete = false;
|
||||
STATUS_SUCCESS
|
||||
} else {
|
||||
st
|
||||
}
|
||||
}
|
||||
IOCTL_HID_WRITE_REPORT | IOCTL_UMDF_HID_SET_OUTPUT_REPORT => {
|
||||
on_output_report(request, ioctl)
|
||||
}
|
||||
IOCTL_UMDF_HID_SET_FEATURE => {
|
||||
log("[pf-ds] SET_FEATURE (stub ok)");
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(request),
|
||||
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,
|
||||
};
|
||||
|
||||
if ioctl != IOCTL_HID_READ_REPORT {
|
||||
dbglog!(
|
||||
"[pf-ds] ioctl 0x{ioctl:08x} -> 0x{:08x} complete={complete}",
|
||||
status as u32
|
||||
);
|
||||
}
|
||||
if complete {
|
||||
// SAFETY: request valid and not forwarded.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, status) };
|
||||
}
|
||||
}
|
||||
|
||||
// Copy `src` into the request's output memory and set the completed byte count.
|
||||
fn copy_to_output(request: WDFREQUEST, src: &[u8]) -> NTSTATUS {
|
||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid; mem receives the memory handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut mem)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: mem valid; outlen receives the buffer size.
|
||||
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
||||
if outlen < src.len() {
|
||||
return STATUS_INVALID_BUFFER_SIZE;
|
||||
}
|
||||
// SAFETY: mem valid; src is a valid buffer of src.len() bytes.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfMemoryCopyFromBuffer,
|
||||
mem,
|
||||
0usize,
|
||||
src.as_ptr() as *mut c_void,
|
||||
src.len()
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
// SAFETY: request valid.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, src.len() as u64)
|
||||
};
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
// The 0x02 gate: a game writing an output report (rumble / lightbar / ADAPTIVE TRIGGERS). Per the
|
||||
// UMDF marshalling convention the report data is the *input* buffer and the report id is carried in
|
||||
// the *output* buffer length. We log it.
|
||||
fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> 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;
|
||||
|
||||
// report id from output-buffer length (UMDF convention).
|
||||
let mut report_id: u32 = 0;
|
||||
let mut outmem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid; output memory is optional here.
|
||||
if nt_success(unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut outmem)
|
||||
}) {
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: outmem valid.
|
||||
let _ =
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, outmem, &mut outlen) };
|
||||
report_id = outlen as u32;
|
||||
}
|
||||
|
||||
let n = inlen.min(48);
|
||||
let mut hex = String::new();
|
||||
if !inbuf.is_null() {
|
||||
// SAFETY: inbuf valid for inlen bytes; we read at most n.
|
||||
let bytes = unsafe { core::slice::from_raw_parts(inbuf, n) };
|
||||
for b in bytes {
|
||||
hex.push_str(&format!("{b:02x} "));
|
||||
}
|
||||
}
|
||||
let kind = if ioctl == IOCTL_HID_WRITE_REPORT {
|
||||
"WRITE_REPORT"
|
||||
} else {
|
||||
"SET_OUTPUT_REPORT"
|
||||
};
|
||||
dbglog!("[pf-ds] *** OUTPUT {kind} reportId={report_id} len={inlen} data: {hex}");
|
||||
|
||||
// Publish the game's 0x02 output report to shared memory for the host (rumble / lightbar /
|
||||
// player-LEDs / adaptive triggers). output_report @76, output_seq @72.
|
||||
if !inbuf.is_null() && inlen > 0 {
|
||||
let n = inlen.min(64);
|
||||
with_shm(|view| {
|
||||
// SAFETY: view is a mapped 256-byte section; write the report then bump the host-polled seq.
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(inbuf, view.add(76), n);
|
||||
let seqp = view.add(72) as *mut u32;
|
||||
let seq = core::ptr::read_unaligned(seqp).wrapping_add(1);
|
||||
core::ptr::write_unaligned(seqp, seq);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// SAFETY: request valid.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, inlen as u64) };
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
// GET_FEATURE: report id from the input buffer; reply with the matching DualSense feature blob.
|
||||
fn on_get_feature(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;
|
||||
if inbuf.is_null() || inlen < 1 {
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
}
|
||||
// SAFETY: inbuf valid for >=1 byte.
|
||||
let report_id = unsafe { *inbuf };
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
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> = format!("Global\\pfds-shm-{}", SHM_INDEX.load(Ordering::Relaxed))
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// SAFETY: name is a valid NUL-terminated UTF-16 string.
|
||||
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, name.as_ptr()) };
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: h is a valid mapping handle; map the whole section. The view keeps the section alive,
|
||||
// so the handle can be closed right away.
|
||||
let view = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, SHM_SIZE) } as *mut u8;
|
||||
unsafe { CloseHandle(h) };
|
||||
if view.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: view points at >= 4 mapped bytes.
|
||||
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-{})",
|
||||
SHM_INDEX.load(Ordering::Relaxed)
|
||||
);
|
||||
}
|
||||
f(view);
|
||||
}
|
||||
// SAFETY: view came from MapViewOfFile.
|
||||
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| {
|
||||
let mut buf = [0u8; 64];
|
||||
// SAFETY: view points at a mapped 256-byte section; input lives at offset 8..72.
|
||||
unsafe { core::ptr::copy_nonoverlapping(view.add(8), buf.as_mut_ptr(), 64) };
|
||||
if buf[0] == 0x01 {
|
||||
if let Ok(mut g) = INPUT_REPORT.lock() {
|
||||
*g = buf;
|
||||
}
|
||||
}
|
||||
});
|
||||
// SAFETY: timer valid; parent is the manual queue.
|
||||
let queue =
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfTimerGetParentObject, timer) } as WDFQUEUE;
|
||||
let mut request: WDFREQUEST = core::ptr::null_mut();
|
||||
// SAFETY: queue valid; request receives the next pended request if any.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfIoQueueRetrieveNextRequest, queue, &mut request)
|
||||
};
|
||||
if nt_success(st) {
|
||||
let report = INPUT_REPORT.lock().map(|g| *g).unwrap_or(NEUTRAL_REPORT);
|
||||
let s = copy_to_output(request, &report);
|
||||
// SAFETY: request valid and dequeued.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, s) };
|
||||
}
|
||||
let _ = STATUS_UNSUCCESSFUL; // keep the const referenced
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# pf-xusb - punktfunk virtual Xbox 360 XUSB companion (UMDF2, classic XInput).
|
||||
# A member of the in-tree drivers workspace (shares the vendored wdk-sys/wdk-build with the bindgen pin
|
||||
# + the crt-static .cargo/config), built from source per release like pf-vdisplay.
|
||||
[package]
|
||||
name = "pf-xusb"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
description = "punktfunk virtual Xbox 360 XUSB companion (UMDF2 - classic XInput)"
|
||||
|
||||
[package.metadata.wdk.driver-model]
|
||||
driver-type = "UMDF"
|
||||
umdf-version-major = 2
|
||||
target-umdf-version-minor = 31
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[build-dependencies]
|
||||
wdk-build.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wdk.workspace = true
|
||||
wdk-sys.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
nightly = ["wdk-sys/nightly", "wdk/nightly"]
|
||||
@@ -0,0 +1,79 @@
|
||||
# pf-xusb — virtual Xbox 360 XUSB companion (UMDF2, classic XInput)
|
||||
|
||||
A **pure-user-mode** UMDF2 driver that makes a virtual Xbox 360 controller visible to classic
|
||||
**`XInputGetState`** with **no kernel bus driver** (no ViGEmBus) — the HIDMaestro approach. It is the
|
||||
Windows counterpart to ViGEm's X360 target, owned in-tree.
|
||||
|
||||
## Why this is not the HID driver
|
||||
|
||||
XInput does **not** use HID. `xinput1_4.dll` enumerates the **XUSB device-interface GUID**
|
||||
`{EC87F1E3-C13B-4100-B5F7-8B84D54260CB}` (`SetupDiEnumDeviceInterfaces`), opens the Nth present
|
||||
instance (= player slot 0–3) with `CreateFile`, and polls it with buffered IOCTLs. So this driver:
|
||||
|
||||
- is **not** a HID minidriver (no `MsHidUmdf`) — it's a plain UMDF2 function driver under `WUDFRd`,
|
||||
**System** setup class;
|
||||
- registers the XUSB interface with `WdfDeviceCreateDeviceInterface(device, &XUSB_GUID, NULL)`;
|
||||
- answers the XUSB IOCTLs (all `METHOD_BUFFERED`, delivered to user mode by the reflector) from
|
||||
controller state the host publishes into a shared section `Global\pfxusb-shm-0`; a game's rumble
|
||||
(`SET_STATE`) is published back for the host to forward to the client.
|
||||
|
||||
The WAIT_* IOCTLs return `STATUS_INVALID_DEVICE_REQUEST`, which makes `xinput1_4` fall back to
|
||||
synchronous `GET_STATE` polling — so no manual queue / timer is needed for classic XInput. (WGI/
|
||||
GameInput admission additionally needs a `xinputhid` `UpperFilters` registry tripwire + the async
|
||||
`WAIT_FOR_INPUT` pump — not implemented; classic XInput does not need it.)
|
||||
|
||||
## Verified wire formats (source: HIDMaestro `driver/companion.c`, nefarius/XInputHooker `XUSB.h`, ViGEm)
|
||||
|
||||
| IOCTL | Code | Reply |
|
||||
| --- | --- | --- |
|
||||
| `GET_INFORMATION` | `0x80006000` | 12 B: `[0]`=ver `0x0103`, `[2]`=count `0x01`, `[8]`=VID `045E`, `[10]`=PID `028E` — marks the slot **connected** |
|
||||
| `GET_CAPABILITIES` | `0x8000E004` | 24 B (or 36 B V2 if `outLen>=36`): Type `0x03`/SubType `0x01`, motor max `0xFFFF` (advertise rumble) |
|
||||
| `GET_STATE` | `0x8000E00C` | **29 B**: `[0]`ver `[2]`count `[5]`u32 packet# `[0x0B]`u16 wButtons `[0x0D]`LT `[0x0E]`RT `[0x0F..0x16]`4×i16 sticks |
|
||||
| `SET_STATE` | `0x8000A010` | input 5 B `{00, led, large, small, subcmd}`: `subcmd 0x02`=rumble (large `[2]`, small `[3]`), `0x01`=player-LED |
|
||||
| `GET_LED_STATE` | `0x8000E008` | `{0,0,0x06}` |
|
||||
| `GET_BATTERY_INFORMATION` | `0x8000E018` | `{0,0x01,0x03,0}` |
|
||||
| `WAIT_GUIDE_BUTTON` / `WAIT_FOR_INPUT` | `0x8000E014` / `0x8000E3AC` | `STATUS_INVALID_DEVICE_REQUEST` → GET_STATE fallback |
|
||||
|
||||
`wButtons` is the `XINPUT_GAMEPAD_*` bitmap (DPAD_UP `0x0001` … A `0x1000` B `0x2000` X `0x4000`
|
||||
Y `0x8000`). `dwPacketNumber` (GET_STATE `[5]`) must increment whenever the payload changes.
|
||||
|
||||
## Shared-memory layout `Global\pfxusb-shm-0` (64 B) — host writes state, driver writes rumble
|
||||
|
||||
`magic u32 @0` (`"PFXU"` `0x55584650`) · `packet u32 @4` (host bumps → dwPacketNumber) · `wButtons u16
|
||||
@8` · `LT @10` · `RT @11` · `LX/LY/RX/RY i16 @12/@14/@16/@18` · `rumble_seq u32 @24` (driver bumps) ·
|
||||
`large @28` · `small @29`.
|
||||
|
||||
## Validated live on `.173` (2026-06-22)
|
||||
|
||||
`XInputGetState(0)` returns **CONNECTED** with the pushed buttons/sticks and an incrementing
|
||||
`dwPacketNumber`; `XInputSetState(0xC000, 0x4000)` reaches the driver as `00 00 c0 40 02` → host sees
|
||||
`large=192 small=64`. Test tools: `C:\Users\Public\giprobe\xusbtest.exe` (creates the `pf_xusb`
|
||||
devnode + cycling state via shm) and `xinputtest.exe` (`XInputGetState`/`SetState` harness).
|
||||
|
||||
## Build / sign / install (same recipe as the DualSense driver)
|
||||
|
||||
Built from `C:\Users\Public\m0\windows-drivers-rs\examples\pf-xusb` (the `../../crates` paths resolve
|
||||
there); these repo files are the canonical copies — keep them in sync.
|
||||
|
||||
1. `cargo make` (env `LIBCLANG_PATH`, `Version_Number=10.0.26100.0`) → `target\debug\pf_xusb_package\`.
|
||||
2. Clear the FORCE_INTEGRITY PE bit (bit `0x80` at `e_lfanew+0x5e` of `pf_xusb.dll`).
|
||||
3. `signtool sign /fd SHA256 /sha1 6A52984E54376C45A1C236B1A2C8A746C5AB6131 pf_xusb.dll`.
|
||||
4. `Inf2Cat /driver:<pkg> /os:10_X64` → re-sign `pf_xusb.cat` with the same thumbprint.
|
||||
5. `pnputil /add-driver pf_xusb.inf` (no `/install`; the host SwDeviceCreate's `pf_xusb` per session).
|
||||
|
||||
## Host integration (done)
|
||||
|
||||
`crates/punktfunk-host/src/inject/gamepad_windows.rs` is the Windows `GamepadManager` (used by
|
||||
`PadBackend::Xbox360`): it SwDeviceCreate's the `pf_xusb` companion, maps `pfxusb-shm-<index>`, writes
|
||||
the XInput state from the client's gamepad frame (already XInput-convention) and forwards rumble. There
|
||||
is **no ViGEmBus dependency** anymore. The driver is vendored + pnputil-installed by the Inno Setup
|
||||
installer (`packaging/windows/gamepad-drivers/` + `install-gamepad-drivers.ps1`).
|
||||
|
||||
## Multi-pad
|
||||
|
||||
The host stamps each pad's index into the device Location (`pszDeviceLocation`); the driver reads it
|
||||
via `WdfDeviceAllocAndQueryProperty(DevicePropertyLocationInformation)` in EvtDeviceAdd and maps its own
|
||||
`pfxusb-shm-<index>`. `UmdfHostProcessSharing=ProcessSharingDisabled` (the INF) gives each pad its own
|
||||
WUDFHost, so the per-pad `SHM_INDEX` static doesn't collide. Validated live: two pads → two distinct
|
||||
XInput slots. (XInput assigns the player slot 0-3 by interface-enumeration order, independent of this
|
||||
index — which only routes shared memory.)
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Build script for the `pf-xusb` UMDF driver — provides Cargo the WDK linker flags.
|
||||
|
||||
fn main() -> Result<(), wdk_build::ConfigError> {
|
||||
wdk_build::configure_wdk_binary_build()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
;/*++
|
||||
; punktfunk virtual Xbox 360 XUSB companion — a non-HID UMDF2 driver that registers the XUSB
|
||||
; device-interface GUID {EC87F1E3-...} and answers the buffered XInput IOCTLs, so classic
|
||||
; XInputGetState() reads the pad without a kernel bus driver (the HIDMaestro approach). System class,
|
||||
; hosted by the in-box WUDFRd reflector. Created per-session by the host via SwDeviceCreate
|
||||
; (hardware id `pf_xusb`); `root\pf_xusb` is the devgen/devcon test id.
|
||||
;--*/
|
||||
[Version]
|
||||
Signature = "$WINDOWS NT$"
|
||||
Class = System
|
||||
ClassGuid = {4D36E97D-E325-11CE-BFC1-08002BE10318}
|
||||
Provider = %ProviderString%
|
||||
CatalogFile = pf_xusb.cat
|
||||
PnpLockdown = 1
|
||||
|
||||
[DestinationDirs]
|
||||
DefaultDestDir = 13
|
||||
|
||||
[SourceDisksNames]
|
||||
1 = %DiskId1%,,,""
|
||||
|
||||
[SourceDisksFiles]
|
||||
pf_xusb.dll = 1,,
|
||||
|
||||
[Manufacturer]
|
||||
%StdMfg%=Standard, NT$ARCH$.10.0...22000
|
||||
|
||||
[Standard.NT$ARCH$.10.0...22000]
|
||||
%DeviceDesc%=pfXusb, root\pf_xusb, pf_xusb
|
||||
|
||||
[pfXusb.NT]
|
||||
CopyFiles=Drivers_Dir
|
||||
Include=WUDFRD.inf
|
||||
Needs=WUDFRD.NT
|
||||
|
||||
[Drivers_Dir]
|
||||
pf_xusb.dll
|
||||
|
||||
[pfXusb.NT.HW]
|
||||
Include=WUDFRD.inf
|
||||
Needs=WUDFRD.NT.HW
|
||||
|
||||
[pfXusb.NT.Services]
|
||||
Include=WUDFRD.inf
|
||||
Needs=WUDFRD.NT.Services
|
||||
|
||||
[pfXusb.NT.Wdf]
|
||||
UmdfService=pf_xusb, pfXusb_Install
|
||||
UmdfServiceOrder=pf_xusb
|
||||
UmdfKernelModeClientPolicy=AllowKernelModeClients
|
||||
UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects
|
||||
UmdfMethodNeitherAction=Copy
|
||||
UmdfFsContextUsePolicy=CanUseFsContext2
|
||||
UmdfHostProcessSharing=ProcessSharingDisabled
|
||||
|
||||
[pfXusb_Install]
|
||||
UmdfLibraryVersion=$UMDFVERSION$
|
||||
ServiceBinary=%13%\pf_xusb.dll
|
||||
|
||||
[Strings]
|
||||
ProviderString = "punktfunk"
|
||||
StdMfg = "(Standard system devices)"
|
||||
DiskId1 = "punktfunk XUSB Installation Disk"
|
||||
DeviceDesc = "punktfunk Virtual Xbox 360 (XUSB)"
|
||||
@@ -0,0 +1,462 @@
|
||||
// punktfunk virtual Xbox 360 XUSB companion — UMDF2 driver presenting the XUSB device interface so
|
||||
// classic XInput (XInputGetState) reads the pad with no kernel bus driver (the HIDMaestro approach).
|
||||
//
|
||||
// xinput1_4.dll enumerates GUID_DEVINTERFACE_XUSB, opens the Nth instance (= player slot), and polls
|
||||
// it with buffered IOCTLs. We register the interface and answer those IOCTLs from controller state the
|
||||
// host publishes into a shared-memory section (`Global\pfxusb-shm-0`); a game's rumble (SET_STATE) is
|
||||
// published back for the host to forward. Byte formats are the source-verified xusb22 wire layout
|
||||
// (HIDMaestro driver/companion.c + nefarius/XInputHooker XUSB.h + ViGEm XUSB_REPORT).
|
||||
//
|
||||
// We answer the WAIT_* IOCTLs with STATUS_INVALID_DEVICE_REQUEST, which makes xinput1_4 fall back to
|
||||
// synchronous GET_STATE polling — so no manual queue / timer is needed for classic XInput.
|
||||
|
||||
#![allow(non_snake_case, non_upper_case_globals, clippy::missing_safety_doc)]
|
||||
|
||||
use core::ffi::c_void;
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
use wdk_sys::{
|
||||
call_unsafe_wdf_function_binding, windows::OutputDebugStringA, GUID, NTSTATUS, PCUNICODE_STRING,
|
||||
PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFREQUEST,
|
||||
WDF_DRIVER_CONFIG, WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES,
|
||||
};
|
||||
|
||||
// DEVICE_REGISTRY_PROPERTY: DevicePropertyLocationInformation (the const isn't re-exported at the
|
||||
// wdk_sys root; the value is stable WDM).
|
||||
const DEVICE_PROPERTY_LOCATION_INFORMATION: i32 = 10;
|
||||
|
||||
/// The pad index this device serves (which `pfxusb-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);
|
||||
|
||||
// ---- NTSTATUS ----
|
||||
const STATUS_SUCCESS: NTSTATUS = 0;
|
||||
const STATUS_INVALID_DEVICE_REQUEST: NTSTATUS = 0xC000_0010u32 as NTSTATUS;
|
||||
const STATUS_INVALID_BUFFER_SIZE: NTSTATUS = 0xC000_0206u32 as NTSTATUS;
|
||||
|
||||
#[inline]
|
||||
fn nt_success(s: NTSTATUS) -> bool {
|
||||
s >= 0
|
||||
}
|
||||
|
||||
// GUID_DEVINTERFACE_XUSB {EC87F1E3-C13B-4100-B5F7-8B84D54260CB} — what xinput1_4 enumerates + opens.
|
||||
const GUID_DEVINTERFACE_XUSB: GUID = GUID {
|
||||
Data1: 0xEC87_F1E3,
|
||||
Data2: 0xC13B,
|
||||
Data3: 0x4100,
|
||||
Data4: [0xB5, 0xF7, 0x8B, 0x84, 0xD5, 0x42, 0x60, 0xCB],
|
||||
};
|
||||
|
||||
// ---- XUSB IOCTLs (METHOD_BUFFERED) ----
|
||||
const IOCTL_XUSB_GET_INFORMATION: u32 = 0x8000_6000;
|
||||
const IOCTL_XUSB_GET_CAPABILITIES: u32 = 0x8000_E004;
|
||||
const IOCTL_XUSB_GET_LED_STATE: u32 = 0x8000_E008;
|
||||
const IOCTL_XUSB_GET_STATE: u32 = 0x8000_E00C;
|
||||
const IOCTL_XUSB_SET_STATE: u32 = 0x8000_A010;
|
||||
const IOCTL_XUSB_WAIT_GUIDE_BUTTON: u32 = 0x8000_E014;
|
||||
const IOCTL_XUSB_GET_BATTERY_INFORMATION: u32 = 0x8000_E018;
|
||||
const IOCTL_XUSB_POWER_DOWN: u32 = 0x8000_A01C;
|
||||
const IOCTL_XUSB_GET_XINPUT_MANAGEMENT_DRIVER: u32 = 0x8000_6380;
|
||||
const IOCTL_XUSB_WAIT_FOR_INPUT: u32 = 0x8000_E3AC;
|
||||
const IOCTL_XUSB_GET_INFORMATION_EX: u32 = 0x8000_E3FC;
|
||||
|
||||
// Xbox 360 wired identity (what GET_INFORMATION reports). 0x0103 unblocks SET_STATE (vibration).
|
||||
const XUSB_VID: u16 = 0x045E;
|
||||
const XUSB_PID: u16 = 0x028E;
|
||||
const XUSB_VERSION: u16 = 0x0103;
|
||||
|
||||
// ---- WDF enum values ----
|
||||
const WdfIoQueueDispatchParallel: i32 = 2;
|
||||
const WdfUseDefault: i32 = 2; // WDF_TRI_STATE
|
||||
|
||||
// ---- shared-memory layout (host ↔ driver), must match the host's xbox_xusb_windows backend ----
|
||||
// magic u32 @0 ("PFXU"); packet u32 @4 (host bumps on state change → dwPacketNumber); the XUSB_REPORT
|
||||
// payload @8: wButtons u16 @8, bLeftTrigger @10, bRightTrigger @11, sThumbLX i16 @12, LY @14, RX @16,
|
||||
// RY @18; rumble_seq u32 @24 (driver bumps on SET_STATE); rumble large @28, small @29.
|
||||
const FILE_MAP_RW: u32 = 0x0002 | 0x0004;
|
||||
const SHM_MAGIC: u32 = 0x5558_4650; // "PFXU" little-endian
|
||||
const SHM_SIZE: usize = 64;
|
||||
|
||||
unsafe extern "system" {
|
||||
fn OpenFileMappingW(access: u32, inherit: i32, name: *const u16) -> *mut c_void;
|
||||
fn MapViewOfFile(h: *mut c_void, access: u32, hi: u32, lo: u32, len: usize) -> *mut c_void;
|
||||
fn UnmapViewOfFile(addr: *const c_void) -> i32;
|
||||
fn CloseHandle(h: *mut c_void) -> i32;
|
||||
}
|
||||
|
||||
fn log(s: &str) {
|
||||
if let Ok(c) = std::ffi::CString::new(s) {
|
||||
// SAFETY: c is a valid null-terminated string for the duration of the call.
|
||||
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
||||
}
|
||||
use std::io::Write;
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("C:\\Users\\Public\\pfxusb-driver.log")
|
||||
{
|
||||
let _ = writeln!(f, "{s}");
|
||||
}
|
||||
}
|
||||
macro_rules! dbglog { ($($a:tt)*) => { log(&format!($($a)*)) } }
|
||||
|
||||
#[unsafe(export_name = "DriverEntry")]
|
||||
pub unsafe extern "system" fn driver_entry(
|
||||
driver: PDRIVER_OBJECT,
|
||||
registry_path: PCUNICODE_STRING,
|
||||
) -> NTSTATUS {
|
||||
log("[pf-xusb] DriverEntry");
|
||||
// SAFETY: zeroed config then Size + callback set.
|
||||
let mut config: WDF_DRIVER_CONFIG = unsafe { core::mem::zeroed() };
|
||||
config.Size = core::mem::size_of::<WDF_DRIVER_CONFIG>() as ULONG;
|
||||
config.EvtDriverDeviceAdd = Some(evt_device_add);
|
||||
// SAFETY: all pointers valid; provided by the loader.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDriverCreate,
|
||||
driver,
|
||||
registry_path,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut config,
|
||||
WDF_NO_HANDLE.cast::<WDFDRIVER>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the pad index the host stamped into the device Location (`pszDeviceLocation`), 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-xusb] EvtDeviceAdd");
|
||||
|
||||
let mut device: WDFDEVICE = core::ptr::null_mut();
|
||||
// SAFETY: device_init valid; attributes null; device receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceCreate,
|
||||
&mut device_init,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut device
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
dbglog!("[pf-xusb] WdfDeviceCreate failed 0x{:08x}", st as u32);
|
||||
return st;
|
||||
}
|
||||
|
||||
let idx = query_shm_index(device);
|
||||
SHM_INDEX.store(idx, Ordering::Relaxed);
|
||||
dbglog!("[pf-xusb] shm index = {idx}");
|
||||
|
||||
// Register the XUSB device interface (no reference string) — what xinput1_4 enumerates + opens.
|
||||
// SAFETY: device valid; GUID static; null reference string.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfDeviceCreateDeviceInterface,
|
||||
device,
|
||||
&GUID_DEVINTERFACE_XUSB,
|
||||
core::ptr::null()
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
dbglog!(
|
||||
"[pf-xusb] WdfDeviceCreateDeviceInterface failed 0x{:08x}",
|
||||
st as u32
|
||||
);
|
||||
return st;
|
||||
}
|
||||
|
||||
// Default parallel queue: all the XUSB IOCTLs land here.
|
||||
// SAFETY: zeroed config then fields set; Size matches the struct.
|
||||
let mut qcfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
|
||||
qcfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
|
||||
qcfg.DispatchType = WdfIoQueueDispatchParallel;
|
||||
qcfg.PowerManaged = WdfUseDefault;
|
||||
qcfg.DefaultQueue = 1;
|
||||
qcfg.EvtIoDeviceControl = Some(evt_io_device_control);
|
||||
qcfg.Settings.Parallel.NumberOfPresentedRequests = u32::MAX;
|
||||
let mut queue: WDFQUEUE = core::ptr::null_mut();
|
||||
// SAFETY: device + config valid; attributes null; queue receives the handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfIoQueueCreate,
|
||||
device,
|
||||
&mut qcfg,
|
||||
WDF_NO_OBJECT_ATTRIBUTES,
|
||||
&mut queue
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
dbglog!("[pf-xusb] WdfIoQueueCreate failed 0x{:08x}", st as u32);
|
||||
return st;
|
||||
}
|
||||
|
||||
log("[pf-xusb] device ready (XUSB interface registered)");
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
// Open + map the host's shared section and run `f` against the mapped base if magic is valid, then
|
||||
// unmap. Re-mapped per access (the host may recreate the section across restarts).
|
||||
fn with_shm<F: FnOnce(*mut u8)>(f: F) {
|
||||
let name: Vec<u16> = format!("Global\\pfxusb-shm-{}", SHM_INDEX.load(Ordering::Relaxed))
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
// SAFETY: name is a valid NUL-terminated UTF-16 string.
|
||||
let h = unsafe { OpenFileMappingW(FILE_MAP_RW, 0, name.as_ptr()) };
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: h is a valid mapping handle; map the whole section; the view keeps it alive.
|
||||
let view = unsafe { MapViewOfFile(h, FILE_MAP_RW, 0, 0, SHM_SIZE) } as *mut u8;
|
||||
unsafe { CloseHandle(h) };
|
||||
if view.is_null() {
|
||||
return;
|
||||
}
|
||||
// SAFETY: view points at >= 4 mapped bytes.
|
||||
let magic = unsafe { core::ptr::read_unaligned(view as *const u32) };
|
||||
if magic == SHM_MAGIC {
|
||||
f(view);
|
||||
}
|
||||
// SAFETY: view came from MapViewOfFile.
|
||||
unsafe { UnmapViewOfFile(view as *const c_void) };
|
||||
}
|
||||
|
||||
/// The current controller state from shared memory (zeros / neutral if the host hasn't connected).
|
||||
/// Returns `(dwPacketNumber, wButtons, lt, rt, lx, ly, rx, ry)`.
|
||||
fn read_state() -> (u32, u16, u8, u8, i16, i16, i16, i16) {
|
||||
let mut out = (0u32, 0u16, 0u8, 0u8, 0i16, 0i16, 0i16, 0i16);
|
||||
with_shm(|v| {
|
||||
// SAFETY: v points at a mapped SHM_SIZE section with valid magic.
|
||||
unsafe {
|
||||
out.0 = core::ptr::read_unaligned(v.add(4) as *const u32);
|
||||
out.1 = core::ptr::read_unaligned(v.add(8) as *const u16);
|
||||
out.2 = *v.add(10);
|
||||
out.3 = *v.add(11);
|
||||
out.4 = core::ptr::read_unaligned(v.add(12) as *const i16);
|
||||
out.5 = core::ptr::read_unaligned(v.add(14) as *const i16);
|
||||
out.6 = core::ptr::read_unaligned(v.add(16) as *const i16);
|
||||
out.7 = core::ptr::read_unaligned(v.add(18) as *const i16);
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Publish a game's rumble (from SET_STATE) into shared memory for the host to forward.
|
||||
fn publish_rumble(large: u8, small: u8) {
|
||||
with_shm(|v| {
|
||||
// SAFETY: v points at a mapped SHM_SIZE section; rumble_seq @24, large @28, small @29.
|
||||
unsafe {
|
||||
*v.add(28) = large;
|
||||
*v.add(29) = small;
|
||||
let seqp = v.add(24) as *mut u32;
|
||||
let seq = core::ptr::read_unaligned(seqp).wrapping_add(1);
|
||||
core::ptr::write_unaligned(seqp, seq);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Build the 29-byte GET_STATE buffer (the layout xinput1_4 parses).
|
||||
fn build_get_state() -> [u8; 29] {
|
||||
let (packet, buttons, lt, rt, lx, ly, rx, ry) = read_state();
|
||||
let mut s = [0u8; 29];
|
||||
s[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
||||
s[2] = 0x01; // device count
|
||||
s[5..9].copy_from_slice(&packet.to_le_bytes());
|
||||
s[0x0B..0x0D].copy_from_slice(&buttons.to_le_bytes());
|
||||
s[0x0D] = lt;
|
||||
s[0x0E] = rt;
|
||||
s[0x0F..0x11].copy_from_slice(&lx.to_le_bytes());
|
||||
s[0x11..0x13].copy_from_slice(&ly.to_le_bytes());
|
||||
s[0x13..0x15].copy_from_slice(&rx.to_le_bytes());
|
||||
s[0x15..0x17].copy_from_slice(&ry.to_le_bytes());
|
||||
s
|
||||
}
|
||||
|
||||
// GET_INFORMATION: 12 bytes — version, device count, VID/PID. Marks the slot connected.
|
||||
fn build_information() -> [u8; 12] {
|
||||
let mut info = [0u8; 12];
|
||||
info[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
||||
info[2] = 0x01; // one device/port
|
||||
info[8..10].copy_from_slice(&XUSB_VID.to_le_bytes());
|
||||
info[10..12].copy_from_slice(&XUSB_PID.to_le_bytes());
|
||||
info
|
||||
}
|
||||
|
||||
// GET_CAPABILITIES V1 (24 bytes): Type=0x03 SubType=0x01 (gamepad), button/stick masks, motor max
|
||||
// = 0xFFFF (advertise rumble). The V2 (36-byte) form prepends a 16-byte header when WGI asks for 36.
|
||||
#[rustfmt::skip]
|
||||
const CAPS_V1: [u8; 24] = [
|
||||
0x03, 0x01, 0x00, 0x01, 0xFF, 0xF7, 0xFF, 0xFF,
|
||||
0xC0, 0xFF, 0xC0, 0xFF, 0xC0, 0xFF, 0xC0, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF,
|
||||
];
|
||||
|
||||
fn build_caps_v2() -> [u8; 36] {
|
||||
let mut c = [0u8; 36];
|
||||
c[0..6].copy_from_slice(&[0x03, 0x01, 0x01, 0x01, 0x0C, 0x00]);
|
||||
c[6..8].copy_from_slice(&XUSB_VID.to_le_bytes());
|
||||
c[8..10].copy_from_slice(&XUSB_PID.to_le_bytes());
|
||||
c[10..16].copy_from_slice(&[0x10, 0x01, 0x00, 0xFA, 0x34, 0x22]);
|
||||
c[16..36].copy_from_slice(&CAPS_V1[4..24]); // the XINPUT_CAPABILITIES struct body
|
||||
c
|
||||
}
|
||||
|
||||
extern "C" fn evt_io_device_control(
|
||||
_queue: WDFQUEUE,
|
||||
request: WDFREQUEST,
|
||||
output_len: usize,
|
||||
input_len: usize,
|
||||
ioctl: ULONG,
|
||||
) {
|
||||
let status: NTSTATUS = match ioctl {
|
||||
IOCTL_XUSB_GET_INFORMATION => copy_to_output(request, &build_information()),
|
||||
IOCTL_XUSB_GET_INFORMATION_EX => {
|
||||
let mut ex = [0u8; 64];
|
||||
ex[0..2].copy_from_slice(&XUSB_VERSION.to_le_bytes());
|
||||
ex[2] = 0x01;
|
||||
ex[3] = 0x01;
|
||||
ex[8..10].copy_from_slice(&XUSB_VID.to_le_bytes());
|
||||
ex[10..12].copy_from_slice(&XUSB_PID.to_le_bytes());
|
||||
let n = output_len.min(64);
|
||||
copy_to_output(request, &ex[..n])
|
||||
}
|
||||
IOCTL_XUSB_GET_CAPABILITIES => {
|
||||
if output_len >= 36 {
|
||||
copy_to_output(request, &build_caps_v2())
|
||||
} else {
|
||||
copy_to_output(request, &CAPS_V1)
|
||||
}
|
||||
}
|
||||
IOCTL_XUSB_GET_STATE => copy_to_output(request, &build_get_state()),
|
||||
IOCTL_XUSB_GET_LED_STATE => copy_to_output(request, &[0x00, 0x00, 0x06]),
|
||||
IOCTL_XUSB_GET_BATTERY_INFORMATION => {
|
||||
copy_to_output(request, &[0x00, 0x01, 0x03, 0x00])
|
||||
}
|
||||
IOCTL_XUSB_SET_STATE => on_set_state(request),
|
||||
IOCTL_XUSB_POWER_DOWN | IOCTL_XUSB_GET_XINPUT_MANAGEMENT_DRIVER => STATUS_SUCCESS,
|
||||
// Decline the async waits → xinput1_4 falls back to synchronous GET_STATE polling.
|
||||
IOCTL_XUSB_WAIT_GUIDE_BUTTON | IOCTL_XUSB_WAIT_FOR_INPUT => STATUS_INVALID_DEVICE_REQUEST,
|
||||
other => {
|
||||
dbglog!("[pf-xusb] unhandled IOCTL 0x{other:08x} in={input_len} out={output_len}");
|
||||
STATUS_INVALID_DEVICE_REQUEST
|
||||
}
|
||||
};
|
||||
// SAFETY: request valid and not forwarded.
|
||||
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, status) };
|
||||
}
|
||||
|
||||
// SET_STATE: the rumble packet. Classic xusb22 layout is small; the motor bytes sit near the end.
|
||||
// We publish a best-effort (large = byte 3, small = byte 4 for the 5-byte form) and log the raw bytes
|
||||
// so the exact offsets can be confirmed against a real pad.
|
||||
fn on_set_state(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) {
|
||||
let mut len: usize = 0;
|
||||
// SAFETY: inmem valid.
|
||||
let p = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut len) }
|
||||
as *const u8;
|
||||
if !p.is_null() && len >= 2 {
|
||||
let n = len.min(8);
|
||||
// SAFETY: p valid for len bytes; read at most n.
|
||||
let bytes = unsafe { core::slice::from_raw_parts(p, n) };
|
||||
let mut hex = String::new();
|
||||
for b in bytes {
|
||||
hex.push_str(&format!("{b:02x} "));
|
||||
}
|
||||
dbglog!("[pf-xusb] SET_STATE len={len} data: {hex}");
|
||||
// Observed 5-byte form {00, led, largeMotor, smallMotor, subcmd}: subcmd 0x02 = rumble
|
||||
// (large/low-freq at [2], small/high-freq at [3]); 0x01 = player-LED set (ignored).
|
||||
// 4-byte = raw XINPUT_VIBRATION → the two motor hi bytes.
|
||||
if len >= 5 && bytes[4] == 0x02 {
|
||||
publish_rumble(bytes[2], bytes[3]);
|
||||
} else if len == 4 {
|
||||
publish_rumble(bytes[1], bytes[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
|
||||
// Copy `src` into the request's (buffered) output buffer and set the completed byte count.
|
||||
fn copy_to_output(request: WDFREQUEST, src: &[u8]) -> NTSTATUS {
|
||||
let mut mem: WDFMEMORY = core::ptr::null_mut();
|
||||
// SAFETY: request valid; mem receives the memory handle.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestRetrieveOutputMemory, request, &mut mem)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
let mut outlen: usize = 0;
|
||||
// SAFETY: mem valid; outlen receives the buffer size.
|
||||
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, mem, &mut outlen) };
|
||||
if outlen < src.len() {
|
||||
return STATUS_INVALID_BUFFER_SIZE;
|
||||
}
|
||||
// SAFETY: mem valid; src is a valid buffer of src.len() bytes.
|
||||
let st = unsafe {
|
||||
call_unsafe_wdf_function_binding!(
|
||||
WdfMemoryCopyFromBuffer,
|
||||
mem,
|
||||
0usize,
|
||||
src.as_ptr() as *mut c_void,
|
||||
src.len()
|
||||
)
|
||||
};
|
||||
if !nt_success(st) {
|
||||
return st;
|
||||
}
|
||||
// SAFETY: request valid.
|
||||
unsafe {
|
||||
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, src.len() as u64)
|
||||
};
|
||||
STATUS_SUCCESS
|
||||
}
|
||||
Reference in New Issue
Block a user