refactor(windows): trim the inert IOCTL channel from the DualSense driver

The host<->driver channel is the shared-memory section (hidclass blocks the device
stack and UMDF has no control device), so the first-attempt in-driver IOCTL channel
never fired. Remove it: the custom device interface, IOCTL_PFDS_SET_INPUT/GET_OUTPUT,
the output queue, and the on_set_input/complete_one_read/deliver_output helpers. The
driver keeps the HID handshake, the 8ms read timer fed from the shared section, and
on_output_report publishing the game's 0x02 to the section. Rebuilt + reloaded + the
channel still verifies both directions live on the RTX box.

Also list `pf_dualsense` as a second hardware id (alongside `root\pf_dualsense`) so the
host's SwDeviceCreate'd software device binds the same driver as a devgen one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:34:00 +00:00
parent 4a73102d48
commit 01dc0b616c
2 changed files with 35 additions and 193 deletions
@@ -25,7 +25,10 @@ pf_dualsense.dll=1
%ManufacturerString%=pf, NT$ARCH$.10.0...22000 %ManufacturerString%=pf, NT$ARCH$.10.0...22000
[pf.NT$ARCH$.10.0...22000] [pf.NT$ARCH$.10.0...22000]
%DeviceDesc%=pfDualSense, root\pf_dualsense ; Two hardware ids: `root\pf_dualsense` for a root-enumerated devnode (devgen/devcon tests) and
; `pf_dualsense` for the host's SwDeviceCreate'd software device (the `root\` prefix is reserved for
; root enumeration, so SwDeviceCreate rejects it with E_INVALIDARG).
%DeviceDesc%=pfDualSense, root\pf_dualsense, pf_dualsense
[pfDualSense.NT] [pfDualSense.NT]
CopyFiles=UMDriverCopy CopyFiles=UMDriverCopy
+31 -192
View File
@@ -14,10 +14,10 @@ use core::ffi::c_void;
use core::sync::atomic::{AtomicPtr, Ordering}; use core::sync::atomic::{AtomicPtr, Ordering};
use wdk_sys::{ use wdk_sys::{
call_unsafe_wdf_function_binding, windows::OutputDebugStringA, GUID, NTSTATUS, PCUNICODE_STRING, NTSTATUS, PCUNICODE_STRING, PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDF_DRIVER_CONFIG,
PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFQUEUE__, WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES,
WDFREQUEST, WDFTIMER, WDF_DRIVER_CONFIG, WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, WDF_TIMER_CONFIG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFQUEUE__, WDFREQUEST, WDFTIMER,
WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES, WDF_TIMER_CONFIG, call_unsafe_wdf_function_binding, windows::OutputDebugStringA,
}; };
// ---- NTSTATUS values ---- // ---- NTSTATUS values ----
@@ -46,15 +46,6 @@ 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_SET_OUTPUT_REPORT: u32 = hid_ctl(22);
const IOCTL_UMDF_HID_GET_INPUT_REPORT: u32 = hid_ctl(23); const IOCTL_UMDF_HID_GET_INPUT_REPORT: u32 = hid_ctl(23);
// ---- Host control channel: CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, fn, METHOD_BUFFERED=0, access) ----
const fn pfds_ctl(func: u32, access: u32) -> u32 {
(0x0000_0022 << 16) | (access << 14) | (func << 2)
}
/// Host → driver: push the 64-byte `0x01` input report (FILE_WRITE_ACCESS).
const IOCTL_PFDS_SET_INPUT: u32 = pfds_ctl(0x800, 2);
/// Driver → host inverted-call: completed with a game's raw `0x02` output report (FILE_READ_ACCESS).
const IOCTL_PFDS_GET_OUTPUT: u32 = pfds_ctl(0x801, 1);
// ---- WDF enum values ---- // ---- WDF enum values ----
const WdfIoQueueDispatchParallel: i32 = 2; const WdfIoQueueDispatchParallel: i32 = 2;
const WdfIoQueueDispatchManual: i32 = 3; const WdfIoQueueDispatchManual: i32 = 3;
@@ -67,15 +58,6 @@ const DS_VID: u16 = 0x054C;
const DS_PID: u16 = 0x0CE6; const DS_PID: u16 = 0x0CE6;
const DS_VER: u16 = 0x0100; const DS_VER: u16 = 0x0100;
// {7B2F8E4A-9C3D-4E1F-A6B5-1234567890AB} — the host↔driver control interface the punktfunk host
// opens (on the SwDeviceCreate'd device) to push input reports + pull a game's output reports.
const PFDS_CONTROL_GUID: GUID = GUID {
Data1: 0x7b2f_8e4a,
Data2: 0x9c3d,
Data3: 0x4e1f,
Data4: [0xa6, 0xb5, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab],
};
// Sony DualSense USB HID report descriptor (273 bytes), verbatim from inputtino (== inject/dualsense.rs). // 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. // NOTE: inject/dualsense.rs comments this as "232 bytes" — that comment is wrong; it is 273.
#[rustfmt::skip] #[rustfmt::skip]
@@ -151,16 +133,9 @@ fn neutral_report() -> [u8; 64] {
} }
static MANUAL_QUEUE: AtomicPtr<WDFQUEUE__> = AtomicPtr::new(core::ptr::null_mut()); static MANUAL_QUEUE: AtomicPtr<WDFQUEUE__> = AtomicPtr::new(core::ptr::null_mut());
/// Manual queue of pended host `IOCTL_PFDS_GET_OUTPUT` requests (inverted-call); completed with a /// The latest input report the host pushed (report `0x01`) via shared memory; the timer delivers it
/// game's `0x02` output report as it arrives. /// to pended game READ_REPORTs. Defaults to neutral until the host connects.
static OUTPUT_QUEUE: AtomicPtr<WDFQUEUE__> = AtomicPtr::new(core::ptr::null_mut());
/// The latest input report the host pushed (report `0x01`); the timer + each SET_INPUT deliver 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); static INPUT_REPORT: std::sync::Mutex<[u8; 64]> = std::sync::Mutex::new(NEUTRAL_REPORT);
/// One-shot logs so the control channel's first traffic is visible without per-frame spam.
static LOGGED_SET_INPUT: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
static LOGGED_GET_OUTPUT: core::sync::atomic::AtomicBool =
core::sync::atomic::AtomicBool::new(false);
// ---- user-mode shared-memory IPC with the punktfunk host ---- // ---- 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 // UMDF runs in WUDFHost.exe (user-mode) and hidclass blocks a control channel on the device stack
@@ -268,7 +243,10 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
) )
}; };
if !nt_success(st) { if !nt_success(st) {
dbglog!("[pf-ds] default WdfIoQueueCreate failed 0x{:08x}", st as u32); dbglog!(
"[pf-ds] default WdfIoQueueCreate failed 0x{:08x}",
st as u32
);
return st; return st;
} }
@@ -321,47 +299,6 @@ extern "C" fn evt_device_add(_driver: WDFDRIVER, mut device_init: PWDFDEVICE_INI
// SAFETY: timer valid; -80000 == 8ms relative due time (100ns units, negative = relative). // SAFETY: timer valid; -80000 == 8ms relative due time (100ns units, negative = relative).
let _started = unsafe { call_unsafe_wdf_function_binding!(WdfTimerStart, timer, -80000i64) }; let _started = unsafe { call_unsafe_wdf_function_binding!(WdfTimerStart, timer, -80000i64) };
// Output queue: pended host GET_OUTPUT (inverted-call) requests, completed as games write 0x02.
// SAFETY: zeroed config then fields set.
let mut ocfg: WDF_IO_QUEUE_CONFIG = unsafe { core::mem::zeroed() };
ocfg.Size = core::mem::size_of::<WDF_IO_QUEUE_CONFIG>() as ULONG;
ocfg.DispatchType = WdfIoQueueDispatchManual;
ocfg.PowerManaged = WdfUseDefault;
let mut output_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 ocfg,
WDF_NO_OBJECT_ATTRIBUTES,
&mut output_queue
)
};
if !nt_success(st) {
dbglog!("[pf-ds] output WdfIoQueueCreate failed 0x{:08x}", st as u32);
return st;
}
OUTPUT_QUEUE.store(output_queue, Ordering::SeqCst);
// Host↔driver control interface — the punktfunk host opens this to push input + pull output.
// Non-fatal if it fails: the HID device still works for direct-app use, just not the host plane.
// SAFETY: device valid; GUID is a valid static; the reference string is optional (null).
let st = unsafe {
call_unsafe_wdf_function_binding!(
WdfDeviceCreateDeviceInterface,
device,
&PFDS_CONTROL_GUID,
core::ptr::null::<c_void>() as PCUNICODE_STRING
)
};
if !nt_success(st) {
dbglog!(
"[pf-ds] WdfDeviceCreateDeviceInterface failed 0x{:08x}",
st as u32
);
}
log("[pf-ds] device ready (DualSense 054C:0CE6)"); log("[pf-ds] device ready (DualSense 054C:0CE6)");
STATUS_SUCCESS STATUS_SUCCESS
} }
@@ -376,10 +313,7 @@ extern "C" fn evt_io_device_control(
let mut complete = true; let mut complete = true;
// Skip the 8ms READ_REPORT cadence so the log stays readable during a game test; // 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. // the 0x02 OUTPUT report (the gate) and the descriptor handshake still log.
if !matches!( if ioctl != IOCTL_HID_READ_REPORT {
ioctl,
IOCTL_HID_READ_REPORT | IOCTL_PFDS_SET_INPUT | IOCTL_PFDS_GET_OUTPUT
) {
dbglog!("[pf-ds] ioctl 0x{ioctl:08x} out={_output_len} in={_input_len}"); dbglog!("[pf-ds] ioctl 0x{ioctl:08x} out={_output_len} in={_input_len}");
} }
let status: NTSTATUS = match ioctl { let status: NTSTATUS = match ioctl {
@@ -399,42 +333,23 @@ extern "C" fn evt_io_device_control(
st st
} }
} }
IOCTL_HID_WRITE_REPORT | IOCTL_UMDF_HID_SET_OUTPUT_REPORT => on_output_report(request, ioctl), IOCTL_HID_WRITE_REPORT | IOCTL_UMDF_HID_SET_OUTPUT_REPORT => {
on_output_report(request, ioctl)
}
IOCTL_UMDF_HID_SET_FEATURE => { IOCTL_UMDF_HID_SET_FEATURE => {
log("[pf-ds] SET_FEATURE (stub ok)"); log("[pf-ds] SET_FEATURE (stub ok)");
STATUS_SUCCESS STATUS_SUCCESS
} }
IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(request), IOCTL_UMDF_HID_GET_FEATURE => on_get_feature(request),
IOCTL_UMDF_HID_GET_INPUT_REPORT => copy_to_output(request, &neutral_report()), IOCTL_UMDF_HID_GET_INPUT_REPORT => copy_to_output(request, &neutral_report()),
// ---- host control channel ----
IOCTL_PFDS_SET_INPUT => on_set_input(request),
IOCTL_PFDS_GET_OUTPUT => {
let oq: WDFQUEUE = OUTPUT_QUEUE.load(Ordering::SeqCst);
// SAFETY: request valid; oq is the output manual queue created in EvtDeviceAdd.
let st = unsafe {
call_unsafe_wdf_function_binding!(WdfRequestForwardToIoQueue, request, oq)
};
if !LOGGED_GET_OUTPUT.swap(true, Ordering::Relaxed) {
dbglog!(
"[pf-ds] control: first GET_OUTPUT posted (host pump up) st=0x{:08x}",
st as u32
);
}
if nt_success(st) {
complete = false;
STATUS_SUCCESS
} else {
st
}
}
_ => STATUS_NOT_IMPLEMENTED, _ => STATUS_NOT_IMPLEMENTED,
}; };
if !matches!( if ioctl != IOCTL_HID_READ_REPORT {
ioctl, dbglog!(
IOCTL_HID_READ_REPORT | IOCTL_PFDS_SET_INPUT | IOCTL_PFDS_GET_OUTPUT "[pf-ds] ioctl 0x{ioctl:08x} -> 0x{:08x} complete={complete}",
) { status as u32
dbglog!("[pf-ds] ioctl 0x{ioctl:08x} -> 0x{:08x} complete={complete}", status as u32); );
} }
if complete { if complete {
// SAFETY: request valid and not forwarded. // SAFETY: request valid and not forwarded.
@@ -472,7 +387,9 @@ fn copy_to_output(request: WDFREQUEST, src: &[u8]) -> NTSTATUS {
return st; return st;
} }
// SAFETY: request valid. // SAFETY: request valid.
unsafe { call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, src.len() as u64) }; unsafe {
call_unsafe_wdf_function_binding!(WdfRequestSetInformation, request, src.len() as u64)
};
STATUS_SUCCESS STATUS_SUCCESS
} }
@@ -490,9 +407,8 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS {
} }
let mut inlen: usize = 0; let mut inlen: usize = 0;
// SAFETY: inmem valid. // SAFETY: inmem valid.
let inbuf = let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) } as *const u8;
as *const u8;
// report id from output-buffer length (UMDF convention). // report id from output-buffer length (UMDF convention).
let mut report_id: u32 = 0; let mut report_id: u32 = 0;
@@ -503,7 +419,8 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS {
}) { }) {
let mut outlen: usize = 0; let mut outlen: usize = 0;
// SAFETY: outmem valid. // SAFETY: outmem valid.
let _ = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, outmem, &mut outlen) }; let _ =
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, outmem, &mut outlen) };
report_id = outlen as u32; report_id = outlen as u32;
} }
@@ -521,20 +438,10 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS {
} else { } else {
"SET_OUTPUT_REPORT" "SET_OUTPUT_REPORT"
}; };
dbglog!( dbglog!("[pf-ds] *** OUTPUT {kind} reportId={report_id} len={inlen} data: {hex}");
"[pf-ds] *** OUTPUT {kind} reportId={report_id} len={inlen} data: {hex}"
);
// Forward the raw report to a pended host GET_OUTPUT request so the punktfunk host can relay // Publish the game's 0x02 output report to shared memory for the host (rumble / lightbar /
// rumble / lightbar / player-LEDs / adaptive-trigger feedback to the client. // player-LEDs / adaptive triggers). output_report @76, output_seq @72.
if !inbuf.is_null() && inlen > 0 {
// SAFETY: inbuf valid for inlen bytes; cap the copy at 64.
let report = unsafe { core::slice::from_raw_parts(inbuf, inlen.min(64)) };
deliver_output(report);
}
// Publish to shared memory for the host — the real feedback channel (the IOCTL path above is
// inert under hidclass). output_report @76, output_seq @72.
if !inbuf.is_null() && inlen > 0 { if !inbuf.is_null() && inlen > 0 {
let n = inlen.min(64); let n = inlen.min(64);
with_shm(|view| { with_shm(|view| {
@@ -553,73 +460,6 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS {
STATUS_SUCCESS STATUS_SUCCESS
} }
// Host → driver: store the pushed `0x01` input report and deliver it to a pending game READ_REPORT.
fn on_set_input(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 == 0 {
return STATUS_INVALID_PARAMETER;
}
let n = inlen.min(64);
if let Ok(mut guard) = INPUT_REPORT.lock() {
// SAFETY: inbuf valid for inlen >= n bytes.
let src = unsafe { core::slice::from_raw_parts(inbuf, n) };
guard[..n].copy_from_slice(src);
}
if !LOGGED_SET_INPUT.swap(true, Ordering::Relaxed) {
dbglog!("[pf-ds] control: first SET_INPUT ({inlen} bytes) — host input plane up");
}
complete_one_read();
STATUS_SUCCESS
}
// Pull one pended game READ_REPORT and complete it with the current input report.
fn complete_one_read() {
let queue: WDFQUEUE = MANUAL_QUEUE.load(Ordering::SeqCst);
if queue.is_null() {
return;
}
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) };
}
}
// Deliver a game's raw `0x02` output report to a pended host GET_OUTPUT request (if one is posted).
fn deliver_output(data: &[u8]) {
let oq: WDFQUEUE = OUTPUT_QUEUE.load(Ordering::SeqCst);
if oq.is_null() {
return;
}
let mut request: WDFREQUEST = core::ptr::null_mut();
// SAFETY: oq valid; request receives the next pended request if any.
let st = unsafe {
call_unsafe_wdf_function_binding!(WdfIoQueueRetrieveNextRequest, oq, &mut request)
};
if nt_success(st) {
let s = copy_to_output(request, data);
// SAFETY: request valid and dequeued.
unsafe { call_unsafe_wdf_function_binding!(WdfRequestComplete, request, s) };
}
}
// GET_FEATURE: report id from the input buffer; reply with the matching DualSense feature blob. // GET_FEATURE: report id from the input buffer; reply with the matching DualSense feature blob.
fn on_get_feature(request: WDFREQUEST) -> NTSTATUS { fn on_get_feature(request: WDFREQUEST) -> NTSTATUS {
let mut inmem: WDFMEMORY = core::ptr::null_mut(); let mut inmem: WDFMEMORY = core::ptr::null_mut();
@@ -632,9 +472,8 @@ fn on_get_feature(request: WDFREQUEST) -> NTSTATUS {
} }
let mut inlen: usize = 0; let mut inlen: usize = 0;
// SAFETY: inmem valid. // SAFETY: inmem valid.
let inbuf = let inbuf = unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) }
unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) } as *const u8;
as *const u8;
if inbuf.is_null() || inlen < 1 { if inbuf.is_null() || inlen < 1 {
return STATUS_INVALID_PARAMETER; return STATUS_INVALID_PARAMETER;
} }