efb1ba26d7
Two disk-write fixes: - pf-xusb/pf-dualsense no longer write C:\Users\Public\pf*-driver.log unconditionally — the file log is now opt-in (debug builds, or the PFXUSB_DEBUG_LOG / PFDS_DEBUG_LOG system env var), mirroring the audit-§4.4 fix pf-vdisplay already got: a release driver never writes the world-writable Public file (info-leak/DoS surface), and the per-report OUTPUT/SET_STATE hex dumps stop being a sustained per-rumble disk-write path during gameplay. OutputDebugStringA stays unconditional; the host's driver-silence WARN and the gamepad-driver-health failure-mode table now say the log is opt-in. - service.log/host.log get one-generation rotation: at each (re)open a file over 10 MB is renamed to .old, so a crash-restart loop or a RUST_LOG=debug left in host.env can't grow the append-forever logs without bound. Rotation runs only before an open (never under a live appender — host.log's handle lacks FILE_SHARE_DELETE, so a racing rename harmlessly fails). Windows CI compile/clippy pending (drivers workspace + host are not Linux-cross-checkable); rides along with the next pad-driver redeploy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
424 lines
18 KiB
Rust
424 lines
18 KiB
Rust
// 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 DATA section; 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).
|
|
//
|
|
// The host channel is the **sealed pad channel** (design/gamepad-channel-sealing.md, proto v2): the
|
|
// DATA section (`pf_driver_proto::gamepad::XusbShm`) is UNNAMED — we reach it only through a handle
|
|
// the SYSTEM host duplicated into this WUDFHost, bootstrapped over the named `Global\pfxusb-boot-<i>`
|
|
// mailbox. The whole handshake + all shared-memory access lives in `pf_umdf_util` (audited unsafe
|
|
// layer): this crate's channel/IOCTL/state logic is 100% SAFE Rust. The only `unsafe` here is the
|
|
// unavoidable WDF setup FFI in DriverEntry/EvtDeviceAdd, each with a `// SAFETY:` proof.
|
|
//
|
|
// 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)]
|
|
// Every remaining `unsafe {}` (all WDF setup FFI) must carry a `// SAFETY:` proof.
|
|
#![deny(unsafe_op_in_unsafe_fn)]
|
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
|
|
|
use pf_driver_proto::gamepad::XusbShm;
|
|
use pf_umdf_util::channel::{ChannelClient, ChannelConfig};
|
|
use pf_umdf_util::nt_success;
|
|
use pf_umdf_util::section::MappedView;
|
|
use pf_umdf_util::wdf::{self, Request};
|
|
use wdk_sys::{
|
|
GUID, NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
|
|
WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDFDEVICE, WDFDRIVER, WDFQUEUE,
|
|
WDFREQUEST, call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
|
|
};
|
|
|
|
// ---- NTSTATUS ----
|
|
const STATUS_SUCCESS: NTSTATUS = 0;
|
|
const STATUS_INVALID_DEVICE_REQUEST: NTSTATUS = 0xC000_0010u32 as NTSTATUS;
|
|
|
|
// 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
|
|
|
|
// ---- the sealed host channel: layouts + offsets from pf_driver_proto (drift = compile error) ----
|
|
const SHM_MAGIC: u32 = pf_driver_proto::gamepad::XUSB_MAGIC; // "PFXU"
|
|
const SHM_SIZE: usize = core::mem::size_of::<XusbShm>();
|
|
const GAMEPAD_PROTO_VERSION: u32 = pf_driver_proto::gamepad::GAMEPAD_PROTO_VERSION;
|
|
|
|
// XusbShm field offsets (host writes state, we answer XInput; we write rumble + health marks).
|
|
const OFF_PACKET: usize = core::mem::offset_of!(XusbShm, packet);
|
|
const OFF_BUTTONS: usize = core::mem::offset_of!(XusbShm, buttons);
|
|
const OFF_LT: usize = core::mem::offset_of!(XusbShm, left_trigger);
|
|
const OFF_RT: usize = core::mem::offset_of!(XusbShm, right_trigger);
|
|
const OFF_LX: usize = core::mem::offset_of!(XusbShm, thumb_lx);
|
|
const OFF_LY: usize = core::mem::offset_of!(XusbShm, thumb_ly);
|
|
const OFF_RX: usize = core::mem::offset_of!(XusbShm, thumb_rx);
|
|
const OFF_RY: usize = core::mem::offset_of!(XusbShm, thumb_ry);
|
|
const OFF_RUMBLE_SEQ: usize = core::mem::offset_of!(XusbShm, rumble_seq);
|
|
const OFF_RUMBLE_LARGE: usize = core::mem::offset_of!(XusbShm, rumble_large);
|
|
const OFF_RUMBLE_SMALL: usize = core::mem::offset_of!(XusbShm, rumble_small);
|
|
const OFF_DRIVER_PROTO: usize = core::mem::offset_of!(XusbShm, driver_proto);
|
|
const OFF_DRIVER_HEARTBEAT: usize = core::mem::offset_of!(XusbShm, driver_heartbeat);
|
|
const OFF_PAD_INDEX: usize = core::mem::offset_of!(XusbShm, pad_index);
|
|
|
|
/// The sealed-channel client (per-pad: `ProcessSharingDisabled` gives each pad its own WUDFHost, so
|
|
/// this static is per-pad). All shared-memory access + the bootstrap handshake live in `pf_umdf_util`.
|
|
static CHANNEL: ChannelClient = ChannelClient::new();
|
|
|
|
/// This pad's channel config (magic/size/pad_index offset + our logger).
|
|
fn channel_cfg() -> ChannelConfig {
|
|
ChannelConfig {
|
|
tag: "pf-xusb",
|
|
boot_name_prefix: "Global\\pfxusb-boot-",
|
|
data_magic: SHM_MAGIC,
|
|
data_size: SHM_SIZE,
|
|
pad_index_off: OFF_PAD_INDEX,
|
|
log,
|
|
}
|
|
}
|
|
|
|
/// Whether the world-writable bring-up file log is enabled (resolved once). OPT-IN — debug builds,
|
|
/// or the `PFXUSB_DEBUG_LOG` (system-wide) env var — the same treatment pf-vdisplay got in audit
|
|
/// §4.4: a RELEASE driver never writes the Public file (info-leak/DoS surface), and the per-rumble
|
|
/// SET_STATE hex dumps stop being a sustained disk-write path during gameplay. DebugView can't see
|
|
/// the UMDF host across session 0, so the file stays the bring-up diagnostic when enabled.
|
|
fn file_log_enabled() -> bool {
|
|
use std::sync::OnceLock;
|
|
static ON: OnceLock<bool> = OnceLock::new();
|
|
*ON.get_or_init(|| cfg!(debug_assertions) || std::env::var_os("PFXUSB_DEBUG_LOG").is_some())
|
|
}
|
|
|
|
/// Process-lifetime append handle to the bring-up log, opened ONCE and shared via a `Mutex`
|
|
/// (pf-vdisplay's pattern) — no per-line open/close.
|
|
fn file_appender() -> Option<&'static std::sync::Mutex<std::fs::File>> {
|
|
use std::sync::OnceLock;
|
|
static APPENDER: OnceLock<Option<std::sync::Mutex<std::fs::File>>> = OnceLock::new();
|
|
APPENDER
|
|
.get_or_init(|| {
|
|
if !file_log_enabled() {
|
|
return None;
|
|
}
|
|
std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open("C:\\Users\\Public\\pfxusb-driver.log")
|
|
.ok()
|
|
.map(std::sync::Mutex::new)
|
|
})
|
|
.as_ref()
|
|
}
|
|
|
|
fn log(s: &str) {
|
|
if let Ok(c) = std::ffi::CString::new(s) {
|
|
// SAFETY: `c` is a valid NUL-terminated string for the duration of the call.
|
|
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
|
}
|
|
use std::io::Write;
|
|
if let Some(m) = file_appender()
|
|
&& let Ok(mut f) = m.lock()
|
|
{
|
|
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: a 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: `driver`/`registry_path` are the loader-provided pointers; the config is valid.
|
|
unsafe {
|
|
call_unsafe_wdf_function_binding!(
|
|
WdfDriverCreate,
|
|
driver,
|
|
registry_path,
|
|
WDF_NO_OBJECT_ATTRIBUTES,
|
|
&mut config,
|
|
WDF_NO_HANDLE.cast::<WDFDRIVER>()
|
|
)
|
|
}
|
|
}
|
|
|
|
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` is the framework-provided init; attributes null; `device` receives it.
|
|
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;
|
|
}
|
|
|
|
// SAFETY: `device` is the live device just created — the exact contract `query_location_index`
|
|
// requires.
|
|
let idx = unsafe { wdf::query_location_index(device) };
|
|
CHANNEL.set_index(idx);
|
|
dbglog!("[pf-xusb] shm index = {idx}");
|
|
|
|
// Register the XUSB device interface (no reference string) — what xinput1_4 enumerates + opens.
|
|
// SAFETY: `device` is live; the GUID is a 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: a zeroed WDF_IO_QUEUE_CONFIG is valid; we then set Size + the fields we use.
|
|
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` + `qcfg` are 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;
|
|
}
|
|
|
|
// Run the sealed-channel handshake on a worker (must NOT block EvtDeviceAdd): publish our pid in
|
|
// the bootstrap mailbox and poll for the host's delivered DATA handle, so the pad attaches (and
|
|
// the host's driver-attach health check goes green) even before any game polls XInput. Bounded;
|
|
// a later host (or a re-delivery) is still picked up by the per-IOCTL pump. This closure is 100%
|
|
// safe — the whole channel state machine lives in pf_umdf_util.
|
|
std::thread::spawn(|| {
|
|
let cfg = channel_cfg();
|
|
for _ in 0..500 {
|
|
if let Some(v) = CHANNEL.pump(&cfg) {
|
|
touch_driver_marks(v);
|
|
return;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
}
|
|
log(
|
|
"[pf-xusb] no sealed-channel delivery within 10s (host absent, or host/driver version mismatch — see above)",
|
|
);
|
|
});
|
|
|
|
log("[pf-xusb] device ready (XUSB interface registered)");
|
|
STATUS_SUCCESS
|
|
}
|
|
|
|
/// The current controller state from the attached DATA section (zeros / neutral when unattached).
|
|
/// Returns `(dwPacketNumber, wButtons, lt, rt, lx, ly, rx, ry)`.
|
|
fn read_state(data: Option<&MappedView>) -> (u32, u16, u8, u8, i16, i16, i16, i16) {
|
|
match data {
|
|
Some(v) => (
|
|
v.read_u32(OFF_PACKET),
|
|
v.read_u16(OFF_BUTTONS),
|
|
v.read_u8(OFF_LT),
|
|
v.read_u8(OFF_RT),
|
|
v.read_i16(OFF_LX),
|
|
v.read_i16(OFF_LY),
|
|
v.read_i16(OFF_RX),
|
|
v.read_i16(OFF_RY),
|
|
),
|
|
None => (0, 0, 0, 0, 0, 0, 0, 0),
|
|
}
|
|
}
|
|
|
|
/// Stamp the driver health marks the host watches: `driver_proto` (the attach signal, idempotent)
|
|
/// and `driver_heartbeat` (+1). Called once the channel attaches and on every serviced IOCTL, so the
|
|
/// host can tell "driver bound and alive" apart from "driver package missing/failed to bind" and see
|
|
/// the game-visible polling path advance.
|
|
fn touch_driver_marks(data: &MappedView) {
|
|
data.write_u32(OFF_DRIVER_PROTO, GAMEPAD_PROTO_VERSION);
|
|
let hb = data.read_u32(OFF_DRIVER_HEARTBEAT).wrapping_add(1);
|
|
data.write_u32(OFF_DRIVER_HEARTBEAT, hb);
|
|
}
|
|
|
|
/// Publish a game's rumble (from SET_STATE) into the DATA section for the host to forward.
|
|
fn publish_rumble(data: Option<&MappedView>, large: u8, small: u8) {
|
|
let Some(v) = data else { return };
|
|
v.write_u8(OFF_RUMBLE_LARGE, large);
|
|
v.write_u8(OFF_RUMBLE_SMALL, small);
|
|
let seq = v.read_u32(OFF_RUMBLE_SEQ).wrapping_add(1);
|
|
v.write_u32(OFF_RUMBLE_SEQ, seq);
|
|
}
|
|
|
|
// Build the 29-byte GET_STATE buffer (the layout xinput1_4 parses).
|
|
fn build_get_state(data: Option<&MappedView>) -> [u8; 29] {
|
|
let (packet, buttons, lt, rt, lx, ly, rx, ry) = read_state(data);
|
|
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,
|
|
) {
|
|
// SAFETY: `request` is the live request for THIS EvtIoDeviceControl invocation — exactly the
|
|
// contract `Request::new` requires. From here everything is safe (the token owns completion).
|
|
let request = unsafe { Request::new(request) };
|
|
|
|
// Sealed-channel pump + health marks first: adopt a (late) delivery, detach when the host's
|
|
// mailbox is gone, and stamp the attach/heartbeat marks the host watches (also covers a host
|
|
// started after this device — the pump attaches on the next XInput poll).
|
|
let data = CHANNEL.pump(&channel_cfg());
|
|
if let Some(v) = data {
|
|
touch_driver_marks(v);
|
|
}
|
|
|
|
let status: NTSTATUS = match ioctl {
|
|
IOCTL_XUSB_GET_INFORMATION => request.copy_to_output(&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);
|
|
request.copy_to_output(&ex[..n])
|
|
}
|
|
IOCTL_XUSB_GET_CAPABILITIES => {
|
|
if output_len >= 36 {
|
|
request.copy_to_output(&build_caps_v2())
|
|
} else {
|
|
request.copy_to_output(&CAPS_V1)
|
|
}
|
|
}
|
|
IOCTL_XUSB_GET_STATE => request.copy_to_output(&build_get_state(data)),
|
|
IOCTL_XUSB_GET_LED_STATE => request.copy_to_output(&[0x00, 0x00, 0x06]),
|
|
IOCTL_XUSB_GET_BATTERY_INFORMATION => request.copy_to_output(&[0x00, 0x01, 0x03, 0x00]),
|
|
IOCTL_XUSB_SET_STATE => on_set_state(&request, data),
|
|
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
|
|
}
|
|
};
|
|
request.complete(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 2, small = byte 3 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: &Request, data: Option<&MappedView>) -> NTSTATUS {
|
|
if let Ok((bytes, len)) = request.input_bytes(8)
|
|
&& len >= 2
|
|
{
|
|
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(data, bytes[2], bytes[3]);
|
|
} else if len == 4 {
|
|
publish_rumble(data, bytes[1], bytes[3]);
|
|
}
|
|
}
|
|
STATUS_SUCCESS
|
|
}
|