fix(gamepad): working per-session SwDeviceCreate for the Windows DualSense
create_swdevice now succeeds. The two requirements (each E_INVALIDARG otherwise): the
enumerator name must have no underscore (use "punktfunk"), and the completion callback is
mandatory (the docs mark pCallback [in], not optional -- NULL is rejected). Back on the
typed windows-rs SwDeviceCreate (a raw-FFI diagnosis confirmed it's the OS, not the
binding), parameterized by pad index (instance pf_pad_<index>), waiting on the callback.
Per-session device: created on connect, SwDeviceClose'd on drop -- no leftovers, no phantom.
Live-verified on the RTX box: device materializes, the UMDF driver binds, SDL3 identifies it
as a PS5 ("DualSense Wireless Controller"), input flows; removed on disconnect. The
dualsense-windows-test CLI now cycles input + prints any 0x02 feedback for diagnosis.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,10 @@
|
|||||||
//! bytes. `hidclass` gates the device stack, so this user-mode IPC is the only viable channel (a
|
//! bytes. `hidclass` gates the device stack, so this user-mode IPC is the only viable channel (a
|
||||||
//! UMDF driver has no control device); see `windows-dualsense-scoping.md`.
|
//! UMDF driver has no control device); see `windows-dualsense-scoping.md`.
|
||||||
//!
|
//!
|
||||||
//! Device lifecycle: each pad `SwDeviceCreate`s the `root\pf_dualsense` devnode on open and
|
//! Device lifecycle: each pad `SwDeviceCreate`s a `pf_pad_<index>` software devnode (hardware id
|
||||||
//! `SwDeviceClose`s it on drop, so the virtual DualSense appears/disappears with the session —
|
//! `pf_dualsense`, enumerator `punktfunk`) on open and `SwDeviceClose`s it on drop, so the virtual
|
||||||
//! matching the Linux UHID pad. (The driver itself must already be installed; the installer stages it.)
|
//! DualSense appears/disappears with the session — matching the Linux UHID pad. (The driver itself
|
||||||
|
//! must already be installed; the installer stages it.)
|
||||||
|
|
||||||
use super::dualsense_proto::{
|
use super::dualsense_proto::{
|
||||||
parse_ds_output, serialize_state, DsFeedback, DsState, DS_INPUT_REPORT_LEN, DS_TOUCH_H,
|
parse_ds_output, serialize_state, DsFeedback, DsState, DS_INPUT_REPORT_LEN, DS_TOUCH_H,
|
||||||
@@ -46,9 +47,9 @@ const OFF_INPUT: usize = 8;
|
|||||||
const OFF_OUT_SEQ: usize = 72;
|
const OFF_OUT_SEQ: usize = 72;
|
||||||
const OFF_OUTPUT: usize = 76;
|
const OFF_OUTPUT: usize = 76;
|
||||||
|
|
||||||
/// A single virtual DualSense: the `root\pf_dualsense` software devnode (the driver loads on it and
|
/// A single virtual DualSense: the SwDeviceCreate'd `pf_pad_<index>` software devnode (the driver
|
||||||
/// the HID DualSense appears to games) plus the shared-memory section the driver maps. Dropping it
|
/// loads on it and the HID DualSense appears to games) plus the shared-memory section the driver maps.
|
||||||
/// removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
/// Dropping it removes the devnode (`SwDeviceClose`) and unmaps + closes the section.
|
||||||
struct DsWinPad {
|
struct DsWinPad {
|
||||||
/// Per-session devnode from SwDeviceCreate, when it succeeds. `None` falls back to an out-of-band
|
/// Per-session devnode from SwDeviceCreate, when it succeeds. `None` falls back to an out-of-band
|
||||||
/// `pf_dualsense` devnode (installer/devgen).
|
/// `pf_dualsense` devnode (installer/devgen).
|
||||||
@@ -60,16 +61,15 @@ struct DsWinPad {
|
|||||||
last_out_seq: u32,
|
last_out_seq: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Context for the async `SwDeviceCreate` completion callback: the event to signal + the result.
|
/// Context for the `SwDeviceCreate` completion callback: an event to signal + the HRESULT it reports.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct SwCreateCtx {
|
struct SwCreateCtx {
|
||||||
event: HANDLE,
|
event: HANDLE,
|
||||||
result: HRESULT,
|
result: HRESULT,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `SwDeviceCreate` fires this on a worker thread once the device is created. We stash the result and
|
/// `SwDeviceCreate` fires this once PnP has enumerated the device; stash the result and wake the
|
||||||
/// wake the waiting [`create_swdevice`]; the creator blocks on the event, so there's no concurrent
|
/// creator, which blocks on the event (so there's no concurrent access to `*ctx`).
|
||||||
/// access to `*ctx`.
|
|
||||||
unsafe extern "system" fn sw_create_cb(
|
unsafe extern "system" fn sw_create_cb(
|
||||||
_dev: HSWDEVICE,
|
_dev: HSWDEVICE,
|
||||||
result: HRESULT,
|
result: HRESULT,
|
||||||
@@ -77,27 +77,30 @@ unsafe extern "system" fn sw_create_cb(
|
|||||||
_id: PCWSTR,
|
_id: PCWSTR,
|
||||||
) {
|
) {
|
||||||
if !ctx.is_null() {
|
if !ctx.is_null() {
|
||||||
let c = ctx as *mut SwCreateCtx;
|
// SAFETY: ctx is the &mut SwCreateCtx the creator passed; it outlives this callback.
|
||||||
// SAFETY: c is the &mut SwCreateCtx the creator passed; it outlives this callback (the
|
|
||||||
// creator waits on the event before dropping it).
|
|
||||||
unsafe {
|
unsafe {
|
||||||
|
let c = ctx as *mut SwCreateCtx;
|
||||||
(*c).result = result;
|
(*c).result = result;
|
||||||
let _ = SetEvent((*c).event);
|
let _ = SetEvent((*c).event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn the virtual DualSense software device under enumerator `punktfunk` (hardware id
|
/// Spawn the per-session virtual DualSense devnode for pad `index` under enumerator `punktfunk`
|
||||||
/// `pf_dualsense`, which the INF matches). The returned `HSWDEVICE` owns the devnode for the session
|
/// (instance `pf_pad_<index>`, hardware id `pf_dualsense` which the INF matches). The returned
|
||||||
/// — `SwDeviceClose` removes it.
|
/// `HSWDEVICE` owns it — `SwDeviceClose` removes it on drop, so the pad appears/disappears with the
|
||||||
|
/// session and nothing persists.
|
||||||
///
|
///
|
||||||
/// NB: enumerator names with an underscore (`pf_dualsense`) get E_INVALIDARG — hence `punktfunk`.
|
/// Two requirements each yield E_INVALIDARG if violated: the enumerator name must not contain `_`
|
||||||
/// TODO: a SECOND E_INVALIDARG remains — passing the completion callback is rejected (callback-absent
|
/// (hence `punktfunk`, not `pf_dualsense`), and the completion callback is mandatory (the docs mark
|
||||||
/// is accepted but then the devnode doesn't materialize). Until that's resolved [`DsWinPad::open`]
|
/// `pCallback` as `[in]`, not optional — a NULL callback is rejected). The caller must be
|
||||||
/// treats a failure as non-fatal and relies on an out-of-band `pf_dualsense` devnode (installer /
|
/// Administrator (the host service runs as LocalSystem).
|
||||||
/// dev-box `devgen`); see `docs/windows-dualsense-scoping.md`.
|
fn create_swdevice(index: u8) -> Result<HSWDEVICE> {
|
||||||
fn create_swdevice() -> Result<HSWDEVICE> {
|
|
||||||
let hwids: Vec<u16> = "pf_dualsense".encode_utf16().chain([0u16, 0u16]).collect();
|
let hwids: Vec<u16> = "pf_dualsense".encode_utf16().chain([0u16, 0u16]).collect();
|
||||||
|
let instid: Vec<u16> = format!("pf_pad_{index}")
|
||||||
|
.encode_utf16()
|
||||||
|
.chain(std::iter::once(0))
|
||||||
|
.collect();
|
||||||
let desc: Vec<u16> = "punktfunk Virtual DualSense"
|
let desc: Vec<u16> = "punktfunk Virtual DualSense"
|
||||||
.encode_utf16()
|
.encode_utf16()
|
||||||
.chain(std::iter::once(0))
|
.chain(std::iter::once(0))
|
||||||
@@ -105,10 +108,10 @@ fn create_swdevice() -> Result<HSWDEVICE> {
|
|||||||
// SAFETY: zeroed then the fields we use are set; cbSize identifies the struct version.
|
// SAFETY: zeroed then the fields we use are set; cbSize identifies the struct version.
|
||||||
let mut info: SW_DEVICE_CREATE_INFO = unsafe { std::mem::zeroed() };
|
let mut info: SW_DEVICE_CREATE_INFO = unsafe { std::mem::zeroed() };
|
||||||
info.cbSize = std::mem::size_of::<SW_DEVICE_CREATE_INFO>() as u32;
|
info.cbSize = std::mem::size_of::<SW_DEVICE_CREATE_INFO>() as u32;
|
||||||
|
info.pszInstanceId = PCWSTR(instid.as_ptr());
|
||||||
info.pszzHardwareIds = PCWSTR(hwids.as_ptr());
|
info.pszzHardwareIds = PCWSTR(hwids.as_ptr());
|
||||||
info.pszDeviceDescription = PCWSTR(desc.as_ptr());
|
info.pszDeviceDescription = PCWSTR(desc.as_ptr());
|
||||||
// SWDeviceCapabilities: DriverRequired (8) | SilentInstall (2) | Removable (1).
|
info.CapabilityFlags = 0x0000_000B; // DriverRequired | SilentInstall | Removable
|
||||||
info.CapabilityFlags = 0x0000_000B;
|
|
||||||
|
|
||||||
// SAFETY: a manual-reset, initially-unsignaled, unnamed event.
|
// SAFETY: a manual-reset, initially-unsignaled, unnamed event.
|
||||||
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null())? };
|
let event = unsafe { CreateEventW(None, true, false, PCWSTR::null())? };
|
||||||
@@ -116,7 +119,7 @@ fn create_swdevice() -> Result<HSWDEVICE> {
|
|||||||
event,
|
event,
|
||||||
result: HRESULT(0),
|
result: HRESULT(0),
|
||||||
};
|
};
|
||||||
// SAFETY: info + hwids/desc outlive the call; ctx outlives the callback (we wait below).
|
// SAFETY: info + the buffers + ctx outlive the call (we wait on the event before returning);
|
||||||
// windows-rs returns the HSWDEVICE (the C out-param) as the Result value.
|
// windows-rs returns the HSWDEVICE (the C out-param) as the Result value.
|
||||||
let hsw = match unsafe {
|
let hsw = match unsafe {
|
||||||
SwDeviceCreate(
|
SwDeviceCreate(
|
||||||
@@ -137,7 +140,8 @@ fn create_swdevice() -> Result<HSWDEVICE> {
|
|||||||
return Err(anyhow!("SwDeviceCreate failed: {e}"));
|
return Err(anyhow!("SwDeviceCreate failed: {e}"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// SAFETY: event is valid; block up to 10s for the creation callback.
|
// Block until PnP finishes enumerating (the callback signals), then check its result.
|
||||||
|
// SAFETY: event is valid.
|
||||||
unsafe {
|
unsafe {
|
||||||
WaitForSingleObject(event, 10_000);
|
WaitForSingleObject(event, 10_000);
|
||||||
let _ = CloseHandle(event);
|
let _ = CloseHandle(event);
|
||||||
@@ -145,7 +149,10 @@ fn create_swdevice() -> Result<HSWDEVICE> {
|
|||||||
if ctx.result.is_err() {
|
if ctx.result.is_err() {
|
||||||
// SAFETY: hsw is the handle SwDeviceCreate returned.
|
// SAFETY: hsw is the handle SwDeviceCreate returned.
|
||||||
unsafe { SwDeviceClose(hsw) };
|
unsafe { SwDeviceClose(hsw) };
|
||||||
return Err(anyhow!("SwDeviceCreate callback reported {:?}", ctx.result));
|
return Err(anyhow!(
|
||||||
|
"SwDeviceCreate enumeration failed: {:?}",
|
||||||
|
ctx.result
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(hsw)
|
Ok(hsw)
|
||||||
}
|
}
|
||||||
@@ -207,13 +214,13 @@ impl DsWinPad {
|
|||||||
});
|
});
|
||||||
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
std::ptr::write_unaligned(base as *mut u32, SHM_MAGIC);
|
||||||
}
|
}
|
||||||
// Best-effort: spawn a per-session devnode via SwDeviceCreate. It currently fails with a
|
// Spawn the per-session devnode via SwDeviceCreate; `SwDeviceClose` removes it on drop. On the
|
||||||
// SwDevice quirk (see create_swdevice), so on failure we keep the section + data plane and
|
// rare failure we keep the section + data plane and fall back to an out-of-band `pf_dualsense`
|
||||||
// rely on an out-of-band `pf_dualsense` devnode (installer / dev-box devgen).
|
// devnode (installer / dev-box devgen).
|
||||||
let hsw = match create_swdevice() {
|
let hsw = match create_swdevice(index) {
|
||||||
Ok(h) => Some(h),
|
Ok(h) => Some(h),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; using an out-of-band pf_dualsense devnode");
|
tracing::warn!(error = %format!("{e:#}"), "SwDeviceCreate failed; falling back to an out-of-band pf_dualsense devnode");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -224,26 +224,41 @@ fn real_main() -> Result<()> {
|
|||||||
kind: 2,
|
kind: 2,
|
||||||
capabilities: 0,
|
capabilities: 0,
|
||||||
});
|
});
|
||||||
// ls_x 16384 → report byte1 0xC0; BTN_A (Cross) → report byte8 0x28.
|
|
||||||
mgr.handle(&GamepadEvent::State(GamepadFrame {
|
|
||||||
index: 0,
|
|
||||||
active_mask: 1,
|
|
||||||
buttons: punktfunk_core::input::gamepad::BTN_A,
|
|
||||||
left_trigger: 0,
|
|
||||||
right_trigger: 0,
|
|
||||||
ls_x: 16384,
|
|
||||||
ls_y: 0,
|
|
||||||
rs_x: 0,
|
|
||||||
rs_y: 0,
|
|
||||||
}));
|
|
||||||
println!(
|
println!(
|
||||||
"virtual DualSense created via SwDeviceCreate (VID 054C/PID 0CE6). Holding {secs}s — \
|
"virtual DualSense up — cycling Cross + sweeping the left stick for {secs}s. Watch it \
|
||||||
verify Get-PnpDevice VID_054C + a HID read (expect byte1=0xC0, byte8=0x28)."
|
in joy.cpl / Steam / a game; any rumble / lightbar / trigger the game sends prints below."
|
||||||
);
|
);
|
||||||
let deadline = Instant::now() + Duration::from_secs(secs);
|
let deadline = Instant::now() + Duration::from_secs(secs);
|
||||||
|
let (mut i, mut last) = (0i32, Instant::now());
|
||||||
while Instant::now() < deadline {
|
while Instant::now() < deadline {
|
||||||
mgr.pump(|_, _, _| {}, |_| {});
|
// Surface a game's feedback: rumble (universal) + lightbar / player-LED / adaptive
|
||||||
std::thread::sleep(Duration::from_millis(50));
|
// triggers (DualSense-only) coming back over the shared section.
|
||||||
|
mgr.pump(
|
||||||
|
|pad, lo, hi| println!(" rumble from game: pad={pad} low={lo} high={hi}"),
|
||||||
|
|o| println!(" hid output from game: {o:?}"),
|
||||||
|
);
|
||||||
|
if last.elapsed() >= Duration::from_millis(400) {
|
||||||
|
last = Instant::now();
|
||||||
|
i += 1;
|
||||||
|
let buttons = if i % 2 == 0 {
|
||||||
|
punktfunk_core::input::gamepad::BTN_A // Cross
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let lx = (((i % 64) - 32) * 1024) as i16; // sweep left stick X
|
||||||
|
mgr.handle(&GamepadEvent::State(GamepadFrame {
|
||||||
|
index: 0,
|
||||||
|
active_mask: 1,
|
||||||
|
buttons,
|
||||||
|
left_trigger: 0,
|
||||||
|
right_trigger: 0,
|
||||||
|
ls_x: lx,
|
||||||
|
ls_y: 0,
|
||||||
|
rs_x: 0,
|
||||||
|
rs_y: 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(15));
|
||||||
}
|
}
|
||||||
println!("dualsense-windows-test: done (devnode removed)");
|
println!("dualsense-windows-test: done (devnode removed)");
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user