feat(gamepad): SwDeviceCreate per-session devnode (best-effort) + windows self-test

DualSenseWindowsManager now SwDeviceCreate's the pf_dualsense devnode per session
(SwDeviceClose on drop), matching the Linux UHID pad's lifecycle. It's best-effort:
SwDeviceCreate currently hits an unresolved E_INVALIDARG when a completion callback is
passed (an underscore in the enumerator name was a second cause, fixed by using
"punktfunk"), so on failure the host keeps the section + data plane and falls back to
an out-of-band devnode (installer/devgen) — see docs/windows-dualsense-scoping.md.

Add a `dualsense-windows-test` host CLI that drives the manager (create devnode + push
a frame + hold), used to validate the path. Live on the RTX box: the manager creates
the section + pushes report 0x01 and a devnode serves it to a HID read (b1=0xC0,
b8=0x28) — the host-side data plane works end to end.

cargo check + clippy -D warnings clean on x86_64-pc-windows-msvc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:34:00 +00:00
parent 01dc0b616c
commit fde438a1ed
4 changed files with 185 additions and 11 deletions
+47
View File
@@ -201,6 +201,53 @@ fn real_main() -> Result<()> {
println!("dualsense-test: done");
Ok(())
}
// Windows: create a virtual DualSense via the UMDF driver (SwDeviceCreate per-session devnode
// + the shared-memory channel) and hold it, pushing one fixed frame (Cross + LS-right). Drives
// the real DualSenseWindowsManager, so it validates the device lifecycle end to end. Verify
// while it holds: `Get-PnpDevice` shows a VID_054C device, and a HID read returns the pushed
// report (byte1=0xC0, byte8=0x28). On exit the pad drops → SwDeviceClose removes the devnode.
#[cfg(target_os = "windows")]
Some("dualsense-windows-test") => {
use crate::gamestream::gamepad::{GamepadEvent, GamepadFrame};
use inject::dualsense_windows::DualSenseWindowsManager;
use std::time::{Duration, Instant};
let secs: u64 = args
.iter()
.skip_while(|a| *a != "--seconds")
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(20);
let mut mgr = DualSenseWindowsManager::new();
// Arrival creates the pad (SwDeviceCreate + section); State pushes the report.
mgr.handle(&GamepadEvent::Arrival {
index: 0,
kind: 2,
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!(
"virtual DualSense created via SwDeviceCreate (VID 054C/PID 0CE6). Holding {secs}s — \
verify Get-PnpDevice VID_054C + a HID read (expect byte1=0xC0, byte8=0x28)."
);
let deadline = Instant::now() + Duration::from_secs(secs);
while Instant::now() < deadline {
mgr.pump(|_, _, _| {}, |_| {});
std::thread::sleep(Duration::from_millis(50));
}
println!("dualsense-windows-test: done (devnode removed)");
Ok(())
}
// Capture→encode→file pipeline spike (dev tool).
Some("spike") => spike::run(parse_spike(&args[1..])?),
// Native punktfunk/1 host (QUIC control plane + UDP data plane).