From 01dc0b616c2f8ed3f2cfc00f30ab42398c0e677a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sun, 21 Jun 2026 21:34:00 +0000 Subject: [PATCH] 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 --- .../windows/dualsense-driver/pf_dualsense.inx | 5 +- packaging/windows/dualsense-driver/src/lib.rs | 223 +++--------------- 2 files changed, 35 insertions(+), 193 deletions(-) diff --git a/packaging/windows/dualsense-driver/pf_dualsense.inx b/packaging/windows/dualsense-driver/pf_dualsense.inx index c979082..36615bc 100644 --- a/packaging/windows/dualsense-driver/pf_dualsense.inx +++ b/packaging/windows/dualsense-driver/pf_dualsense.inx @@ -25,7 +25,10 @@ pf_dualsense.dll=1 %ManufacturerString%=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] CopyFiles=UMDriverCopy diff --git a/packaging/windows/dualsense-driver/src/lib.rs b/packaging/windows/dualsense-driver/src/lib.rs index ee0496f..925401c 100644 --- a/packaging/windows/dualsense-driver/src/lib.rs +++ b/packaging/windows/dualsense-driver/src/lib.rs @@ -14,10 +14,10 @@ use core::ffi::c_void; use core::sync::atomic::{AtomicPtr, Ordering}; use wdk_sys::{ - call_unsafe_wdf_function_binding, windows::OutputDebugStringA, GUID, NTSTATUS, PCUNICODE_STRING, - PDRIVER_OBJECT, PWDFDEVICE_INIT, ULONG, WDFDEVICE, WDFDRIVER, WDFMEMORY, WDFQUEUE, WDFQUEUE__, - WDFREQUEST, WDFTIMER, WDF_DRIVER_CONFIG, WDF_IO_QUEUE_CONFIG, WDF_NO_HANDLE, - WDF_NO_OBJECT_ATTRIBUTES, WDF_OBJECT_ATTRIBUTES, WDF_TIMER_CONFIG, + 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 ---- @@ -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_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 ---- const WdfIoQueueDispatchParallel: i32 = 2; const WdfIoQueueDispatchManual: i32 = 3; @@ -67,15 +58,6 @@ const DS_VID: u16 = 0x054C; const DS_PID: u16 = 0x0CE6; 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). // NOTE: inject/dualsense.rs comments this as "232 bytes" — that comment is wrong; it is 273. #[rustfmt::skip] @@ -151,16 +133,9 @@ fn neutral_report() -> [u8; 64] { } static MANUAL_QUEUE: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); -/// Manual queue of pended host `IOCTL_PFDS_GET_OUTPUT` requests (inverted-call); completed with a -/// game's `0x02` output report as it arrives. -static OUTPUT_QUEUE: AtomicPtr = 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. +/// 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); -/// 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 ---- // 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) { - dbglog!("[pf-ds] default WdfIoQueueCreate failed 0x{:08x}", st as u32); + dbglog!( + "[pf-ds] default WdfIoQueueCreate failed 0x{:08x}", + st as u32 + ); 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). 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::() 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::() 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)"); STATUS_SUCCESS } @@ -376,10 +313,7 @@ extern "C" fn evt_io_device_control( 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 !matches!( - ioctl, - IOCTL_HID_READ_REPORT | IOCTL_PFDS_SET_INPUT | IOCTL_PFDS_GET_OUTPUT - ) { + if ioctl != IOCTL_HID_READ_REPORT { dbglog!("[pf-ds] ioctl 0x{ioctl:08x} out={_output_len} in={_input_len}"); } let status: NTSTATUS = match ioctl { @@ -399,42 +333,23 @@ extern "C" fn evt_io_device_control( 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 => { 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()), - // ---- 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, }; - if !matches!( - ioctl, - IOCTL_HID_READ_REPORT | IOCTL_PFDS_SET_INPUT | IOCTL_PFDS_GET_OUTPUT - ) { - dbglog!("[pf-ds] ioctl 0x{ioctl:08x} -> 0x{:08x} complete={complete}", status as u32); + 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. @@ -472,7 +387,9 @@ fn copy_to_output(request: WDFREQUEST, src: &[u8]) -> NTSTATUS { return st; } // 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 } @@ -490,9 +407,8 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS { } let mut inlen: usize = 0; // SAFETY: inmem valid. - let inbuf = - unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) } - as *const u8; + 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; @@ -503,7 +419,8 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS { }) { let mut outlen: usize = 0; // 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; } @@ -521,20 +438,10 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS { } else { "SET_OUTPUT_REPORT" }; - dbglog!( - "[pf-ds] *** OUTPUT {kind} reportId={report_id} len={inlen} data: {hex}" - ); + dbglog!("[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 - // rumble / lightbar / player-LEDs / adaptive-trigger feedback to the client. - 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. + // 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| { @@ -553,73 +460,6 @@ fn on_output_report(request: WDFREQUEST, ioctl: ULONG) -> NTSTATUS { 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. fn on_get_feature(request: WDFREQUEST) -> NTSTATUS { let mut inmem: WDFMEMORY = core::ptr::null_mut(); @@ -632,9 +472,8 @@ fn on_get_feature(request: WDFREQUEST) -> NTSTATUS { } let mut inlen: usize = 0; // SAFETY: inmem valid. - let inbuf = - unsafe { call_unsafe_wdf_function_binding!(WdfMemoryGetBuffer, inmem, &mut inlen) } - as *const u8; + 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; }